月度归档:2013年05月

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

PHP控制文件另存为操作

记录一段代码:

$zzna = $db->Execute("select geo_zone_type_name from ".TABLE_GEO_ZONES_TYPE." where geo_zone_type_id = ".$ztid);
$output_name = trim($zzna->fields['geo_zone_type_name'])?trim($zzna->fields['geo_zone_type_name']):'zone_csv_file';
				
header("Content-type: text/csv");
header("Content-disposition: attachment;filename=" . $output_name . ".csv");	
				
$fp = fopen("php://output",'w');
foreach($data as $d){
    fputcsv($fp,$d);
}
fclose($fp);
exit;

文件另存为操作实际跟PHP没有干系,文件下载另存为实际由HTTP协议控制,通过HTTP的两个头指令Content-type 和 Content-disposition可以触发浏览器把接收到的内容保存为文件。

永久链接: http://blog.ifeeline.com/844.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

Zen-cart运费模块逻辑分析

Zen-cart中的运费模块用一个shipping类来管理,比如有几种运输方式,这个类依次调用这几种运输方式的特定方法获取结果,然后展示出来,如果客户选中某种运输方式,Zen-cart把运输方式代码和运费值记录下来。

实际上,逻辑非常简单:

##includes/classes/shipping.php
class shipping extends base {
  var $modules;
  // class constructor
  function shipping($module = '') {}
  function calculate_boxes_weight_and_tare() {}
  function quote($method = '', $module = '', $calc_boxes_weight_tare = true) {}
  function cheapest() {}
}

在构造函数中会检查MODULE_SHIPPING_INSTALLED,这个常量保存了已经安装的运输方式。然后把相关的类代码加载进来(包括对应的语言文件),然后每个运输方式都生成一个超全局变量:

$GLOBALS[$include_modules[$i]['class']] = new $include_modules[$i]['class'];

然后就可以调用quote方法获取全部运行方式的列表,这个方法内实际调用具体的运输的quote方法获取该运输方式的的相关详细(说明、运费值等)。实际上这个过程还是比较简单的。不过这里需要说明下calculate_boxes_weight_and_tare()函数,它实际是在shipping类的quote方法中运行的,由quote方法的$calc_boxes_weight_tare参数控制,默认是true。这个逻辑过程如下:
1 货物在打包的时候会有一个最大重量,比如1公斤,假设现在是5公斤,那么就可能需要分成几个包了,对于这种情况,打包的包装比如使用了1公斤,那么平均下来就是(5+1)/1 = 6, 一共需要弄成6个包(因为最大是1公斤)。
2 如果货物没有超过打包的最大重量,但是需要加上打包的包装重量,比如0.5公斤+0.1公斤(包装重量),那么这个包的最总重量是0.6公斤。

calculate_boxes_weight_and_tare()函数中,用$shipping_weight表示单个包的重量,$shipping_num_boxes表示有多少个包(默认是1)。所以,在没有多个包的情况下,$shipping_weight当前这个包的重量(添加了包装重量),如果有多个包,$shipping_weight*$shipping_num_boxes才等于总重量。

这个逻辑过程的配置对应后台:(Configuration -> Shipping/Packaging)
Zen-cart运输方式设置
这里设置了单个包的最大重量,当超过最大重量时需要添加的打包的重量(百分比+固定值),当没有超过时需要添加大打包重量(百分比+固定值)。

cheapest()函数返回最便宜的运输方式(默认会选中这个)。

在每个具体的运输方式的quote方法中,需要返回如下格式:

$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)));

正如你所看,methods是一个二维数组,说明它可以有多个结果,所以对于同一个模块,可以有多个结果。如果去模板文件中查看运费模块的选择,就可以看到:

<?php
// --------->  列表
for ($i=0, $n=sizeof($quotes); $i<$n; $i++) {
	//没有嵌套 
      	if(sizeof($quotes[$i]['methods'])==1){
          	// simple shipping method
          	$thisquoteid = $quotes[$i]['id'].'_'.$quotes[$i]['methods'][0]['id'];
			?>
	<tr class="<?php echo $extra; ?>">
			<?php
          	if($quotes[$i]['error']){
			?>
    	<td colspan="2"><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['error']; ?>)</td>
	</tr>
			<?php
          	}else{
            	if($selected_shipping['id'] == $thisquoteid){
				?>
         <td class="bold"><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['methods'][0]['title']; ?>)</td>
         <td class="bold"><?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax'])); ?> -- <?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax']),true,'CNY'); ?></td>
	</tr>
		<?php
            	}else{
		?>
    	<td><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['methods'][0]['title']; ?>)</td>
      	<td><?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax'])); ?> -- <?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax']),true,'CNY'); ?></td>
  	</tr>
		<?php
            	}
			
          	}
	//嵌套了多种
     	} else {
          	// shipping method with sub methods (multipickup)
          	for ($j=0, $n2=sizeof($quotes[$i]['methods']); $j<$n2; $j++) {
            	$thisquoteid = $quotes[$i]['id'].'_'.$quotes[$i]['methods'][$j]['id'];
				?>
  	<tr class="<?php echo $extra; ?>">
				<?php
            	if($quotes[$i]['error']){
				?>
   		<td colspan="2"><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['error']; ?>)</td>
   	</tr>
				<?php
            	}else{
              		if($selected_shipping['id'] == $thisquoteid){
					?>
  		<td class="bold"><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['methods'][$j]['title']; ?>)</td>
      	<td class="bold"><?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][$j]['cost'], $quotes[$i]['tax'])); ?>  -- <?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax']),true,'CNY'); ?></td>
  	</tr>
					<?php
              		}else{
					?>
        <td><?php echo $quotes[$i]['module']; ?>&nbsp;(<?php echo $quotes[$i]['methods'][$j]['title']; ?>)</td>
        <td><?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][$j]['cost'], $quotes[$i]['tax'])); ?>  -- <?php echo $currencies->format(zen_add_tax($quotes[$i]['methods'][0]['cost'], $quotes[$i]['tax']),true,'CNY'); ?></td>
  	</tr>
					<?php
              		}
            	}
			
          	}///////
		  
       	}	
		
 }
?>

可以看到,对嵌套了多种计算结果的运费模块,也可以正确处理,这个好处在于,如果费率表一样,但是每个货运公司给出的折扣不一样,那么就可以选出价格最低的一个。

这中设计模式在Zen-cart多出应用,比如Order Total, Payment模块。

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

Zen-cart的配置系统

Zen-cart配置系统

可以在后台管理配置,配置选项是分组的。数据表结构:
Zen-cart的配置表

这个结构还是比较简单的。在configuration表中,最重要的字段是configuration_key和configuration_value,它们会分别被如下定义成常量:

define(configuration_key,configuration_value);

这样这个KEY就全局可用了(全局常量)。

实现这个过程的是如下这段代码:

##includes/init_includes/init_db_config_read.php
$use_cache = (isset($_GET['nocache']) ? false : true ) ;
$configuration = $db->Execute('select configuration_key as cfgkey, configuration_value as cfgvalue
                                 from ' . TABLE_CONFIGURATION, '', $use_cache, 150);
while (!$configuration->EOF) {
  /**
 * dynamic define based on info read from DB
 */
  define(strtoupper($configuration->fields['cfgkey']), $configuration->fields['cfgvalue']);
  $configuration->MoveNext();
}
$configuration = $db->Execute('select configuration_key as cfgkey, configuration_value as cfgvalue
                          from ' . TABLE_PRODUCT_TYPE_LAYOUT);

while (!$configuration->EOF) {
  /**
 * dynamic define based on info read from DB
 * @ignore
 */
  define(strtoupper($configuration->fields['cfgkey']), $configuration->fields['cfgvalue']);
  $configuration->movenext();
}
if (file_exists(DIR_WS_CLASSES . 'db/' . DB_TYPE . '/define_queries.php')) {
  /**
 * Load the database dependant query defines
 */
  include(DIR_WS_CLASSES . 'db/' . DB_TYPE . '/define_queries.php');
}

注意:从数据中读取出来的配置项可能会很多,如果每个请求都读数据库,压力上升可能会很快,所以这里使用了查询缓存,具体可以参考:http://blog.ifeeline.com/806.html. 但是,如果修改了配置,希望配置马上生效(可能来自缓存),那么可以手动指定nocache参数,那么就可以重新从数据库读取了。

从上面代码可以看出,除了系统的配置,还有从TABLE_PRODUCT_TYPE_LAYOUT中读取的配置,这个跟系统配置差不多,它用来实现针对不同的产品类型应用不同的布局配置。

具体内容可以参考:http://blog.ifeeline.com/810.html

从这个配置文件读入来看,它也仅仅是从数据库读入然后define而已,并没有发现有继承关系(虽然产品类型实现了继承)。

最后这段是定义一些最常用的SQL查询,避免多次编写。

DEFINE('SQL_CC_ENABLED', "select configuration_key from " . TABLE_CONFIGURATION . " where configuration_key RLIKE 'CC_ENABLED' and configuration_value= '1'");
DEFINE('SQL_SHOW_PRODUCT_INFO_CATEGORY', "select configuration_key, configuration_value from " . TABLE_CONFIGURATION . " where configuration_key RLIKE 'SHOW_PRODUCT_INFO_CATEGORY' and configuration_value > 0 order by configuration_value");
DEFINE('SQL_SHOW_PRODUCT_INFO_MAIN',"select configuration_key, configuration_value from " . TABLE_CONFIGURATION . " where configuration_key RLIKE 'SHOW_PRODUCT_INFO_MAIN' and configuration_value > 0 order by configuration_value");
DEFINE('SQL_SHOW_PRODUCT_INFO_MISSING',"select configuration_key, configuration_value from " . TABLE_CONFIGURATION  . " where configuration_key RLIKE 'SHOW_PRODUCT_INFO_MISSING' and configuration_value > 0 order by configuration_value");
DEFINE('SQL_SHOW_PRODUCT_INFO_LISTING_BELOW',"select configuration_key, configuration_value from " . TABLE_CONFIGURATION . " where configuration_key RLIKE 'SHOW_PRODUCT_INFO_LISTING_BELOW' and configuration_value > 0 order by configuration_value");
DEFINE('SQL_BANNER_CHECK_QUERY', "select count(*) as count from " . TABLE_BANNERS_HISTORY . "                where banners_id = '%s' and date_format(banners_history_date, '%%Y%%m%%d') = date_format(now(), '%%Y%%m%%d')");
DEFINE('SQL_BANNER_CHECK_UPDATE', "update " . TABLE_BANNERS_HISTORY . " set banners_shown = banners_shown +1 where banners_id = '%s' and date_format(banners_history_date, '%%Y%%m%%d') = date_format(now(), '%%Y%%m%%d')");
DEFINE('SQL_BANNER_UPDATE_CLICK_COUNT', "update " . TABLE_BANNERS_HISTORY . " set banners_clicked = banners_clicked + 1 where banners_id = '%s' and date_format(banners_history_date, '%%Y%%m%%d') = date_format(now(), '%%Y%%m%%d')");
DEFINE('SQL_ALSO_PURCHASED', "select p.products_id, p.products_image
                     from " . TABLE_ORDERS_PRODUCTS . " opa, " . TABLE_ORDERS_PRODUCTS . " opb, "
                            . TABLE_ORDERS . " o, " . TABLE_PRODUCTS . " p
                     where opa.products_id = '%s'
                     and opa.orders_id = opb.orders_id
                     and opb.products_id != '%s'
                     and opb.products_id = p.products_id
                     and opb.orders_id = o.orders_id
                     and p.products_status = 1
                     group by p.products_id
                     order by o.date_purchased desc
                     limit " . MAX_DISPLAY_ALSO_PURCHASED);
DEFINE('SQL_SHOW_SHOPPING_CART_EMPTY',"select configuration_key, configuration_value from " . TABLE_CONFIGURATION . " where configuration_key RLIKE 'SHOW_SHOPPING_CART_EMPTY' and configuration_value > 0 order by configuration_value");

比如如果以后要显示同时购买的产品的,执行SQL_ALSO_PURCHASED即可(替换变量占位符)。

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

Zen-cart产品类型

Zen-cart产品布局

product_types表中的字段type_master_type,引用自身的主键,说明类型有继承关系。查看默认表数据:
产品类型层次
注意看,这里的产品类型的嵌套关系。不过这里要实现的功能不明(要覆盖继承过来的选项?)。另外,可以看到,表product_types_to_category对应了目录和产品类型的关联,比如某些目录是某种产品类等:
限制目录产品类型
可以把一种或多种产品类型限制到当前目录或当前目录和子目录,对应的记录记录到product_types_to_category中,如果父目录删除了某种类型限制,不管子目录的同类型是不是由父目录引入的,子目录的类型都会被删除。产品类型限制到目录,很明显,只能是指定的类型能放入该目录中。

另外,在product表中有一个字段products_type标志了这个产品的类型(采用哪些布局参数),这个字段默认是1,所以就是一般的产品的。这个字段决定了如何找到对应的产品类型的handler,比如如果找到产品的类型对应的handler是product,那么就路由到main_page=product_info,如果是product_music,就会路由到main_page=product_music_info(这个由zen_href_link()函数完成)。

不同类型的产品操作:
Zen-cart产品布局设置

虽然Zen-cart中引入了多种产品类型,但是它的控制能力非常有限,通常都被忽略。

产品布局配置参数读入可以参考:http://blog.ifeeline.com/820.html

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

Zen-cart中的数据库查询结果缓存

Zen-cart中提供了对数据库查询结果进行缓存功能。数据库查询结果缓存原理非常简单,对查询的SQL语句取散列值,然后把查询返回的结果关联这个散列值保存起来,当有同样的查询时就从保存的结果中直接返回(前提是没有过期)。

首先看缓存类的框架:

class cache extends base {
  //判断缓存是否存在,$zf_cachetime用来指定缓存时间,对应过期的缓存当然不能判断为存在
  function sql_cache_exists($zf_query, $zf_cachetime) {
    global $db;
    $zp_cache_name = $this->cache_generate_cache_name($zf_query);
    switch (SQL_CACHE_METHOD) {
      case 'file':
      //................
      break;
      case 'database':
      //................
      break;
      case 'memory':
      return false;
      break;
      case 'none':
      default:
      return false;
      break;
    }
  }
  //判断缓存是否过期
  function sql_cache_is_expired($zf_query, $zf_cachetime) {
  }
  //马上过期
  function sql_cache_expire_now($zf_query) {
  }
  //保存缓存
  function sql_cache_store($zf_query, $zf_result_array) {
  }
  //读取缓存
  function sql_cache_read($zf_query) {
  }
  //刷新缓存
  function sql_cache_flush_cache() {
  }
  //生成缓存名
  function cache_generate_cache_name($zf_query) {
  }
}

从源代码可以看出,Zen-cart只支持把缓存保存到file和database中,保存到database中是比较少用的,在高并发的情况下,更加倾向把结果保存到memory中,不过Zen-cart没有提供实现(可做二次开发)。

另外,保存的方式使用常量SQL_CACHE_METHOD指定,这个值需要在配置文件中指定:

#includes/configure.php
define('SQL_CACHE_METHOD', 'file'); 
define('DIR_FS_SQL_CACHE', 'D:/www/web/z151.com/public_html/cache');

如果SQL_CACHE_METHOD为none,那么根本不会进行缓存。
注:SQL_CACHE_METHOD在安装Zen-cart时就会提示把缓存的的方法,是file还是database,默认是none。

缓存对象的生成是在环境初始化时进行的:

$autoLoadConfig[30][] = array('autoType'=>'classInstantiate',
                                'className'=>'cache',
                                'objectName'=>'zc_cache');

这个对象的名字叫zc_cache,它是环境初始化中首先构建的对象之一(在它之前是数据库初始化)。

那么这个对象何时会用到?实际上就是在触发SELECT查询时这个对象就会被使用。

##includes/clssses/db/mysql/query_factory.php

class queryFactory extends base {
  function Execute($zf_sql, $zf_limit = false, $zf_cache = false, $zf_cachetime=0) {
    //....
    global $zc_cache;
  }
}

Execute方法是Zen-cart中对数据库操作封装最核心的方法,zf_sql是要执行的SQL(不仅限于查询),zf_limit限制返回的结果数量,zf_cache表示是否缓存查询结果(只对SELECT查询起作用),zf_cachetime指定缓存时间。

目前Zen-cart默认代码应用查询缓存的有如下地方:

##includes/classes/category_tree.php
//...
$categories = $db->Execute($categories_query, '', true, 150);
//...

另一个地方是读取配置文件:

##includes/init_includes/init_db_config_read.php
$use_cache = (isset($_GET['nocache']) ? false : true ) ;
$configuration = $db->Execute('select configuration_key as cfgkey, configuration_value as cfgvalue
                                 from ' . TABLE_CONFIGURATION, '', $use_cache, 150);

这两个地方读取的数据都是经常不改变的,所以缓存起来有必要。特别是配置表,如果每次都查询数据库,并发量一大,就会耗费大量的数据库资源,而这些消耗是完全没有必要的。

另外,虽然Zen-cart中的cache类是用来缓存数据库查询结果的,但是并不意味着你只能用它来缓存数据库查询结果,你完成可以把某个变量缓存起来,比如某个变量每次请求时都要重新构建,而已内容都是一样的,这种情况就非常合适缓存,只是你需要使用这个变量之前首先要判断缓存是否存在,这个操作控制起来非常简单。

举例:

$cs = $db->Execute("select * from countries");
while(!$cs->EOF){
  echo $cs->fields['country_name']."<br />";
  $cs->MoveNext();
}

使用压力测试工具对这对代码进行测试,获取结果。

然后对带数据缓存的版本进行测试:

$cs = $db->Execute("select * from countries",0,true,2000);
while(!$cs->EOF){
  echo $cs->fields['country_name']."<br />";
  $cs->MoveNext();
}

对比结果就能得出结论。

在高并发环境中,对于频繁访问的数据,如果能对数据进行缓存,那么数据库的压力下减是非常明显的。如果并发量很小,几乎感觉不到缓存有什么作用。

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

光与色,色彩入门

光和色不可分割,光是色产生的原因,色是光被感知的结果。”色是光之子,而光是色之母”(约翰内斯.伊顿)。

光是一种电磁波辐射能,一种客观存在的运动着的物质现象。光波的波长决定色彩色现特征。波长的单一、混合程度决定色彩的纯度,色的鲜浊度。振幅决定色的明暗度。视觉感知的色彩本质来说,可分两个基本的大类,既有彩色系与无彩色系。无彩色系指黑、白以及黑白混合后的多种深浅灰色;除去黑白灰以外的所有颜色属于有彩色系。

有彩色系的所有色都具有共同的属性和特征,即色彩的三属性:色相、明度、纯度,而无彩色系的所有色都属于中性色,只具明度而不具色相和纯度属性。

1、色相
色相是颜色的名称,由光波波长决定。色相表示方法–色相环。
色彩色相图
2、明度
明度是色彩具有的明暗程度。无彩色系的黑、白、灰及有彩色系的任何色都具有明度特性。光波振幅的高低决定色彩明度,物体受光量大,反射率高,明度高;物体受光量小、反射率低,明度低。

3、纯度
波长的单一、混合程度决定色彩的纯度,色的鲜浊度。光波单一,色纯;关波复杂,色不纯。光谱中各单色光的色是人感知的极限纯度的纯色。纯色加光或减光,加入白、黑或其他色彩,纯度就会减低,色味减弱。

HSB颜色模式是符合人思维的一种颜色模式。
HBS色彩
色立体图:上下表示明亮度,圆环表色相,圆半径表示同色相某明度下的纯度变化。

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