You are here

1使用Batch API批量修改各种会员价格

admin 的头像
Submitted by admin on 星期五, 2015-09-18 07:26

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们在为不同角色设置不同价格时,曾经用到过一个uc_batch_price模块,它能够根据定义好的折扣,一次性的批量修改所有商品的会员价格。具体配置可以参看第三集,这里我们主要讲解一下代码,首先是info文件:

name = Ubercart Batch Price

description = Ubercart Batch Price.

dependencies[] = uc_price_per_role

package = Ubercart - extra

core = 7.x

这里面的内容我们都比较熟悉了,我在编写程序的时候,如果这个模块具有通用性,我尽量都采用英文的形式,这样更便于其它用户接受。这里我们依赖于uc_price_per_role模块。

uc_batch_price.module文件中,我们实现了hook_menu,这里定义了两个菜单项:

/**

 * 实现钩子hook_menu().

 */

function uc_batch_price_menu() {

  $items['admin/store/settings/price_per_role/default'] = array(

    'title' => 'Default setting',

    'description' => 'Configure price per role settings.',

    'access arguments' => array('administer store'),

    'page callback' => 'drupal_get_form',

    'page arguments' => array('uc_batch_price_default_form'),

    'type' => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

 

  $items['admin/store/settings/price_per_role/batch'] = array(

    'title'            => 'Batch process price',

    'page callback'    => 'drupal_get_form',

    'page arguments'    =>  array('uc_batch_price_batch_form'),

    'access arguments' => array('administer store'),

    'weight'           => 10,

    'type'             => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

  return $items;

}

admin/store/settings/price_per_role/default,这是是定义配置表单的,对应的表单定义位于uc_batch_price.admin.inc'admin/store/settings/price_per_role/batch,用来定义批处理所在的表单页面的,对应的表单也位于uc_batch_price.admin.inc

我们先来看一下,不同角色的默认折扣配置页面的实现代码:

function uc_batch_price_default_form(){

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  $roles = user_roles();

  //$rid = 1;

  //debug($roles);

  foreach($enabled_roles as $rid => $enabled){

//$rid++;

    $form['role'][$rid] = array(

      '#type' => 'fieldset',

      '#collapsible' => TRUE,

      '#collapsed' => FALSE,

      '#title' => $roles[$rid],

    );

    $form['role'][$rid]['multiplicator_'.$rid] = array(

      '#type' => 'textfield',

      '#title' => t('乘数'),

      '#description' => t('大于0小于等于1的小数'),

      '#default_value' => isset($uc_price_per_role_default[$rid]) ? $uc_price_per_role_default[$rid] : 0.8,

      '#size' => 16,

    );

  }

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_default_form_validate($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  foreach($enabled_roles as $rid => $enabled){

    $multiplicator = $form_state['values']['multiplicator_'.$rid];

    if (!empty($multiplicator) && !is_numeric($multiplicator)) {

      form_set_error('multiplicator_'.$rid, t('乘数必须是一个数字.'));

    }

    elseif (!empty($multiplicator)) {

      if(($multiplicator > 1) || ($multiplicator < 0)){

         form_set_error('multiplicator_'.$rid, t('乘数必须是一个大于0小于等于1的数字.'));

  }

    }

  }

}

 

function uc_batch_price_default_form_submit($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = array();

  foreach($enabled_roles as $rid => $enabled){

    $uc_price_per_role_default[$rid] = $form_state['values']['multiplicator_'.$rid];

  }

  variable_set('uc_price_per_role_default', $uc_price_per_role_default);

}

 

    在uc_batch_price_default_form里面,我们为每个与积分相关的会员角色,定义了一个表单元素,用来设置折扣值,比如8折,就是输入0.8就可以了。在表单的验证函数里面,我们对输入的折扣进行了验证,确保它是一个从01的小数。在提交函数里面,我们把每个角色的折扣值,都保存到了变量$uc_price_per_role_default里面,并通过variable_set,将它保存到数据库中。这里面的逻辑没有什么复杂的。

我们来看批处理的代码部分:

function uc_batch_price_batch_form(){

  $product_types = uc_product_types();

  $types = node_type_get_types();

  $options = array();

  //debug($product_types);

  //debug($types);

  foreach ($product_types as $key => $value) {

    $options[$value] = $types[$value]->name;

  }


  $form['types'] = array(

    '#title' => t('Content types to be processed'),

    '#type' =>'checkboxes',

    '#description' =>t('All nodes of these selected types will be processed'),

    '#options' => $options,

  );

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_batch_form_submit($form,&$form_state){

  $types = array_filter($form_state['values']['types']);

  //drupal_set_message('1234567');

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

  batch_set($batch);

}

 

function uc_batch_price_price_process($types, &$context){

  //drupal_set_message('123456');

  //debug($types);

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  //drupal_set_message('123456');

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

  $result = $query->execute();

  foreach ($result as $record) {

    //drupal_set_message('123456');

    $product = node_load($record->nid);

$list_price = $product->list_price;

    foreach($enabled_roles as $rid => $enabled){

      if(!empty($uc_price_per_role_default[$rid])){

      //if(empty($product->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

    $product->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

  }

}

    node_save($product);

    $context['sandbox']['progress']++;

    $context['sandbox']['last_nid'] = $product->nid;

  }

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

}

 

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

这里面的代码,包括四部分,表单定义、表单的提交处理函数、批处理函数、批处理的完成函数。我们的批处理程序,通常都是由这四部分组成的。

在表单定义函数uc_batch_price_batch_form里面,我们使用uc_product_types获取了所有的产品类型,把它作为选项,赋值给复选框。注意,这里的node_type_get_types,没有什么用,这个程序是同别的地方复制过来的。

在表单提交函数uc_batch_price_batch_form_submit里面,我们首先获取所有需要处理的产品类型,注意这里是如何提取复选框里面的数据的,我们使用了array_filter

$types = array_filter($form_state['values']['types']);

接着,我们定义了一个批处理:

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

我们看到,批处理就是一个数组,里面包含几个键:operationsfinishedtitleinit_messageprogress_messageerror_messagefile。这里面的titleinit_messageerror_message都比较好理解,分别用来设置标题、初始化消息、错误消息;progress_message,用来设置批处理过程中的消息,提示用户完成了多少,这个我一般没有用过,一直都没有用过,因为上面有个进度条,可以看到大致的进度;file用来指定批处理所在的文件,在批处理的过程中,完成一批就会发送一个新的HTTP请求,新的请求过来以后,默认会加载所有的module文件,如果我们这里没有设置这个file文件的话,由于我们将它放到了uc_batch_price.admin.inc里面,Drupal就会找不到我们的批处理函数,这样就会报错,通过设置file,系统就可以加载uc_batch_price.admin.inc文件,继续执行里面的批处理函数了。

以前的时候,我都是把批处理的逻辑代码放到module文件里面的,也尝试过几次,放到admin.inc这样的文件里面,总是报错,后来在阅读别人的代码时,发现批处理没有放到module文件里面,我才学会用了通过设置这个file键,将批处理的逻辑放到inc文件里面。这是一个小的进步。我昨天,看别人写的一篇文章,如何在themetemplate.php文件里面实现mytheme_form_alter这个钩子函数,以前就不知道这个技巧,在Drupal6下面是不可以这样的,但是在Drupal7下面,却是可以的,技术在分享中进步。我通过阅读module_invoke_allmodule_implementsystem_listAPI函数,终于弄明白了为什么mytheme_form_alter也可以工作的原因,原来是这样的,在system_list里面,获取模块列表的SQL语句是这样写的:

$result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC");

这里同时支持了thememodule,一下子就明白了为什么。我对批处理里面的file键的学习,就和这个差不多,在阅读别人的代码里面,看到了这个用法,留心了一下,下次实现同样的功能时,就派上了用场。

finished是用来指定批处理的完成函数的,这里指定为uc_batch_price_batch_finished。我们来看看批处理完成函数的模式:

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

在这里面,逻辑处理非常简单,如果成功了,设置一个成功的消息,如果失败了设置一个失败的消息,消息的设置,使用的是drupal_set_message;这里唯一有点难以理解的是

$error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE)

我们只需要知道,这段代码是用来显示错误消息的即可,对于error_operation里面的结构,有兴趣的可以继续研究,工作中是用不到的。所以我们在编写批处理的过程中,对于这个批处理完成函数,只需要修改它的函数名字,消息的内容,包括成功时的消息、失败时的消息,就这三个地方。其它工作就是复制粘贴。

我们来看看批处理数组的最后一个参数operations

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

这是一个数组,数组里面又是一个数组;在里面的数组中,第一个uc_batch_price_price_process,就是批处理的处理函数,第二个array($types),就是传递给处理函数的参数;我们从operations这个名字可以看出,它是一个复数形式,就是可以支持多个处理函数,但是我用到的都是一个处理函数就够了的情况;另外需要注意的是,参数的传递,也是采用数组的形式,这意味着我们可以传递多个参数。注意,在Drupal7下面,带有参数的处理函数是这样写的:

function uc_batch_price_price_process($types, &$context)

这里的$types放到了&$context前面,我记得以前在Drupal6下面,参数的传递好像是这样的:

function uc_batch_price_price_process(&$context, $types)

如果在Drupal7下面也这样写的话,就会出问题,我开始就是这样搬过来的,结果接收不到参数,不知道为什么。所以这里特别强调一下,这个参数的顺序。

在表单提交函数的最后,使用batch_set($batch),这样就将我们定义好的批处理传递给了Drupal系统本身。batch_set($batch),这句话,就这么用的,不需要修改。

我们现在来看一下批处理的处理函数里面的逻辑,这里面的逻辑大致包含三大块,头部、中部、尾部。头部和尾部的代码在每个同类的函数中变化都不大,只有中间部分的代码,才真正是我们需要着重修改的。

我们来看头部代码:

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

这里的size是用来定义一批次,能够处理多少条数据的,我一般使用20,根据实际也会有所调整。比如每条数据的计算工作量比较大时,我们可以把size调的小一点;单条数据的计算量比较小时,我们就可以把size相应的调的大一点。

接下来,是一个初始化设置,用来设置上下文里面的三个变量,progresslast_nidmax;其中progressmax是基本上所有的场合都用到的,progress表示已经处理了多少,max表示总共有多少需要处理,为了获取总共有多少条记录需要处理,我们这里使用了db_select查询语句;而last_nid是我们这里用到的,或者说是特有的,它表示最后处理的节点的nid。注意这些变量都是存放到$contextsandbox里面的,sandbox中文常常翻译为沙盒。

我们现在来看看底部的代码部分:

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

这段代码的含义是,如果进度progress等于最大记录时,将$context['finished']设置为1,否则将它设置为两者的除数。当$context['finished']等于1时,批处理系统就会结束处理,调用批处理的完成函数。

掐头去尾后,就剩下了中间的逻辑部分,在中间的这部分代码中,我们首先获取当前需要我们处理的这一批数据,然后对这一批数据进行迭代循环,每循环一次progress都会自动加1,我们同时将last_nid设置为当前的节点的nid。这里面唯一需要一点技巧的就是,如何获取当前批次的所有数据,我们这里使用了db_select

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

我们这里可以看到last_nid的用处了,由于我们是按照节点从小到大的顺序来处理的,通过使用last_nid,就可以将已经处理过了的排除在外了,读取过来的,恰好是我们当前需要处理的数据。

迭代循环里面的逻辑代码,我们这里的逻辑是加载当前节点,根据我们设置好的折扣,来修改对应角色的价格,修改过后保存节点。这部分是需要我们自己编写的。

通过这个例子,我们可以看到,批处理的相关代码,三段论,固定格式的,我们只需要改改里面的名字,然后再把中间部分的逻辑处理部分改为我们自己的即可,写过一次以后,下次再写,就跟走平地一样,和写一个只需要处理20条记录的程序代码,难度差不多,费不了多少事。

我们最后顺便看一下,这个模块的另一个钩子实现,hook_node_presave

function uc_batch_price_node_presave($node) {

  if (!empty($node->role_prices)) {

    $enabled = variable_get('uc_price_per_role_enabled', array());

    $enabled_roles = array_filter($enabled);

    $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

$list_price = $node->list_price;

foreach($enabled_roles as $rid => $enabled){

      if(empty($node->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

        $node->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

      }

    }

  }

}

    在保存节点之前,如果特定的会员价格为空的,我们根据默认折扣,为它赋值,这样用户就不需要分别计算不同会员的价格了。这里用的是hook_node_presave,类似的钩子还有

hook_node_insert,hook_node_update,hook_node_load,hook_node_view,hook_node_view_alter,hook_node_delete,hook_node_validate,hook_node_prepare。随着对应的entity钩子的流行,这些特定于节点的钩子函数,用途越来越窄了。


Drupal版本: