分类目录归档:Zen-cart

Zen-cart 插件 – 蛋疼的Fast and Easy AJAX Checkout for Zen Cart

之前曾经写过一篇文件时关于这个插件的,链接为http://blog.ifeeline.com/260.html,随着鬼佬的这个版本的更新,这篇文章中的Bug已经得到修改,不过恶心的鬼佬在版本升级中又引入了恶心的Bug,其中的一个Bug简直让人非常恶心。随我慢慢道来…

注:这里使用的quick_checkout版本是2.25.2

Zen-cart插件Bug
如果希望修改地址时弹出,或者像把这里的Change Address Pop-up改为true,不幸的是,这个弹出框需要一个第三方的JS插件支持,而这个插件是收费的。如果把它改为了true,而没有安装这个JS插件,将产生脚本错误。

插件对tpl_modules_checkout_new_address.php做了修改,不过很脑残,这个鬼佬的程序非常不严谨:

<?php
if (ACCOUNT_STATE == 'true') {
 	if ($flag_show_pulldown_states == true) {
		?>
		<label class="inputLabel" for="stateZone" id="zoneLabel"><?php echo ENTRY_STATE. (zen_not_null(ENTRY_STATE_TEXT) ? '&nbsp;<span class="alert">' . ENTRY_STATE_TEXT . '</span>' : ''); ?></label>
		<?php
   		echo zen_draw_pull_down_menu('zone_id', zen_prepare_country_zones_pull_down($selected_country), $zone_id, 'id="stateZone"'); 
    }
	?>

	<?php 
	if ($flag_show_pulldown_states == true) { ?>
		<br class="clearBoth" id="stBreak" />
	<?php 
	} ?>
	<label class="inputLabel" for="state" id="stateLabel"><?php echo $state_field_label; ?></label>
	<?php
    echo zen_draw_input_field('state', '', zen_set_field_length(TABLE_ADDRESS_BOOK, 'entry_state', '40') . ' id="state"');
	if (zen_not_null(ENTRY_STATE_TEXT)) echo '&nbsp;<span class="alert" id="stText">' . ENTRY_STATE_TEXT . '</span>';
    if ($flag_show_pulldown_states == false) {
      	echo zen_draw_hidden_field('zone_id', $zone_name, ' ');
    }
	?>
	<br class="clearBoth" />
<?php
}
?>

这段代码本没有问题,图示:

后台开启State,并且State总是下拉时:
Zen-cart打开省份下拉

当一个国家没有分配省时,要求自己填写,注意看后面的那个星号。
Zen-cart国家省份下来

这是以上代码:

if (zen_not_null(ENTRY_STATE_TEXT)) echo '&nbsp;<span class="alert" id="stText">' . ENTRY_STATE_TEXT . '</span>';

产生的作用。不过鬼佬的代码妈的的把这行去掉了,真是自作聪明啊,导致了切换省份时触发的JS函数错误:

  function hideStateField(theForm) {
    theForm.state.disabled = true;
    theForm.state.className = 'hiddenField';
    theForm.state.setAttribute('className', 'hiddenField');
    document.getElementById("stateLabel").className = 'hiddenField';
    document.getElementById("stateLabel").setAttribute('className', 'hiddenField');
    document.getElementById("stText").className = 'hiddenField';
    document.getElementById("stText").setAttribute('className', 'hiddenField');
    document.getElementById("stBreak").className = 'hiddenField';
    document.getElementById("stBreak").setAttribute('className', 'hiddenField');
  }

document.getElementById(“stText”)这行代码,由于stText不存在,导致JS脚本错误。

接下来的这个大Bug简直让人恶心:
点击访问http://www.vfeelit.com/index.php?main_page=checkout_shipping_address,输入新地址,那么应该应用新地址,新地址添加由模块checkout_new_address.php(被覆盖),这个逻辑没有问题,$_SESSION[‘sendto’] = $new_id;说明被设置了最新添加的地址,然后它会被定位到quick_check,打开header_php.php看:

        $check_address = $db->Execute("SELECT customers_default_shipping_address_id FROM " . TABLE_CUSTOMERS . " WHERE customers_id = " . (int)$_SESSION['customer_id'] . " AND customers_default_shipping_address_id > 0 LIMIT 1;"); 
        // set the default shipping address id if it exists (we need to do this in case the customer logged in from outside of FEAC)
        if ($check_address->RecordCount() > 0) $_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id'];

看那行注释,说是当客户不使用FEAC登录进来时,需要设置。注意,这里的customers_default_shipping_address_id是FEAC插件的扩展。它用来记录默认运输地址。这里的意思就是当设置了这个ID时,$_SESSION[‘sendto’]永远都使用这个。

你会看到改变地址,也会定向到quick_checkout,也会设置$_SESSION[‘sendto’],那么上面的代码岂不是覆盖掉这个设置? 是的,TMD你会发现你添加了新地址,新地址成功添加,但是它不给你应用(它应用了所谓的默认运输地址,这个鬼佬真他妈脑残)。

实际上,你看到,当客户不是使用FEAC登录时,$_SESSION[‘sendto’]没有被设置,而$_SESSION[‘customer_default_shipping_address_id’]也没有被设置,所以这里的这个代码是顾此失彼。可以判断如果没有设置,才运行这个代码:

        if ($check_address->RecordCount() > 0){
			if((int)$_SESSION['sendto'] > 0){ //已经被赋值,通过FEAX登录
				$_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id']; //实际通过FEC登录时这个已经被设置
			}else{
				$_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id']; //非FEAX登录
			}
		}

在注册新用户时,customers_default_shipping_address_id并没设置(或设置为0),注意,$_SESSION[‘customer_default_shipping_address_id’]这个customer没有s。当从地址簿中选择地址簿时,选中的这个地址会被设置为customers_default_shipping_address_id:

//checkout_new_address.php(模块)
				// update default shipping address ID
				if (!$_SESSION['COWOA']) {
					$db->Execute("UPDATE " . TABLE_CUSTOMERS . " 
						  SET customers_default_shipping_address_id = " . (int)$_POST['address'] . " 
						  WHERE customers_id = " . (int)$_SESSION['customer_id'] . " LIMIT 1;");
				}

实际上,当添加新的运输地址时,也应该设置为默认的运输地址。

在qc_login的header_php.php中:

$_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_customer->fields['customers_default_shipping_address_id'];

登录时$_SESSION[‘sendto’]就被设置为默认的运输地址。在quick_checkout包含的文件quick_checkout_fec.php中有对这些地址的修正:

  if (!$_SESSION['sendto']) {
    $_SESSION['sendto'] = $_SESSION['customer_default_address_id'];
  } else {
  // verify the selected shipping address
    $check_address_query = "SELECT count(address_book_id) AS total
                            FROM   " . TABLE_ADDRESS_BOOK . "
                            WHERE  customers_id = :customersID
                            AND    address_book_id = :addressBookID
                            LIMIT 1";

    $check_address_query = $db->bindVars($check_address_query, ':customersID', (int)$_SESSION['customer_id'], 'integer');
    $check_address_query = $db->bindVars($check_address_query, ':addressBookID', (int)$_SESSION['sendto'], 'integer');
    $check_address = $db->Execute($check_address_query);

    if ($check_address->fields['total'] != '1') {
      $_SESSION['sendto'] = $_SESSION['customer_default_address_id'];
      $_SESSION['shipping'] = '';
    }
  }

意思是先应用默认运输地址,然后才应用默认地址。

这里描述的这个Bug是非常恶心的。可能是,这个插件前后经过不同的人维护,顾此失彼导致严重Bug,以前75美金的插件,现在卖150美金,安装还收费,其它依赖的也是收费,想钱想疯了。

还有一个删除地址簿的Bug,也是顾此失彼,不过问题不大就不罗列了。附加文件保留:quick_checkout_BUG_Fix

永久链接:http://blog.ifeeline.com/962.html

Zen-cart 改变运输地址 和 改变账单地址 流程 和 小Bug

改变运输地址 和 改变账单地址

main_page=checkout_shipping_address   			
main_page=checkout_payment_address

这种情况都会加载地址薄表单 和 地址簿列表:
Zen-cart修改地址流程
这个流程展示文件是如何包含的。

tpl_modules_checkout_address_book.php模板中,对应默认地址的选中:

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $_SESSION['sendto']), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

单选框是否被选中,完全依赖当前的$_SESSION[‘sendto’],从上面的示例图可以看到tpl_modules_checkout_address_book.php模板会被两个控制器(checkout_shipping_address 和 checkout_payment_address)包含,地址是否选中不应该只依赖当前的$_SESSION[‘sendto’],我发现在Zen-cart151的默认模板中都是如此设置的,这明显是一个Bug,不过问题不大。

首先在tpl_modules_checkout_address_book.php开头添加

if($addressType == 'billto'){
	$checked = $_SESSION['billto'];
}else{
	$checked = $_SESSION['sendto'];
}

然后把

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $_SESSION['sendto']), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

替换成:

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $checked), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

这样的小Bug在Zen-cart目前所有版本都是如此,那意思是说只要点修改账单地址,点一下继续,那的账单地址就可以被改变了,而你的本意可能是希望不做任何修改。

永久链接:http://blog.ifeeline.com/959.html

Zen-cart Geo-zone扩展

Zen-cart扩展Geo-Zone管理

可以参考Zen-cart中的地区与税率

这里主要扩展了geo_zones这个表进行扩展,后台文件对应admin/geo_zones.php

//加入如下代码防止错误
///////////////////////
$cnt = $db->metaColumns(TABLE_GEO_ZONES);
if(!isset($cnt['COST'])){
	$db->Execute("ALTER TABLE ".TABLE_GEO_ZONES." ADD cost VARCHAR( 256 ) NULL DEFAULT ''");
}
if(!isset($cnt['GEO_ZONE_TYPE_ID'])){
	$db->Execute("ALTER TABLE ".TABLE_GEO_ZONES." ADD geo_zone_type_id int(11) NOT NULL DEFAULT '0'");
}
///////////////////////

cost字段主要用来保存这个区的费率,geo_zone_type_id用来对geo_zone进行分类,标识这个区是哪种类型,比如DHL或Fedex类型的分区:
Zen-cart新表geo_zones_type

其它改动就是调整查询插入更新时的相关的SQL,不过在新添加或编辑Geo_Zone时,需要下拉出Geo_Zone_Type:

zen_draw_pull_down_menu('geo_zone_type_id', zen_get_zone_types("请选择Geo Zone Type")

//zen_get_zone_types()函数
function zen_get_zone_types($default = '') {
    global $db;

	$zone_class_array = array();
    if ($default) {
      	$zone_class_array[] = array('id' => '',
                                 'text' => $default);
    }

    $zone_class = $db->Execute("select geo_zone_type_id, geo_zone_type_name
                                from " . TABLE_GEO_ZONES_TYPE . "
                                order by geo_zone_type_name");

    while (!$zone_class->EOF) {
      	$zone_class_array[] = array('id' => $zone_class->fields['geo_zone_type_id'],
                                  'text' => $zone_class->fields['geo_zone_type_name']);
      	$zone_class->MoveNext();
    }
	return $zone_class_array;
}

这个就可以在后台管理Geo_Zone对应的费率(cost)了。每个Geo_Zone里面包含了一个或多个国家,它们对应一个费率,再由Geo_Zone_Type知道是哪种Geo_Zone,然后可以安装规则计算运费。这个扩展意义就在这里了。

永久链接:http://blog.ifeeline.com/946.html

Zen-cart 扩展国家表管理

Zen-cart中国家表的管理比较粗糙,特别是在要对应国家下拉列表进行调整时,比如把常用的国家调整到最前面,那么就需要扩展一下国家表字段了。

Zen-cart国家表管理扩展

涉及到国家表管理的是admin/countries.php文件,需要对这个文件做一些改动:

///////////////////////
$cnt = $db->metaColumns(TABLE_COUNTRIES);
if(!isset($cnt[strtoupper('countries_name_cn')])){	
	$db->Execute("ALTER TABLE ".TABLE_COUNTRIES." ADD countries_name_cn VARCHAR( 64 ) NULL DEFAULT ''");
}
if(!isset($cnt[strtoupper('order_by')])){	
	$db->Execute("ALTER TABLE ".TABLE_COUNTRIES." ADD order_by int(11) NOT NULL DEFAULT '0'");
}
///////////////////////

然后就是对插入编辑时的SQL进行修改,当然接下来还有修改表单(这里忽略)。

Zen-cart国家表字段编辑

这样可以添加中文名称和排序码,排序码添加了之后还要修改两个获取国家下拉列表的函数(前台后台分别对应一个):

#includes/functions/functions_lookups.php
  function zen_get_countries($countries_id = '', $with_iso_codes = false) {
    global $db;

    $countries_array = array();
    if (zen_not_null($countries_id)) {
    } else {
	  ///////////////////////
	  $cnt = $db->metaColumns(TABLE_COUNTRIES);
	  if(!isset($cnt[strtoupper('order_by')])){	
		$db->Execute("ALTER TABLE ".TABLE_COUNTRIES." ADD order_by int(11) NOT NULL DEFAULT '0'");
	  }
	  ///////////////////////
      $countries = "select countries_id, countries_name
                    from " . TABLE_COUNTRIES . "
                    order by order_by, countries_name";

      $countries_values = $db->Execute($countries);

      while (!$countries_values->EOF) {
        $countries_array[] = array('countries_id' => $countries_values->fields['countries_id'],
                                   'countries_name' => $countries_values->fields['countries_name']);

        $countries_values->MoveNext();
      }
    }

    return $countries_array;
  }

#admin/includes/functions/general.php
  function zen_get_countries($default = '') {
    global $db;
    $countries_array = array();
    if ($default) {
      $countries_array[] = array('id' => '',
                                 'text' => $default);
    }
	
	///////////////////////
	$cnt = $db->metaColumns(TABLE_COUNTRIES);
	if(!isset($cnt[strtoupper('countries_name_cn')])){	
		$db->Execute("ALTER TABLE ".TABLE_COUNTRIES." ADD countries_name_cn VARCHAR( 64 ) NULL DEFAULT ''");
	}
	if(!isset($cnt[strtoupper('order_by')])){	
		$db->Execute("ALTER TABLE ".TABLE_COUNTRIES." ADD order_by int(11) NOT NULL DEFAULT '0'");
	}
	///////////////////////

    $countries = $db->Execute("select countries_id, countries_name,countries_name_cn
                               from " . TABLE_COUNTRIES . "
                               order by order_by, countries_name");

    while (!$countries->EOF) {
      $countries_array[] = array('id' => $countries->fields['countries_id'],
                                 'text' => $countries->fields['countries_name']." - ".$countries->fields['countries_name_cn']);
      $countries->MoveNext();
    }

    return $countries_array;
  }

Zen-cart国家调整排序

Zen-cart管家管理后台界面

函数中加入了判断对应字段是否存在的逻辑,防止出错。

永久链接:http://blog.ifeeline.com/940.html

Zen-cart消息提示messageStack类详解

类文件:includes/classes/message_stack.php

初始化:

  $autoLoadConfig[0][] = array('autoType'=>'class',
                                'loadFile'=>'message_stack.php');

  $autoLoadConfig[130][] = array('autoType'=>'classInstantiate',
                                 'className'=>'messageStack',
                                 'objectName'=>'messageStack');

最终得到一个消息堆栈类变量$messageStack。首先看此类的构造函数:

  function messageStack() {

    $this->messages = array();

    if (isset($_SESSION['messageToStack']) && $_SESSION['messageToStack']) {
      $messageToStack = $_SESSION['messageToStack'];
      for ($i=0, $n=sizeof($messageToStack); $i<$n; $i++) {
        $this->add($messageToStack[$i]['class'], $messageToStack[$i]['text'], $messageToStack[$i]['type']);
      }
      $_SESSION['messageToStack']= '';
    }
  }

用messages数组来保存消息栈,然后接收来自$_SESSION[‘messageToStack’]的消息(如果有),调用add压入数组。实际上,当调用add_session方法时,信息将记录到$_SESSION[‘messageToStack’]数组中,表示这个信息是来自上一次请求产生的。

这个类最重要的就是add方法:

  function add($class, $message, $type = 'error') {
    global $template, $current_page_base;
    $message = trim($message);
    $duplicate = false;
    if (strlen($message) > 0) {
      if ($type == 'error') {
        $theAlert = array('params' => 'class="messageStackError larger"', 'class' => $class, 'text' => zen_image($template->get_template_dir(ICON_IMAGE_ERROR, DIR_WS_TEMPLATE, $current_page_base,'images/icons'). '/' . ICON_IMAGE_ERROR, ICON_ERROR_ALT) . '  ' . $message);
      } elseif ($type == 'warning') {
      } elseif ($type == 'success') {
      } elseif ($type == 'caution') {
      } else {
        $theAlert = array('params' => 'class="messageStackError larger"', 'class' => $class, 'text' => $message);
      }
      //确保不重复
      for ($i=0, $n=sizeof($this->messages); $i<$n; $i++) {
        if ($theAlert['text'] == $this->messages[$i]['text'] && $theAlert['class'] == $this->messages[$i]['class']) $duplicate = true;
      }
      if (!$duplicate) $this->messages[] = $theAlert;
    }
  }

这个方法的第一个参数是信息的分组标识符,比如在购物车产生的信息,可用shopping_cart这个标识符来记录所有的信息,方法的第三参数是信息类型,一共有error warning success caution,success表示成功操作时使用,error表示产生错误时使用,warning和caution都表示警告,warning的程度比较重,用来发出警告信息时使用。

add_session方法的调用跟add方法类似,不过它会把信息同时压入$_SESSION[‘messageToStack’]中(构造函数利用它获取来此上次请求的消息):

  function add_session($class, $message, $type = 'error') {

    if (!$_SESSION['messageToStack']) {
      $messageToStack = array();
    } else {
      $messageToStack = $_SESSION['messageToStack'];
    }

    $messageToStack[] = array('class' => $class, 'text' => $message, 'type' => $type);
    $_SESSION['messageToStack'] = $messageToStack;
    $this->add($class, $message, $type);
  }

要注意正确使用add和add_session,如果信息不需要在下一个请求中共享,只需要调用add即可。一般,如果POST提交了,最后可能发起一次新的请求,如果POST过程有信息产生,这个时候就要用add_session。

output($class)方法用来输出信息,$class表示分组标识符。size($class)表示$class这个分组的信息大小。

永久链接: http://blog.ifeeline.com/867.html

Zen-cart中的导航类navigationHistory详解

类文件位置:includes/classes/navigation_history.php。这个类的初始化:

  $autoLoadConfig[80][] = array('autoType'=>'classInstantiate',
                                'className'=>'navigationHistory',
                                'objectName'=>'navigation',
                                'checkInstantiated'=>true,
                                'classSession'=>true);


  $autoLoadConfig[120][] = array('autoType'=>'objectMethod',
                                'objectName'=>'navigation',
                                'methodName' => 'add_current_page');

以上代码会最终会执行:

if(!$_SESSION['navigaton']) $_SESSION['navigation'] = new navigaionHistory();
$_SESSION['navigation']->add_current_page();

注意,这个对象引用是保存到SESSION中的,每次都会调用add_current_page()方法,所以这个方法中必须要有去重复的功能。

参考navigaionHistory代码:

class navigationHistory extends base {
  var $path, $snapshot;

  function navigationHistory() {
    $this->reset();
  }

  function reset() {
    $this->path = array();
    $this->snapshot = array();
  }

  function add_current_page() {
    global $request_type, $cPath;
    $get_vars = "";

    if (is_array($_GET)) {
      reset($_GET);
      while (list($key, $value) = each($_GET)) {
        if ($key != 'main_page') {
          $get_vars[$key] = $value;
        }
      }
    }

    $set = 'true';
    for ($i=0, $n=sizeof($this->path); $i<$n; $i++) {
      if ( ($this->path[$i]['page'] == $_GET['main_page']) ) {
        if (isset($cPath)) { //有$cPath
          //原来没有设置cPath,说明和当前页不匹配 应该是index
          if (!isset($this->path[$i]['get']['cPath'])) {
            continue;
          } else { //当前页有cPath
            //如果cPath等,截掉当前页之后的内容(不包括当前页)
            if ($this->path[$i]['get']['cPath'] == $cPath) {
              array_splice($this->path, ($i+1));
              $set = 'false';
              break;
	    //当前页不等,拆分cPath比对,确保是真的不等,如果拆分等的话,那么之前应该匹配,应该也是不等,只要不等就截断(包括当前页)
            } else {
              $old_cPath = explode('_', $this->path[$i]['get']['cPath']);
              $new_cPath = explode('_', $cPath);

              $exit_loop = false;
              for ($j=0, $n2=sizeof($old_cPath); $j<$n2; $j++) {
                if ($old_cPath[$j] != $new_cPath[$j]) {
                  array_splice($this->path, ($i));
                  $set = 'true';
                  $exit_loop = true;
                  break;
                }
              }
              if ($exit_loop == true) break;
            }
          }
        } else { //没有$cPath   截断(包括当前页),说明需要重新设置当前页
          array_splice($this->path, ($i));
          $set = 'true';
          break;
        }
      }
    }

    if ($set == 'true') {
      if ($_GET['main_page']) {
        $page = $_GET['main_page'];
      } else {
        $page = 'index';
      }
      $this->path[] = array('page' => $page,
                            'mode' => $request_type,
                            'get' => $get_vars,
                            'post' => array() /*$_POST*/);
    }
  }

  function remove_current_page() {

    $last_entry_position = sizeof($this->path) - 1;
    if ($this->path[$last_entry_position]['page'] == $_GET['main_page']) {
      unset($this->path[$last_entry_position]);
    }
  }

  //设置快照,常见出现地方是当前页需要登录才能访问,那么先把当前页进行快照,然后跳到登录页,成功后回到这个快照
  function set_snapshot($page = '') { //$page可以是数组或字符串
    global $request_type;
    $get_vars = array();
    if (is_array($page)) {
      $this->snapshot = array('page' => $page['page'],
                              'mode' => $page['mode'],
                              'get' => $page['get'],
                              'post' => $page['post']);
    } else {
      reset($_GET);
      while (list($key, $value) = each($_GET)) {
        if ($key != 'main_page') {
          $get_vars[$key] = $value;
        }
      }
      if ($_GET['main_page']) {
        $page = $_GET['main_page'];
      } else {
        $page = 'index';
      }
      $this->snapshot = array('page' => $page,
                              'mode' => $request_type,
                              'get' => $get_vars,
                              'post' => array()/*$_POST*/);
    }
  }

  function clear_snapshot() {
    $this->snapshot = array();
  }

  function set_path_as_snapshot($history = 0) {
    $pos = (sizeof($this->path)-1-$history);
    $this->snapshot = array('page' => $this->path[$pos]['page'],
                            'mode' => $this->path[$pos]['mode'],
                            'get' => $this->path[$pos]['get'],
                            'post' => $this->path[$pos]['post']);
  }
}

add_current_page会把当前页添加到path数组中(去重复),remove_current_page去除添加的当前页,set_snapshot对当前页进行快照记录,或者对指定的页面进行快照(传递数组)。Zen-cart中,对add_current_page的调用,只在初始化时进行调用一次,实际它已经能很好的工作了。但是set_snapshot就在很多地方需要用到:

 
//对当前页面进行快照  $_SESSION['navigation']->set_snapshot();
  if (!in_array($_GET['main_page'], array(FILENAME_LOGIN, FILENAME_CREATE_ACCOUNT))) {
    if (!isset($_GET['set_session_login'])) {
      $_GET['set_session_login'] = 'true';
      $_SESSION['navigation']->set_snapshot();
    }
    zen_redirect(zen_href_link(FILENAME_LOGIN, '', 'SSL'));
  }

//对指定页进行快照 $_SESSION['navigation']->set_snapshot(array('mode' => 'SSL', 'page' => FILENAME_CHECKOUT_SHIPPING));
    if (zen_get_customer_validate_session($_SESSION['customer_id']) == false) {
      $_SESSION['navigation']->set_snapshot(array('mode' => 'SSL', 'page' => FILENAME_CHECKOUT_SHIPPING));
      zen_redirect(zen_href_link(FILENAME_LOGIN, '', 'SSL'));
    }

实际上,如果需要跳回当前页就对当前页进行快照,如果不是跳回当前页,就需要对指定页进行快照。

Zen-cart MySQL判断表的某字段是否存在

MySQL中可以通过如下语句把表的结构返回

SHOW COLUMNS FROM 表名;

mysql> show columns from countries;
+----------------------+-------------+------+-----+---------+----------------+
| Field                | Type        | Null | Key | Default | Extra          |
+----------------------+-------------+------+-----+---------+----------------+
| countries_id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| countries_name       | varchar(64) | NO   | MUL |         |                |
| countries_iso_code_2 | char(2)     | NO   | MUL |         |                |
| countries_iso_code_3 | char(3)     | NO   | MUL |         |                |
| address_format_id    | int(11)     | NO   | MUL | 0       |                |
| countries_name_cn    | varchar(64) | YES  |     |         |                |
+----------------------+-------------+------+-----+---------+----------------+

可以看到,这个格式是固定的,如果要判断某字段是否存在,只要循环判断就可以实现。

Zen-cart中对这个操作进行了封装,MySQL的工厂类($db)有一个函数:

  function metaColumns($zp_table) {
    $sql = "SHOW COLUMNS from :tableName:";
    $sql = $this->bindVars($sql, ':tableName:', $zp_table, 'noquotestring');
    $res = $this->execute($sql);    
    while (!$res->EOF) 
    {
      $obj [strtoupper($res->fields['Field'])] = new queryFactoryMeta($res->fields); 
      $res->MoveNext();
    }    
    return $obj;
  }

它把字段名作为下标,引用返回的一行数据。queryFactoryMeta类根据传入的对象的Type的值往该对象添加两个属性(type 和 max_length),用来表示字段的数据类型和长度。

class queryFactoryMeta {

  function queryFactoryMeta($zp_field) {
    $type = $zp_field['Type'];
    $rgx = preg_match('/^[a-z]*/', $type, $matches);
    $this->type = $matches[0];
    $this->max_length = preg_replace('/[a-z\(\)]/', '', $type);
  }
}

所以,如果在Zen-cart中要判断某个表的某字段是否存在,只要使用isset()就可以了,参考如下代码:

$cnt = $db->metaColumns(TABLE_GEO_ZONES);
if(!isset($cnt['GEO_ZONE_TYPE_ID'])){
	$db->Execute("ALTER TABLE ".TABLE_GEO_ZONES." ADD geo_zone_type_id INT( 11 ) NOT NULL DEFAULT 0");
}
if(!isset($cnt['COST'])){
	$db->Execute("ALTER TABLE ".TABLE_GEO_ZONES." ADD cost VARCHAR( 64 ) NULL DEFAULT ''");
}

这段代码判断某字段,如GEO_ZONE_TYPE_ID是否存在,如果不存在,就添加这个字段,这个为自动扩展系统提供了基础代码。

永久链接:http://blog.ifeeline.com/846.html

Zen-cart运费手动估算功能

运费手动估算
参考:http://shipping.vfeelit.com/index.php?main_page=shipping_estimator

Zen-cart中的运费估算出现在购物车页面,它只能根据购物车中的重量估算运费,而无法手动输入重量进行估算。手动输入重量可以快速比较各种运输方式的价格。

这里的这个功能从Zen-cart默认的运费估算移植而来,精简了一些代码。核心代码:

	if(isset($_POST['action']) && ($_POST['action'] == 'submit')){
		if(isset($_POST['shipping_weight'])){
			$_SESSION['shipping_weight'] = (int)$_POST['shipping_weight'];
		}
		if (isset($_POST['zone_country_id'])){
			$_SESSION['zone_country_id'] = $_POST['zone_country_id'];
		}
		zen_redirect(zen_href_link('shipping_estimator'));
		exit;
	}

	if (isset($_SESSION['zone_country_id'])){
		$country_info = zen_get_countries($_SESSION['zone_country_id'],true);
		$order->delivery = array('country' => array('id' => $_SESSION['zone_country_id'], 'title' => $country_info['countries_name'], 'iso_code_2' => $country_info['countries_iso_code_2'], 'iso_code_3' =>  $country_info['countries_iso_code_3']), 'country_id' => $_SESSION['zone_country_id']);	
	} else {
		$country_info = zen_get_countries(STORE_COUNTRY,true);
		$order->delivery = array('country' => array('id' => STORE_COUNTRY, 'title' => $country_info['countries_name'], 'iso_code_2' => $country_info['countries_iso_code_2'], 'iso_code_3' =>  $country_info['countries_iso_code_3']),'country_id' => STORE_COUNTRY);
	}

	$total_weight = 10;
	if(isset($_SESSION['shipping_weight'])){
		$total_weight = (int)$_SESSION['shipping_weight'];
	}
	
	///////
  	require(DIR_WS_CLASSES . 'shipping.php');
  	$shipping_modules = new shipping;
  	$quotes = $shipping_modules->quote();
	$qs = array();
	foreach($quotes as $q){
		if($q['id'] == 'freeshipper'){
			continue;
		}else{
			$qs[] = $q;
		}
	}
	$quotes = $qs;	
  	///////
	
  	// set selections for displaying
  	$selected_country = $order->delivery['country']['id'];
  	$free_shipping = false;
	
	$show_in = 'shipping_estimator';

接下来就是一个表单输出。

注意问题:从代码可以看出,我把freeshipper这种运输方式给过滤掉了。默认,如果购物车中没有产品或产品重量为零,这个运输方式就可能被激活。而我不想去修改freeshipper的逻辑,所以这里把它过滤掉。另外,每个运费模块的类的构造函数中都有判断模块是否启用的代码逻辑:

      	if (zen_get_shipping_enabled($this->code) ) {
        	$this->enabled = ((MODULE_SHIPPING_HK_POST_AIR_MAIL_STATUS == 'True') ? true : false);
      	}

当前运输方式是否启用,由zen_get_shipping_enabled()函数控制,这个函数根据购物车的内容控制运费模块是否启用,很明显,对于手动输入重量来计算运费的情况,是一大限制,可以修改这个函数的逻辑,不过我更加倾向修改我的自定义运费模块来解决这个问题:

//....
     	global $order, $db, $total_weight;
      	if (zen_get_shipping_enabled($this->code) || ($total_weight > 0)) {
        	$this->enabled = ((MODULE_SHIPPING_HK_POST_AIR_MAIL_STATUS == 'True') ? true : false);
      	}

从外部获取$total_weight,在运费模块初始化时,重量确定,然后判断这个重量,只要大于零模块就总是启用。

原创文章,转载务必保留出处。
永久链接: http://blog.ifeeline.com/841.html

Zen-cart自定义运费模块-China Post

中国邮政小包有挂号和平邮,这里说的是挂号国家邮政小包。这个运输方式把国家分成10个区,每个区对应不同的费率(每公斤多少钱)。
中国邮政航空小包

由于针对不同国家可能对应了不同费率,所有实现这个插件并没有那么简单,以上图片展示的只是一些基本参数,实际的国家分区费率表需要导入到数据库中,而这些Zen-cart并没有实现。

这些工作量是有点大的,比如为了能使用国家对应的中文名称包国家导入到分区表,我们首先需要扩展countries表,把每个国家的中文名对应上去。这个步骤可以写一段程序批量导入,这里跳过了。

接下来是建立分区,这里重复利用Zen-cart默认的geo_zone,关注geo_zone可以参考:http://blog.ifeeline.com/798.html。

要导入的表:
国家分区费率表导入

不过为了让每个geo_zone对应费率,需要对geo_zone进行扩展(添加cost字段)

ALTER TABLE `geo_zones` ADD `cost` VARCHAR( 64 ) NULL DEFAULT '0'

这样分区对应的国家的对应关系就建立起来了,每个分区用cost字段记录了这个分区的费率。

那么China Post运费模块的quote方法:

    // class methods
    function quote($method = '') {
      	global $order,$shipping_weight,$shipping_num_boxes,$db,$currencies;
	  
	  	$total_weight = $shipping_weight * $shipping_num_boxes;

	  	if($total_weight > (int)MODULE_SHIPPING_CHINA_POST_MAX_WEIGHT){
			return false;	
	  	}
	  	//package weight
	  	$total_weight += (int)MODULE_SHIPPING_CHINA_POST_PACKAGE_WEIGHT;
	  
		$cost = $db->Execute("select gz.geo_zone_name, gz.cost, ztgz.zone_id from geo_zones gz, zones_to_geo_zones ztgz where gz.geo_zone_id = ztgz.geo_zone_id and ztgz.zone_country_id = ".(int)$order->delivery['country']['id']." and ztgz.zone_id = 0 and gz.geo_zone_name like 'ChinaPost_%'");
 
		if($cost->RecordCount() > 0){
		  	$first = round((float)MODULE_SHIPPING_CHINA_POST_COST,2);
		  	$continue = round($cost->fields['cost'],2);
	
		  	$rate = (float)$currencies->currencies['CNY']['value'];
		  	if($rate <= 0){
				$rate = 1;
		  	}
		  
		  	$disc = (float)MODULE_SHIPPING_CHINA_POST_RATE;
		  	if($disc <= 0){
				$disc = 1;
		  	}
		  
		  	$ttl = round($disc*($first+$continue*ceil($total_weight/10)/100)/$rate,2);
		  	$this->quotes = array('id' => $this->code,
								'module' => MODULE_SHIPPING_CHINA_POST_TEXT_TITLE,
								'methods' => array(array('id' => $this->code,
														 'title' => MODULE_SHIPPING_CHINA_POST_TEXT_WAY,
														 'cost' => $ttl)));
		}else{
			return false;	
		}

      	if ($this->tax_class > 0) {
        	$this->quotes['tax'] = zen_get_tax_rate($this->tax_class, $order->delivery['country']['id'], $order->delivery['zone_id']);
      	}

      	if (zen_not_null($this->icon)) $this->quotes['icon'] = zen_image($this->icon, $this->title);

      	return $this->quotes;
    }

主要是根据国家代码把费率给找出来,如果还有其它运输方式也采用类似的方法,那么某个国家可能对应多个geo_zone,这个时候需要过滤出符合当前运输方式的geo_zone,在导入分区表时,每个分区都采用ChinaPost_前缀,所以这里添加了like ‘ChinaPost_%’条件就可以过滤出当前的运输方式的分区,当然务必要避免同一个国家装入同一种运输方式的不同分区中(国家分区导入时需要考虑这个逻辑),否则将得到多个费率,这里总是使用返回排在第一的那条记录。

原创文章,转载务必保留出处。
永久链接:http://blog.ifeeline.com/835.html

Zen-cart自定义运费模块 – HK POST

香港邮政航空小包的计费方式是:挂号费+每公斤的费用。比如13+100/KG, 如果是100克,那么收费是13+0.1*100 = 23元。

香港小包运费设置

Zen-cart中并没有符合这个计算逻辑的运费模块。参照现存的模块,自定义一个非常简单,需要先理解Zen-cart运费模块的工作模式,可参考:
http://blog.ifeeline.com/827.html。

    // class methods
    function quote($method = '') {
      	global $order,$shipping_weight,$shipping_num_boxes,$db,$currencies;
	  
	  	$total_weight = $shipping_weight * $shipping_num_boxes;

	  	if($total_weight > (int)MODULE_SHIPPING_HK_POST_MAX_WEIGHT){
			return false;	
	  	}
	  	//package weight
	  	$total_weight += (int)MODULE_SHIPPING_HK_POST_PACKAGE_WEIGHT;
	  
      	////////////////////
      	$cost = explode(',',MODULE_SHIPPING_HK_POST_COST);
      	$first = 13; $continue = 108;
      
      	if(count($cost) >= 2){
      		$first = (int)$cost[0];
 		$continue = round($cost[1],2);
      	}else{
      		$tmp = round($cost[0],2);
      		if($tmp > 70){
      	  		$continue = $tmp;
      		}
      	}
      	////////////////////

	$rate = (float)$currencies->currencies['CNY']['value'];
      	if($rate <= 0){
	    	$rate = 1;
	}
	  
      	$ttl = round(($first+$continue*ceil($total_weight/10)/100)/$rate,2);
      	$this->quotes = array('id' => $this->code,
                            'module' => MODULE_SHIPPING_HK_POST_TEXT_TITLE,
                            'methods' => array(array('id' => $this->code,
                                                     'title' => MODULE_SHIPPING_HK_POST_TEXT_WAY,
                                                     'cost' => $ttl)));
      	if ($this->tax_class > 0) {
        	$this->quotes['tax'] = zen_get_tax_rate($this->tax_class, $order->delivery['country']['id'], $order->delivery['zone_id']);
      	}

      	if (zen_not_null($this->icon)) $this->quotes['icon'] = zen_image($this->icon, $this->title);

      	return $this->quotes;
    }

需要实现quote方法,这个方法按照如上返回对应内容。运费成本计算逻辑就一句代码:

      	$ttl = round(($first+$continue*ceil($total_weight/10)/100)/$rate,2);

注意,这里除以了人民币的汇率得到基准货币,那是因为插件后台设置的值为人民币为单位。插件其它代码参考Zen-cart默认的运费模块即可完成。

Zen-cart运费计算_香港小包

这里显示的运费就是根据上面的逻辑进行计算的。

原创文章,转载务必保留出处。
永久链接:http://blog.ifeeline.com/830.html