运输模块的工作原理
我们仍以 Zone Rates 模块为例来说明一般的运输模块的运行原理。
运输模块的方法
初始化 includes/modules/shipping/zones.php [101]
function zones() {
$this->code = 'zones';
$this->title = MODULE_SHIPPING_ZONES_TEXT_TITLE;
$this->description = MODULE_SHIPPING_ZONES_TEXT_DESCRIPTION;
$this->sort_order = MODULE_SHIPPING_ZONES_SORT_ORDER;
$this->icon = '';
$this->tax_class = MODULE_SHIPPING_ZONES_TAX_CLASS;
$this->enabled = ((MODULE_SHIPPING_ZONES_STATUS == 'True') ? true : false);
运输模块要初始化的基本内容包括:$code、$title、$description、$sort_order、$enabled。 $code:此模块的唯一标识符。
- $title:显示的模块名称
- $description:显示的描述
- $sort_order:序号
- $enabled:是否生效
其它的内容如 icon、tax-class,又如上面的 Zone Rates 扩展参数等,根据不同的模块需要而定。
安装与删除 includes/modules/shipping/zones.php [172-209]
安装与删除涉及了 4 个方法,分别为 install、remove、key、check,它们的功能分别如下:
- install:添加模块的所有参数至数据库
- remove:从数据库删除此模块的所有参数
- key:模块的所有参数键值
- check:判断模块是否已经安装
运费计算 includes/modules/shipping/zones.php [115]
function quote($method = '')
quote 的作用有两个:
- 获取可用的运输方式,如果一个运输方式有多个子运输方式,也一并输出。
- 在选定此运输方式后,获取最后的运费。
quote 方法只有唯一的 method 参数,此参数的作用是在选定此运输方式时,如果此模块拥有 多种不同子运输方式,可以通过 method 传递此子运输方式。
这里我们拿美国邮政 USPS(United States Postal Service)提供的服务来举例:
USPS 提供有:
- Global Express Guaranteed
- Global Express Guaranteed Non-Document Rectangular
- Global Express Guaranteed Non-Document Non-Rectangular
- Express Mail International (EMS)
- Express Mail International (EMS) Flat Rate Envelope
- Priority Mail International
- Priority Mail International Flat Rate Envelope
- Priority Mail International Flat Rate Box
- First-Class Mail International
不同的服务运费也不一样,所以如果我们使用一个 USPS 运输模块来包含其提供的所有服务时,则必须提供子运输方式给用户选择,最后通过子运输方式来计算最终的运费。
返回值格式 includes/modules/shipping/zones.php [155]
$this->quotes = array('id' => $this->code,
'module' => MODULE_SHIPPING_ZONES_TEXT_TITLE,
'methods' => array(array('id' => $this->code,
'title' => $shipping_method,
'cost' => $shipping_cost)));
运输模块返回的是一个数组,数组结构如上,id 是此模块的唯一识别 ID,module 是模块的 名称,methods 数组是一个二维数组,它指定了可用的运输方式,如果有多个子运输方式,应将 所有子运输方式放在 methods 数组里一起返回。
运输模块的调用流程
选择运输方式 checkout_shipping.php[64-95]
// load all enabled shipping modules
require(DIR_WS_CLASSES . 'shipping.php');
$shipping_modules = new shipping;
if ( defined('MODULE_ORDER_TOTAL_SHIPPING_FREE_SHIPPING') &&
(MODULE_ORDER_TOTAL_SHIPPING_FREE_SHIPPING == 'true') ) {
$pass = false;
switch (MODULE_ORDER_TOTAL_SHIPPING_DESTINATION) {
case 'national':
if ($order->delivery['country_id'] == STORE_COUNTRY) {
$pass = true;
}
break;
case 'international':
if ($order->delivery['country_id'] != STORE_COUNTRY) {
$pass = true;
}
break;
case 'both':
$pass = true;
break;
}
$free_shipping = false;
if ( ($pass == true) && ($order->info['total'] >=
MODULE_ORDER_TOTAL_SHIPPING_FREE_SHIPPING_OVER) ) {
$free_shipping = true;
include(DIR_WS_LANGUAGES . $language .
'/modules/order_total/ot_shipping.php');
}
} else {
$free_shipping = false;
}
首先加载 shipping 类,并新建一个 shipping 类的对象$shipping_modules。shipping 类的初始化如下:
运输方式类 includes/classes/shipping.php[16-42]
// class constructor
function shipping($module = '') {
global $language, $PHP_SELF;
if (defined('MODULE_SHIPPING_INSTALLED') &&
tep_not_null(MODULE_SHIPPING_INSTALLED)) {
$this->modules = explode(';', MODULE_SHIPPING_INSTALLED);
$include_modules = array();
if ( (tep_not_null($module)) && (in_array(substr($module['id'], 0, strpos($module['id'], '_')) . '.' . substr($PHP_SELF, (strrpos($PHP_SELF, '.')+1)), $this->modules)) ) {
$include_modules[] = array('class' => substr($module['id'], 0, strpos($module['id'], '_')), 'file' => substr($module['id'], 0, strpos($module['id'], '_')) . '.' . substr($PHP_SELF, (strrpos($PHP_SELF, '.')+1)));
} else {
reset($this->modules);
while (list(, $value) = each($this->modules)) {
$class = substr($value, 0, strrpos($value, '.'));
$include_modules[] = array('class' => $class, 'file' => $value);
}
}
for ($i=0, $n=sizeof($include_modules); $i<$n; $i++) {
include(DIR_WS_LANGUAGES . $language . '/modules/shipping/' .
$include_modules[$i]['file']);
include(DIR_WS_MODULES . 'shipping/' . $include_modules[$i]['file']);
$GLOBALS[$include_modules[$i]['class']] = new
$include_modules[$i]['class'];
}
}
}
在这里请注意第 20 行的 MODULE_SHIPPING_INSTALLED,这个常量是 OSC 系统维护的一个“已安装运输方式”的表。 它的修改与更新由后台的“admin/modules.php” 文件管理,具体代码位于 186-195 行。
MODULE_SHIPPING_INSTALLED 的值将 所有已安装的运输模块文件名用“;”分隔开来, 所以在 21 行首先将其分解为数组。
接下来的代码,将所有运输模块文件名,组 织成由 class 和 file 为成员的数组 $include_modules。
然后再遍历$include_modules,加载每个运输 模块的语言文件与主文件,并一一新建各自的类 对象。对象的名称与类名一致(其实为文件名去 除后缀名的那部分)。并且这些类对象都为全局变 量。
接着继续来看 checkout_shipping.php 文件, 在 68-95 行的代码讨论了免运费的问题。只有在 激活了 Free Shipping 时,才会进行此计算,Free Shipping 的设置在后台的 Modules->Order Total 的 Shipping 选项里,以图所示:
其中的参数如下:
- Allow Free Shipping:指定是否开启免运费功能。
- Free Shipping For Orders Over:选项设置总金额大于多少才可以免运费。
- Provide free Shipping For Orders Made:是否限定免运费只用于国内或国际,或者不限定。
其中 68 行是对“Allow Free Shipping”的检验,71-85 行的代码即是对“Provide free Shipping For Orders Made”的检验,而 88 行的代码是对“Free Shipping For Orders Over”订单额是否达到指定 金额选项的检验。
为了适应一般的条理性,我们按照先显示可用运输方式,然后选择指定运输方式的顺序来看运输 模块的执行过程。
运输模块的列表获取 checkout_shipping.php[140]
// get all available shipping quotes
$quotes = $shipping_modules->quote();
此处调用了 shipping 类的 quote 方法。
运输模块类 includes/classes/shipping.php[44]
function quote($method = '', $module = '') {
global $total_weight, $shipping_weight, $shipping_quoted, $shipping_num_boxes;
$quotes_array = array();
if (is_array($this->modules)) {
$shipping_quoted = '';
$shipping_num_boxes = 1;
$shipping_weight = $total_weight;
if (SHIPPING_BOX_WEIGHT >= $shipping_weight*SHIPPING_BOX_PADDING/100) {
$shipping_weight = $shipping_weight+SHIPPING_BOX_WEIGHT;
} else {
$shipping_weight = $shipping_weight + ($shipping_weight*SHIPPING_BOX_PADDING/100);
}
if ($shipping_weight > SHIPPING_MAX_WEIGHT) { // Split into many boxes
$shipping_num_boxes = ceil($shipping_weight/SHIPPING_MAX_WEIGHT);
$shipping_weight = $shipping_weight/$shipping_num_boxes;
}
$include_quotes = array();
reset($this->modules);
while (list(, $value) = each($this->modules)) {
$class = substr($value, 0, strrpos($value, '.'));
if (tep_not_null($module)) {
if ( ($module == $class) && ($GLOBALS[$class]->enabled) ) {
$include_quotes[] = $class;
}
} elseif ($GLOBALS[$class]->enabled) {
$include_quotes[] = $class;
}
}
$size = sizeof($include_quotes);
for ($i=0; $i<$size; $i++) {
$quotes = $GLOBALS[$include_quotes[$i]]->quote($method);
if (is_array($quotes)) $quotes_array[] = $quotes;
}
}
return $quotes_array;
}
我在前面已经讲过,每个运输模块 quote 方法具有两个作用,它不仅可以列表出所有可用的 运输方式,而且可以指定对应的运输方式及子运输方式来计算运费。
同样 shipping 类的 quote 也 具有同样的作用,或者换句话说,shipping 类的 quote 方法即是通过所有运输模块的 quote 方法来 达到这两个功能的(事实上的确如此,上面的 quote 方法第 67-83 行的代码已经说明了这点)。
在 shipping 类的初始化时,我们已经将所有已安装的运输模块文件名分解成数组,存放在成 员变量$modules 里。
所以在 49 行,首先判断$this->modules 是否为数组来确定有可用的运输模块。
54-63 的代码实现了包装的重量计算,以及包装的分拆。相关的三个参数在后台的 Configuration->Shipping/Pachaging 里。
详细的参数如下:
- Enter the Maximum Package Weight you will ship:单个包裹的最大容量,对应于常量 SHIPPING_MAX_WEIGHT
- PackageTareweight:单个包装箱的重量,对应于常量SHIPPING_BOX_WEIGHT
- Largerpackages-percentageincrease:超出标准尺寸的包装重量计算倍数(包装计算倍数), 对应于常量 SHIPPING_BOX_PADDING
- Largerpackages-percentageincrease的单位为“%”,如填入10,则为10%
提示: OSC 的包装重量及分拆原理是
- 如果总重量包装计算倍数<=单个包装箱的重量,则按单个包装箱的重量计算包装重量;否则,包装重量为总重量包装计算倍数的值。
- 如果总重量超过单个包裹的最大容量,则按最大容量分拆成若干包裹。否则,使用单个包裹。
第 67-77 行的代码找出所有可安装的运输模块,并确认其是否生效。
第 70-76 行显示了 quote 方法两个不同作用的区别所在,当指定了$module 参数时,只将指定 的$module 包含进最终执行 quote 方法的数组$include_quotes 里。
剩余的代码是运行所有可用的运输模块的 quote 方法,并将返回结果保存至数组,并将此数 组作为返回值。
运输模块列表的显示
运输模块列表的显示从行 264 开始,到 361 行结束。其中显示了如何以单选框的方式输出运输模块选项。代码比较简单,请读者自行分析。
值得提及的一点是,单选框的名称为“shipping”,值为运输模块的 code +“”+ 子运输方式 的 code,如无子运输方式,则为运输模块的 code +“”+ 运输模块的 code。如:Zone Rates 选 项的单选框的值为“zones_zones”。
运输模块的选取 checkout_shipping.php[104]
if (!tep_session_is_registered('shipping')) tep_session_register('shipping');
if ( (tep_count_shipping_modules() > 0) || ($free_shipping == true) ) {
if ( (isset($HTTP_POST_VARS['shipping'])) &&
(strpos($HTTP_POST_VARS['shipping'], '_')) ) {
$shipping = $HTTP_POST_VARS['shipping'];
list($module, $method) = explode('_', $shipping);
if ( is_object($$module) || ($shipping == 'free_free') ) {
if ($shipping == 'free_free') {
$quote[0]['methods'][0]['title'] = FREE_SHIPPING_TITLE;
$quote[0]['methods'][0]['cost'] = '0';
} else {
$quote = $shipping_modules->quote($method, $module);
}
if (isset($quote['error'])) {
tep_session_unregister('shipping');
} else {
if ( (isset($quote[0]['methods'][0]['title'])) &&
(isset($quote[0]['methods'][0]['cost'])) ) {
$shipping = array('id' => $shipping,
'title' => (($free_shipping == true) ?
$quote[0]['methods'][0]['title'] : $quote[0]['module'] . ' (' .
$quote[0]['methods'][0]['title'] . ')'),
'cost' => $quote[0]['methods'][0]['cost']);
tep_redirect(tep_href_link(FILENAME_CHECKOUT_PAYMENT, '', 'SSL'));
}
}
} else {
tep_session_unregister('shipping');
}
}
} else {
$shipping = false;
tep_redirect(tep_href_link(FILENAME_CHECKOUT_PAYMENT, '', 'SSL'));
}
osCommerce 使用 session 的方法传递 shipping 的选择。所以在最开始就使用tep_session_register 注册 shipping。而在选择无效时,则使用 tep_session_unregister 注销 shipping。
第 106 行的 tep_count_shipping_modules 函数,返回已安装并已生效的运输模块的数量。 针对指定的运输模块的处理从第 107 行开始,因为我们在上面提及到 shipping 单选框的值的 格式,所以 110 行,将此值分解为$module 和$method。
在判断了运输模块$module 的正确性后,在第 116 行,通过 shipping 类($shipping_modules 为 其对象)的 quote 方法,间接执行选取的运输模块的 quote 方法。
如果 quote 方法返回的值是正确的,则在第 122 行,构造正确的$shipping 值用于 session 的传 递。 第 126 行显示选择了正确运输模块后,通过 tep_redirect 自动转入付款方式的选择页面。