排序算法:Magento结算总额排序错误导致运费税计算错误

38
在Magento中,有一种功能可以通过指定某个总计在哪些总计之前或之后运行来定义总计的顺序。
我添加了一个自定义总计,如果我将以下行添加到config.xml中,则排序会出错。 错误意味着:“tax_shipping”应该位于“shipping”之后。这会导致运输成本的税费被加倍。
但这违反了条件。
tax_shipping
after: shipping

我猜测:在完整的规则集中一定存在某些矛盾。但是我该如何找到它呢?
这是我添加的唯一规则。如果没有这个规则,tax_shipping将在shipping之后排序。
<shippingprotectiontax>
    <class>n98_shippingprotection/quote_address_total_shippingprotectionTax</class>
    <after>subtotal,discount,shipping,tax</after>
    <before>grand_total</before>
</shippingprotectiontax>

以下是由Mage_Sales_Model_Quote_Address_Total_Collector::_getSortedCollectorCodes()中的usort调用返回的排序数组。 对于那些没有Magento安装的人,代码如下:

/**
 * uasort callback function
 *
 * @param   array $a
 * @param   array $b
 * @return  int
 */
protected function _compareTotals($a, $b)
{
    $aCode = $a['_code'];
    $bCode = $b['_code'];
    if (in_array($aCode, $b['after']) || in_array($bCode, $a['before'])) {
        $res = -1;
    } elseif (in_array($bCode, $a['after']) || in_array($aCode, $b['before'])) {
        $res = 1;
    } else {
        $res = 0;
    }
    return $res;
}

protected function _getSortedCollectorCodes()
{

    ...

    uasort($configArray, array($this, '_compareTotals'));
    Mage::log('Sorted:');

    // this produces the output below
    $loginfo = "";
    foreach($configArray as $code=>$data) {
        $loginfo .= "$code\n";
        $loginfo .= "after: ".implode(',',$data['after'])."\n";
        $loginfo .= "before: ".implode(',',$data['before'])."\n";
        $loginfo .= "\n";
    }
    Mage::log($loginfo);

    ...

日志输出:

nominal
after: 
before: subtotal,grand_total

subtotal
after: nominal
before: grand_total,shipping,freeshipping,tax_subtotal,discount,tax,weee,giftwrapping,cashondelivery,cashondelivery_tax,shippingprotection,shippingprotectiontax

freeshipping
after: subtotal,nominal
before: tax_subtotal,shipping,grand_total,tax,discount

tax_shipping
after: shipping,subtotal,freeshipping,tax_subtotal,nominal
before: tax,discount,grand_total,grand_total

giftwrapping
after: subtotal,nominal
before: 

tax_subtotal
after: freeshipping,subtotal,subtotal,nominal
before: tax,discount,shipping,grand_total,weee,customerbalance,giftcardaccount,reward

weee
after: subtotal,tax_subtotal,nominal,freeshipping,subtotal,subtotal,nominal
before: tax,discount,grand_total,grand_total,tax

shipping
after: subtotal,freeshipping,tax_subtotal,nominal
before: grand_total,discount,tax_shipping,tax,cashondelivery,cashondelivery_tax,shippingprotection,shippingprotectiontax

discount
after: subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee
before: grand_total,tax,customerbalance,giftcardaccount,reward,cashondelivery,cashondelivery_tax,shippingprotection,shippingprotectiontax

cashondelivery
after: subtotal,discount,shipping,nominal,subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee,subtotal,freeshipping,tax_subtotal,nominal
before: tax,grand_total,grand_total,customerbalance,giftcardaccount,tax_giftwrapping,reward,customerbalance,giftcardaccount,reward

shippingprotection
after: subtotal,discount,shipping,nominal,subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee,subtotal,freeshipping,tax_subtotal,nominal
before: tax,grand_total,grand_total,customerbalance,giftcardaccount,tax_giftwrapping,reward,cashondelivery_tax,customerbalance,giftcardaccount,reward

tax
after: subtotal,shipping,discount,tax_subtotal,freeshipping,tax_shipping,nominal,weee,cashondelivery,shippingprotection
before: grand_total,customerbalance,giftcardaccount,tax_giftwrapping,reward,cashondelivery_tax,shippingprotectiontax

shippingprotectiontax
after: subtotal,discount,shipping,tax,nominal,subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee,subtotal,freeshipping,tax_subtotal,nominal,subtotal,shipping,discount,tax_subtotal,freeshipping,tax_shipping,nominal,weee,cashondelivery,shippingprotection
before: grand_total,customerbalance,giftcardaccount,reward

cashondelivery_tax
after: subtotal,discount,shipping,tax,nominal,subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee,subtotal,freeshipping,tax_subtotal,nominal,subtotal,shipping,discount,tax_subtotal,freeshipping,tax_shipping,nominal,weee,cashondelivery
before: grand_total,customerbalance,giftcardaccount,reward

tax_giftwrapping
after: tax,subtotal,shipping,discount,tax_subtotal,freeshipping,tax_shipping,nominal,weee
before: grand_total,customerbalance,giftcardaccount

grand_total
after: subtotal,nominal,shipping,freeshipping,tax_subtotal,discount,tax,tax_giftwrapping,cashondelivery,cashondelivery_tax,shippingprotection,shippingprotectiontax
before: customerbalance,giftcardaccount,reward

reward
after: wee,discount,tax,tax_subtotal,grand_total,subtotal,shipping,nominal,freeshipping,tax_subtotal,tax_shipping,weee,subtotal,shipping,discount,tax_subtotal,freeshipping,tax_shipping,nominal,weee,freeshipping,subtotal,subtotal,nominal,subtotal,nominal,shipping,freeshipping,tax_subtotal,discount,tax,tax_giftwrapping
before: giftcardaccount,customerbalance,customerbalance

giftcardaccount
after: wee,discount,tax,tax_subtotal,grand_total,reward,subtotal,shipping,nominal,freeshipping,tax_shipping,weee
before: customerbalance

customerbalance
after: wee,discount,tax,tax_subtotal,grand_total,reward,giftcardaccount,subtotal,shipping,nominal,freeshipping,tax_shipping,weee
before: 

编辑:

在Vinai的回答之后,我添加了更多的调试代码

$fp = fopen('/tmp/dotfile','w');
fwrite($fp,"digraph TotalOrder\n");
fwrite($fp,"{\n");
foreach($configArray as $code=>$data) {
    $_code = $data['_code'];
    foreach($data['before'] as $beforeCode) {
        fwrite($fp,"$beforeCode -> $_code;\n");
    }
    foreach($data['after'] as $afterCode) {
        fwrite($fp,"$_code -> $afterCode;\n");
    }
}
fwrite($fp,"}\n");
fclose($fp);

使用Graphviz进行可视化:dot -Tpng dotfile > viz.png。这是第一次尝试的结果,在排序后调用。

可视化

编辑2:

我认为这没什么用。

因此,在合并前/后条目之前,我对数组进行了可视化。(就在$configArray = $this->_modelsConfig;之后)

这是没有我的shippingprotectiontax条目的情况:

输入图像描述

这是有我的shippingprotectiontax条目的情况:

输入图像描述

我没有看到任何明显的矛盾。

编辑3:

在uasort之前的配置数组:

array (
  'nominal' => 
  array (
    'class' => 'sales/quote_address_total_nominal',
    'before' => 
    array (
      0 => 'subtotal',
      1 => 'grand_total',
    ),
    'renderer' => 'checkout/total_nominal',
    'after' => 
    array (
    ),
    '_code' => 'nominal',
  ),
  'subtotal' => 
  array (
    'class' => 'sales/quote_address_total_subtotal',
    'after' => 
    array (
      0 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'shipping',
      2 => 'freeshipping',
      3 => 'tax_subtotal',
      4 => 'discount',
      5 => 'tax',
      6 => 'weee',
      7 => 'giftwrapping',
      8 => 'cashondelivery',
      9 => 'cashondelivery_tax',
      10 => 'shippingprotection',
      11 => 'shippingprotectiontax',
    ),
    'renderer' => 'tax/checkout_subtotal',
    'admin_renderer' => 'adminhtml/sales_order_create_totals_subtotal',
    '_code' => 'subtotal',
  ),
  'shipping' => 
  array (
    'class' => 'sales/quote_address_total_shipping',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'freeshipping',
      2 => 'tax_subtotal',
      3 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'discount',
      2 => 'tax_shipping',
      3 => 'tax',
      4 => 'cashondelivery',
      5 => 'cashondelivery_tax',
      6 => 'shippingprotection',
      7 => 'shippingprotectiontax',
    ),
    'renderer' => 'tax/checkout_shipping',
    'admin_renderer' => 'adminhtml/sales_order_create_totals_shipping',
    '_code' => 'shipping',
  ),
  'grand_total' => 
  array (
    'class' => 'sales/quote_address_total_grand',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'nominal',
      2 => 'shipping',
      3 => 'freeshipping',
      4 => 'tax_subtotal',
      5 => 'discount',
      6 => 'tax',
      7 => 'tax_giftwrapping',
      8 => 'cashondelivery',
      9 => 'cashondelivery_tax',
      10 => 'shippingprotection',
      11 => 'shippingprotectiontax',
    ),
    'renderer' => 'tax/checkout_grandtotal',
    'admin_renderer' => 'adminhtml/sales_order_create_totals_grandtotal',
    'before' => 
    array (
      0 => 'customerbalance',
      1 => 'giftcardaccount',
      2 => 'reward',
    ),
    '_code' => 'grand_total',
  ),
  'freeshipping' => 
  array (
    'class' => 'salesrule/quote_freeshipping',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax_subtotal',
      1 => 'shipping',
      2 => 'grand_total',
      3 => 'tax',
      4 => 'discount',
    ),
    '_code' => 'freeshipping',
  ),
  'discount' => 
  array (
    'class' => 'salesrule/quote_discount',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'shipping',
      2 => 'nominal',
      3 => 'freeshipping',
      4 => 'tax_subtotal',
      5 => 'tax_shipping',
      6 => 'weee',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'tax',
      2 => 'customerbalance',
      3 => 'giftcardaccount',
      4 => 'reward',
      5 => 'cashondelivery',
      6 => 'cashondelivery_tax',
      7 => 'shippingprotection',
      8 => 'shippingprotectiontax',
    ),
    'renderer' => 'tax/checkout_discount',
    'admin_renderer' => 'adminhtml/sales_order_create_totals_discount',
    '_code' => 'discount',
  ),
  'tax_subtotal' => 
  array (
    'class' => 'tax/sales_total_quote_subtotal',
    'after' => 
    array (
      0 => 'freeshipping',
      1 => 'subtotal',
      2 => 'subtotal',
      3 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax',
      1 => 'discount',
      2 => 'shipping',
      3 => 'grand_total',
      4 => 'weee',
      5 => 'customerbalance',
      6 => 'giftcardaccount',
      7 => 'reward',
    ),
    '_code' => 'tax_subtotal',
  ),
  'tax_shipping' => 
  array (
    'class' => 'tax/sales_total_quote_shipping',
    'after' => 
    array (
      0 => 'shipping',
      1 => 'subtotal',
      2 => 'freeshipping',
      3 => 'tax_subtotal',
      4 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax',
      1 => 'discount',
      2 => 'grand_total',
      3 => 'grand_total',
    ),
    '_code' => 'tax_shipping',
  ),
  'tax' => 
  array (
    'class' => 'tax/sales_total_quote_tax',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'shipping',
      2 => 'discount',
      3 => 'tax_subtotal',
      4 => 'freeshipping',
      5 => 'tax_shipping',
      6 => 'nominal',
      7 => 'weee',
      8 => 'cashondelivery',
      9 => 'shippingprotection',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'customerbalance',
      2 => 'giftcardaccount',
      3 => 'tax_giftwrapping',
      4 => 'reward',
      5 => 'cashondelivery_tax',
      6 => 'shippingprotectiontax',
    ),
    'renderer' => 'tax/checkout_tax',
    'admin_renderer' => 'adminhtml/sales_order_create_totals_tax',
    '_code' => 'tax',
  ),
  'weee' => 
  array (
    'class' => 'weee/total_quote_weee',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'tax_subtotal',
      2 => 'nominal',
      3 => 'freeshipping',
      4 => 'subtotal',
      5 => 'subtotal',
      6 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax',
      1 => 'discount',
      2 => 'grand_total',
      3 => 'grand_total',
      4 => 'tax',
    ),
    '_code' => 'weee',
  ),
  'customerbalance' => 
  array (
    'class' => 'enterprise_customerbalance/total_quote_customerbalance',
    'after' => 
    array (
      0 => 'wee',
      1 => 'discount',
      2 => 'tax',
      3 => 'tax_subtotal',
      4 => 'grand_total',
      5 => 'reward',
      6 => 'giftcardaccount',
      7 => 'subtotal',
      8 => 'shipping',
      9 => 'nominal',
      10 => 'freeshipping',
      11 => 'tax_shipping',
      12 => 'weee',
    ),
    'renderer' => 'enterprise_customerbalance/checkout_total',
    'before' => 
    array (
    ),
    '_code' => 'customerbalance',
  ),
  'giftcardaccount' => 
  array (
    'class' => 'enterprise_giftcardaccount/total_quote_giftcardaccount',
    'after' => 
    array (
      0 => 'wee',
      1 => 'discount',
      2 => 'tax',
      3 => 'tax_subtotal',
      4 => 'grand_total',
      5 => 'reward',
      6 => 'subtotal',
      7 => 'shipping',
      8 => 'nominal',
      9 => 'freeshipping',
      11 => 'tax_shipping',
      12 => 'weee',
    ),
    'before' => 
    array (
      0 => 'customerbalance',
    ),
    'renderer' => 'enterprise_giftcardaccount/checkout_cart_total',
    '_code' => 'giftcardaccount',
  ),
  'giftwrapping' => 
  array (
    'class' => 'enterprise_giftwrapping/total_quote_giftwrapping',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'nominal',
    ),
    'renderer' => 'enterprise_giftwrapping/checkout_totals',
    'before' => 
    array (
    ),
    '_code' => 'giftwrapping',
  ),
  'tax_giftwrapping' => 
  array (
    'class' => 'enterprise_giftwrapping/total_quote_tax_giftwrapping',
    'after' => 
    array (
      0 => 'tax',
      1 => 'subtotal',
      2 => 'shipping',
      3 => 'discount',
      4 => 'tax_subtotal',
      5 => 'freeshipping',
      6 => 'tax_shipping',
      7 => 'nominal',
      8 => 'weee',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'customerbalance',
      2 => 'giftcardaccount',
    ),
    '_code' => 'tax_giftwrapping',
  ),
  'reward' => 
  array (
    'class' => 'enterprise_reward/total_quote_reward',
    'after' => 
    array (
      0 => 'wee',
      1 => 'discount',
      2 => 'tax',
      3 => 'tax_subtotal',
      4 => 'grand_total',
      5 => 'subtotal',
      6 => 'shipping',
      7 => 'nominal',
      8 => 'freeshipping',
      9 => 'tax_subtotal',
      10 => 'tax_shipping',
      11 => 'weee',
      12 => 'subtotal',
      13 => 'shipping',
      14 => 'discount',
      15 => 'tax_subtotal',
      16 => 'freeshipping',
      17 => 'tax_shipping',
      18 => 'nominal',
      19 => 'weee',
      20 => 'freeshipping',
      21 => 'subtotal',
      22 => 'subtotal',
      23 => 'nominal',
      24 => 'subtotal',
      25 => 'nominal',
      26 => 'shipping',
      27 => 'freeshipping',
      28 => 'tax_subtotal',
      29 => 'discount',
      30 => 'tax',
      31 => 'tax_giftwrapping',
    ),
    'before' => 
    array (
      0 => 'giftcardaccount',
      1 => 'customerbalance',
      2 => 'customerbalance',
    ),
    'renderer' => 'enterprise_reward/checkout_total',
    '_code' => 'reward',
  ),
  'cashondelivery' => 
  array (
    'class' => 'cashondelivery/quote_total',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'discount',
      2 => 'shipping',
      3 => 'nominal',
      4 => 'subtotal',
      5 => 'shipping',
      6 => 'nominal',
      7 => 'freeshipping',
      8 => 'tax_subtotal',
      9 => 'tax_shipping',
      10 => 'weee',
      11 => 'subtotal',
      12 => 'freeshipping',
      13 => 'tax_subtotal',
      14 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax',
      1 => 'grand_total',
      2 => 'grand_total',
      3 => 'customerbalance',
      4 => 'giftcardaccount',
      5 => 'tax_giftwrapping',
      6 => 'reward',
      7 => 'customerbalance',
      8 => 'giftcardaccount',
      9 => 'reward',
    ),
    'renderer' => 'cashondelivery/checkout_cod',
    'admin_renderer' => 'cashondelivery/adminhtml_sales_order_create_totals_cod',
    '_code' => 'cashondelivery',
  ),
  'cashondelivery_tax' => 
  array (
    'class' => 'cashondelivery/quote_taxTotal',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'discount',
      2 => 'shipping',
      3 => 'tax',
      4 => 'nominal',
      5 => 'subtotal',
      6 => 'shipping',
      7 => 'nominal',
      8 => 'freeshipping',
      9 => 'tax_subtotal',
      10 => 'tax_shipping',
      11 => 'weee',
      12 => 'subtotal',
      13 => 'freeshipping',
      14 => 'tax_subtotal',
      15 => 'nominal',
      16 => 'subtotal',
      17 => 'shipping',
      18 => 'discount',
      19 => 'tax_subtotal',
      20 => 'freeshipping',
      21 => 'tax_shipping',
      22 => 'nominal',
      23 => 'weee',
      24 => 'cashondelivery',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'customerbalance',
      2 => 'giftcardaccount',
      3 => 'reward',
    ),
    '_code' => 'cashondelivery_tax',
  ),
  'shippingprotection' => 
  array (
    'class' => 'n98_shippingprotection/quote_address_total_shippingprotection',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'discount',
      2 => 'shipping',
      3 => 'nominal',
      4 => 'subtotal',
      5 => 'shipping',
      6 => 'nominal',
      7 => 'freeshipping',
      8 => 'tax_subtotal',
      9 => 'tax_shipping',
      10 => 'weee',
      11 => 'subtotal',
      12 => 'freeshipping',
      13 => 'tax_subtotal',
      14 => 'nominal',
    ),
    'before' => 
    array (
      0 => 'tax',
      1 => 'grand_total',
      2 => 'grand_total',
      3 => 'customerbalance',
      4 => 'giftcardaccount',
      5 => 'tax_giftwrapping',
      6 => 'reward',
      7 => 'cashondelivery_tax',
      8 => 'customerbalance',
      9 => 'giftcardaccount',
      10 => 'reward',
    ),
    '_code' => 'shippingprotection',
  ),
  'shippingprotectiontax' => 
  array (
    'class' => 'n98_shippingprotection/quote_address_total_shippingprotectionTax',
    'after' => 
    array (
      0 => 'subtotal',
      1 => 'discount',
      2 => 'shipping',
      3 => 'tax',
      4 => 'nominal',
      5 => 'subtotal',
      6 => 'shipping',
      7 => 'nominal',
      8 => 'freeshipping',
      9 => 'tax_subtotal',
      10 => 'tax_shipping',
      11 => 'weee',
      12 => 'subtotal',
      13 => 'freeshipping',
      14 => 'tax_subtotal',
      15 => 'nominal',
      16 => 'subtotal',
      17 => 'shipping',
      18 => 'discount',
      19 => 'tax_subtotal',
      20 => 'freeshipping',
      21 => 'tax_shipping',
      22 => 'nominal',
      23 => 'weee',
      24 => 'cashondelivery',
      25 => 'shippingprotection',
    ),
    'before' => 
    array (
      0 => 'grand_total',
      1 => 'customerbalance',
      2 => 'giftcardaccount',
      3 => 'reward',
    ),
    '_code' => 'shippingprotectiontax',
  ),
)

更新:Magento故障票据:https://jira.magento.com/browse/MCACE-129


1
我相信你是对的。一定存在矛盾。构建依赖图以查找错误并不是一件简单的事情,往往需要耗费大量时间。我认为我会使用像http://www.graphviz.org/这样的工具来处理这些数据。与其记录日志,不如从PHP代码生成一个DOT文件作为GraphViz的输入。这只是一个想法。 - Vinai
看到_getSortedCollectorCodes()方法,我注意到array_unique()不起作用,例如tax_shipping的合并前数组包含两个grand_total条目。输入需要排序吗? - Vinai
1
目前我的工作理论是问题源于uasort()实现不维护相同记录的顺序(不稳定排序)。你能发布$ configArray作为PHP的var_export($configArray)吗?这将有助于调试排序而无需重新创建问题。 - Vinai
1
请使用 var_export($configArray),而不是 var_dump() 或 print_r(),这样的结果是可以直接复制并粘贴到测试脚本中的 PHP 代码。谢谢! - Vinai
不错 @Alex,现在使用PHP7,过去没有真正修复的错误搜索回调会退回到feets上:PHP 5.5和PHP 7.0中函数uasort的不同行为 在SO上和Magento Grand Total without taxes in 1.9 with PHP7 在Magento SE上。 - hakre
显示剩余2条评论
6个回答

20
感谢坚持不懈的@Alex,这里有一个更好的答案和更好的解释 :) 我的第一个答案是错误的。
PHP为所有数组排序函数实现quicksort(参考zend_qsort.c)。如果数组中的两个记录相同,则它们的位置将被交换。
问题在于giftwrap总记录,根据_compareTotals(),它比subtotalnominal大,但等于所有其他总数
根据$confArray输入数组的原始顺序和枢轴元素的位置,可以合法地将giftwrap与例如discount交换,因为两者相等,即使discount大于shipping

从排序算法的角度来看,这可能会更清楚地解决问题:

  • shipping < tax_shipping
  • giftwrapping == shipping
  • giftwrapping == tax_shipping

虽然原始问题是选择快速排序构建有向无环依赖图,但有几种可能的解决方案:

  • 一种(不好的、短期的)解决方案是向giftwrapping总数添加更多的依赖关系,即使可能仍然存在其他总数的问题,只是目前还没有出现。
  • 真正的解决方案是为该问题实现拓扑排序算法。
有趣的是,PHP包并不多。有一个孤立无援的PEAR包Structures_Graph。使用它可能是一个快速的解决方案,但这意味着将$confArray转换为Structures_Graph结构(所以可能不是那么快)。
维基百科做了一个很好的解释问题的工作,因此自己编写解决方案可能是一个有趣的挑战。德语维基百科拓扑排序页面将问题分解为逻辑步骤,并且还有一个PERL示例算法。

现在有 https://github.com/marcj/topsort.php :-) - Alex

17

最终,这是我为这个问题准备的补丁。

它实现了Vinai建议的拓扑排序。

  1. app/code/core/Mage/Sales/Model/Config/Ordered.php复制到app/code/local/Mage/Sales/Model/Config/Ordered.php
  2. 将补丁内容保存到total-sorting.patch文件中,并执行patch -p0 app/code/local/Mage/Sales/Model/Config/Ordered.php

在升级时,请确保重新应用这些步骤。

这个补丁已经测试可以与Magento 1.7.0.2一起使用。

--- app/code/core/Mage/Sales/Model/Config/Ordered.php   2012-08-14 14:19:50.306504947 +0200
+++ app/code/local/Mage/Sales/Model/Config/Ordered.php  2012-08-15 10:00:47.027003404 +0200
@@ -121,6 +121,78 @@
         return $totalConfig;
     }
+// [PATCHED CODE BEGIN] + + /** + * 拓扑排序 + * + * 版权:http://www.calcatraz.com/blog/php-topological-sort-function-384 + * 修复请参见:https://dev59.com/iWfWa4cB1Zd3GeqPfUIs + * + * @param $nodeids 节点ID列表 + * @param $edges 边的数组。每条边都指定为一个具有两个元素的数组:边的源节点和目标节点 + * @return array|null + */ + function topological_sort($nodeids, $edges) { + $L = $S = $nodes = array(); + foreach($nodeids as $id) { + $nodes[$id] = array('in'=>array(), 'out'=>array()); + foreach($edges as $e) { + if ($id==$e[0]) { $nodes[$id]['out'][]=$e[1]; } /** * 对配置数组进行排序 * * 通过构造图,对配置项之间的 before/after 关系进行分析,返回一个排好序的数组。 * * @param $configArray 配置数组 * @return array 排序后的数组 */ public function sortConfigArray($configArray) { // 构造节点和边列表 $nodes = array_keys($configArray); $edges = array();
foreach ($configArray as $code => $data) { $_code = $data['_code']; if (!isset($configArray[$_code])) continue; foreach ($data['before'] as $beforeCode) { if (!isset($configArray[$beforeCode])) continue; $edges[] = array($_code, $beforeCode); }
foreach ($data['after'] as $afterCode) { if (!isset($configArray[$afterCode])) continue; $edges[] = array($afterCode, $_code); } }
// 使用拓扑排序算法进行排序 $sortedNodes = $this->topological_sort($nodes, $edges);
// 按照排序结果重新构造数组 $result = array(); foreach ($sortedNodes as $node) { $result[$node] = $configArray[$node]; } return $result; }
/** * 使用拓扑排序算法进行排序 * * @param $nodes 节点列表 * @param $edges 边列表 * @return array 排序后的节点列表 */ private function topological_sort($nodes, $edges) { $L = array(); $S = array();
$nodes = array_combine($nodes, array_fill(0, count($nodes), array('in'=>array(), 'out'=>array())));
foreach ($edges as $e) { $nodes[$e[0]]['out'][] = $e[1]; $nodes[$e[1]]['in'][] = $e[0]; }
foreach ($nodes as $id=>$n) { if (empty($n['in'])) $S[]=$id; }
while ($id = array_shift($S)) { if (!in_array($id, $L)) { $L[] = $id; foreach($nodes[$id]['out'] as $m) { $nodes[$m]['in'] = array_diff($nodes[$m]['in'], array($id)); if (empty($nodes[$m]['in'])) { $S[] = $m; } } $nodes[$id]['out'] = array(); } }
foreach($nodes as $n) { if (!empty($n['in']) or !empty($n['out'])) { return null; // 不可排序,因为图中存在环 } }
return $L; }
/** * 对配置数组进行排序 * * 该方法是被测试使用的,用于封装 _topSortConfigArray 方法。 * * @param $configArray 配置数组 * @return array 排序后的数组 */ public function _topSortConfigArray($configArray) { return $this->topological_sort($configArray); }// [PATCHED CODE BEGIN] // 如果第一个元素包含“sort_order”键,则调用简单排序 reset($configArray); $element = current($configArray); if (isset($element['sort_order'])) { uasort($configArray, array($this, '_compareSortOrder')); $sortedCollectors = array_keys($configArray); } else { $sortedCollectors = $this->_topSortConfigArray($configArray); } // 将已排序的收集器键存储在变量$sortedCollectors中 // [PATCHED CODE END]
// 如果Mage::app()->useCache('config')返回true,则保存已排序的收集器键 if (Mage::app()->useCache('config')) { Mage::app()->saveCache(serialize($sortedCollectors), $this->_collectorsCacheKey, array( Mage_Core_Model_Config::CACHE_TAG )); }
这段代码实现了按照特定键值进行数组排序。如果第一个元素包含“sort_order”键,则使用uasort函数对整个数组进行排序;否则,将会调用$this->_topSortConfigArray()函数进行排序,并将排序后的收集器键存储在变量$sortedCollectors中。最后,如果Mage::app()->useCache('config')返回true,则保存已排序的收集器键。/** * 使用 before 和 after 进行比较的回调函数 * * @param array $a * @param array $b * @return int */ protected function _compareTotals($a, $b) { $aCode = $a['_code']; $bCode = $b['_code']; if (in_array($aCode, $b['after']) || in_array($bCode, $a['before'])) { $res = -1; } elseif (in_array($bCode, $a['after']) || in_array($aCode, $b['before'])) { $res = 1; } else { $res = 0; } return $res; }
/** * 使用 sort_order 进行比较的回调函数 * * @param array $a * @param array $b * @return int */ protected function _sortTotals($a, $b) { if ($a['sort_order'] == $b['sort_order']) { return 0; } return ($a['sort_order'] < $b['sort_order']) ? -1 : 1; }

编辑: Magento 2 还有另一个建议的更改: https://github.com/magento/magento2/pull/49


1
非常感谢,补丁包含调试代码和可视化效果都非常好! - Anna Völkl
@Alex:我正在使用1.9版本的Magento,我遇到了同样的问题,你能帮我解决这个问题吗?请查看我的问题...http://stackoverflow.com/questions/26118357/conflicting-two-magento-extenstion - David John
嘿,有人在1.9上有过好运吗?我们只需要做同样的事情吗? - WebDevB

3

编辑:这个答案是错误的。请查看评论中的讨论。


正如Vinai所指出的那样,问题在于即使参数不相等,order函数仍然返回0。我修改了该函数,采用键的字符串顺序作为后备方案,代码如下:

protected function _compareTotals($a, $b)
{
    $aCode = $a['_code'];
    $bCode = $b['_code'];
    if (in_array($aCode, $b['after']) || in_array($bCode, $a['before'])) {
        $res = -1;
    } elseif (in_array($bCode, $a['after']) || in_array($aCode, $b['before'])) {
        $res = 1;
    } else {
        $res = strcmp($aCode, $bCode); // was $res = 0 before
    }
    return $res;
}

是的,我已经签署了。我该如何提交补丁? - Alex
抱歉,这个答案也是错误的。请看下面我的答案——问题出在快速排序算法上,但解决方案不正确。 - Vinai
为什么是错的?我的答案避免了将实际上不相等的元素视为相等,因此避免了这些元素的奇怪切换。 - Alex
2
这是错误的,因为它只使用总代码(字符串)作为比较基础,这是一个随机决定因素。我在我的新答案中描述的同样的逻辑问题可能会发生,例如,如果我们有三个总数,a、b和c。假设之前/之后的规则指定a在c之后(a>c)。这给了我们一个无效的排序顺序,对于b来说,因为b>a且b<c,但是根据之前/之后的规则,a比c大。因此,根据输入数据,我们可能再次得到一个无效的总模型排序。a同时比c大和小。 - Vinai
答案的开头是正确的。在两个元素不相等时返回0是罪魁祸首。只是建议的解决方案(代码)是错误的。 - hakre
显示剩余3条评论

2

我决定采用计划B,重载getSortedCollectors... 这很简单并且给了我绝对的控制权,当然,如果我要引入新的模块,我需要检查是否需要在这里添加它们。

<?php
class YourModule_Sales_Model_Total_Quote_Collector extends Mage_Sales_Model_Quote_Address_Total_Collector {

    protected function _getSortedCollectorCodes() {
        return array(
            'nominal',
            'subtotal',
            'msrp',
            'freeshipping',
            'tax_subtotal',
            'weee',
            'shipping',
            'tax_shipping',
            'floorfee',
            'bottlediscount',
            'discount',
            'tax',
            'grand_total',
        );
    }

}

1

我被这个问题困扰了多年!!!

现在我知道为什么过去的一些项目在调整wee和税收组合方面如此困难,可以说是噩梦,我从来没有理解过为什么,昨天我找到了原因,后来我发现了这篇文章,真是可惜...但大多数时候我需要知道答案才能搜索问题..

对于没有恐惧心理的Linux用户,显然的解决方案是下面的代码,基本上我使用古老的Linux命令tsort,它特别按照我们需要的方式进行拓扑排序..

对于我们中的昆虫学家和考古学家,这里有一些指针http://www.gnu.org/software/coreutils/manual/html_node/tsort-invocation.html,I,我正在使用80年代的技术... 嗯呀呀呀

    /**
 * Aggregate before/after information from all items and sort totals based on this data
 *
 * @return array
 */
protected function _getSortedCollectorCodes() {
    if (Mage::app()->useCache('config')) {
        $cachedData = Mage::app()->loadCache($this->_collectorsCacheKey);
        if ($cachedData) {
            return unserialize($cachedData);
        }
    }
    $configArray = $this->_modelsConfig;
    // invoke simple sorting if the first element contains the "sort_order" key
    reset($configArray);
    $element = current($configArray);
    if (isset($element['sort_order'])) {
        uasort($configArray, array($this, '_compareSortOrder'));
        $sortedCollectors = array_keys($configArray);
    } else {
        foreach ($configArray as $code => $data) {
            foreach ($data['before'] as $beforeCode) {
                if (!isset($configArray[$beforeCode])) {
                    continue;
                }
                $configArray[$code]['before'] = array_merge(
                        $configArray[$code]['before'],
                        $configArray[$beforeCode]['before']);
                $configArray[$code]['before'] = array_unique(
                        $configArray[$code]['before']);
                $configArray[$beforeCode]['after'] = array_merge(
                        $configArray[$beforeCode]['after'], array($code),
                        $data['after']);
                $configArray[$beforeCode]['after'] = array_unique(
                        $configArray[$beforeCode]['after']);
            }
            foreach ($data['after'] as $afterCode) {
                if (!isset($configArray[$afterCode])) {
                    continue;
                }
                $configArray[$code]['after'] = array_merge(
                        $configArray[$code]['after'],
                        $configArray[$afterCode]['after']);
                $configArray[$code]['after'] = array_unique(
                        $configArray[$code]['after']);
                $configArray[$afterCode]['before'] = array_merge(
                        $configArray[$afterCode]['before'], array($code),
                        $data['before']);
                $configArray[$afterCode]['before'] = array_unique(
                        $configArray[$afterCode]['before']);
            }
        }
        //uasort($configArray, array($this, '_compareTotals'));
        $res = "";
        foreach ($configArray as $code => $data) {
            foreach ($data['before'] as $beforeCode) {
                if (!isset($configArray[$beforeCode])) {
                    continue;
                }
                $res = $res . "$code $beforeCode\n";
            }
            foreach ($data['after'] as $afterCode) {
                if (!isset($configArray[$afterCode])) {
                    continue;
                }
                $res = $res . "$afterCode $code\n";
            }
        }
        file_put_contents(Mage::getBaseDir('tmp')."/graph.txt", $res);
        $sortedCollectors=explode("\n",shell_exec('tsort '.Mage::getBaseDir('tmp')."/graph.txt"),-1);           
    }
    if (Mage::app()->useCache('config')) {
        Mage::app()
                ->saveCache(serialize($sortedCollectors),
                        $this->_collectorsCacheKey,
                        array(Mage_Core_Model_Config::CACHE_TAG));
    }
    return $sortedCollectors;
}

我为了完整起见发布了完整的函数,当然,至少对我而言,它是完美无缺的...


调用一个shell函数的想法有点疯狂 :-) 当然,在Windows服务器上是行不通的...但还是谢谢! - Alex
@Alex 众所周知,Windows不是Magento支持的操作系统,所以没有问题 :) - user1508488
这里只有一个小问题:未定义并且没有在之前/之后的总数将被省略。 - Alex

0
以上讨论清楚地指出了问题。通常的排序无法在没有定义集合中任意两个元素之间的顺序函数的数据集上工作。如果只定义了一些关系,例如“部分依赖”,则必须使用拓扑排序来遵守声明的“before”和“after”语句。在我的测试中,这些声明在结果集中被打破,因此我添加了额外的模块。令人担忧的是,它不仅影响了附加模块,还可能以不可预测的方式改变整个排序结果。因此,我实现了标准的拓扑排序来解决这个问题:
/**
 * The source data of the nodes and their dependencies, they are not required to be symmetrical node cold list other
 * node in 'after' but not present in its 'before' list:
 * @param $configArray
 *  $configArray = [
 *    <nodeCode>     => ["_code"=> <nodeCode>, "after"=> [<dependsOnNodeCode>, ...], "before"=> [<dependedByCode>, ...] ],
 *     ...
 * ]
 * The procedure updates adjacency list , to have every edge to be listed in the both nodes (in one 'after' and other 'before')
 */
function normalizeDependencies(&$configArray) {
    //For each node in the source data
    foreach ($configArray as $code => $data) {
            //Look up all nodes listed 'before' and update their 'after' for consistency
        foreach ($data['before'] as $beforeCode) {
            if (!isset($configArray[$beforeCode])) {
                continue;
            }
            $configArray[$beforeCode]['after'] = array_unique(array_merge(
                $configArray[$beforeCode]['after'], array($code)
            ));
        }
            //Look up all nodes listed 'after' and update their 'before' for consistency
        foreach ($data['after'] as $afterCode) {
            if (!isset($configArray[$afterCode])) {
                continue;
            }
            $configArray[$afterCode]['before'] = array_unique(array_merge(
                $configArray[$afterCode]['before'], array($code)
            ));
        }
    }
}

/**
 *  http://en.wikipedia.org/wiki/Topological_sorting
 *  Implements Kahn (1962) algorithms
 */
function topoSort(&$array) {
    normalizeDependencies($array);
    $result = array(); // Empty list that will contain the sorted elements
    $front = array(); // Set of all nodeCodes with no incoming edges
    //Push all items with no predecessors in S;
    foreach ($array as $code => $data) {
        if ( empty ($data['after']) ) {
            $front[] = $code;
        }
    }
    // While 'front' is not empty
    while (! empty ($front)) {
        //Deque 'frontier' from 'front'
        $frontierCode = array_shift($front);
        //Push it in 'result'
        $result[$frontierCode]= $array[$frontierCode];
        // Remove all outgoing edges from 'frontier';
        while (! empty ($array[$frontierCode]['before'])) {
            $afterCode = array_shift($array[$frontierCode]['before']);
            // remove corresponding edge e from the graph
            $array[$afterCode]['after'] = array_remove($array[$afterCode]['after'], $frontierCode);
            //* if, no more decencies put node into processing queue:
            // if m has no other incoming edges then
            if ( empty ($array[$afterCode]['after']) ) {
                // insert m into 'front'
                array_push($front, $afterCode);
            }
        }
    }
    if(count($result) != count($array)){
        saveGraph($array, 'mage-dependencies.dot');
        throw new Exception("Acyclic dependencies in data, see graphviz diagram: mage-dependencies.dot for details.");
    }
    return $result;
}
 /**
 * Could not find better way to remove value from array
 *
 * @param $array
 * @param $value
 * @return array
 */
protected function array_remove($array, $value){
    $cp = array();
    foreach($array as $b) {
        if($b != $value){
            $cp[]=$b;
        }
    }
    return $cp;
}

/**
 *  Saves graph in the graphviz format for visualisation:
 *    >dot -Tpng /tmp/dotfile.dot > viz-graph.png
 */
function saveGraph($configArray, $fileName){
    $fp = fopen($fileName,'w');
    fwrite($fp,"digraph TotalOrder\n");
    fwrite($fp,"{\n");
    foreach($configArray as $code=>$data) {
        fwrite($fp,"$code;\n");
        foreach($data['before'] as $beforeCode) {
            fwrite($fp,"$beforeCode -> $code;\n");
        }
        foreach($data['after'] as $afterCode) {
            fwrite($fp,"$code -> $afterCode;\n");
        }
    }
    fwrite($fp,"}\n");
    fclose($fp);
}

问题是,将其(或其他拓扑排序)纳入Magento发布/热修复有多难?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接