訪客購物車 programmatically


需求:每個訪客都會預設幫他們將一件貨物加到購物車
他們便可以直接完成交易

Commerce kickstart已經是一個現成的示範
訪客就算不注冊仍然可以使用購物車
而且一定情況之下關閉瀏覽器之後再回到網站仍然可以找回上次的紀錄繼續購物

但在 init 的時候使用

<?php
commerce_order_new
(0);
?>
是不會將 order 連接到現有的瀏覽器進程的

你需要的是:

<?php
commerce_cart_order_session_save
($order->order_id);
?>
AttachmentSize
download.png11.64 KB

Drupal commerce - 從代碼建立訂單


Drupal commerce 己經成為 Drupal 電子商務應用的主流了,ubercart 落伍了

Ubercart 是一個源自 Drupal5.x 世代的方案
而為了使用 Drupal7 方便的 entities,和令電子商務方案使用一個「弱品牌化」的名字,Drupal commerce 誕生了

但因為 Drupal commerce 龐大的代碼量,為了方便分散工作,代碼都打散到不同的模組,各自有他們的維護者
再加上為數不少的 dependencies 令上手的難度過大
所以 Drupal commerce kickstart 便將一個完整的電子商務網站包裝起來
只需要下載安裝,匯入範例便可以立即使用了

這次的開發和前一次類似,從代碼建立將貨物放到訂單:

<?php
$cp
= commerce_product_new($type);
$cp->is_new = TRUE;
$cp->revision_id = NULL;
$cp->uid = $user->uid;
$cp->status = 1;
$cp->language = LANGUAGE_NONE;   
$cp->created = $cp->changed = time();
$cp->sku = 'shirt_od' . $extras['title'] . drupal_hash_base64(microtime());
$cp->title = $cp->sku;
?>

使用

<?php
field_attach_submit
()
?>
儲存貨品,便可以觸發相對應的 hooks 了:
<?php
$price
= array(LANGUAGE_NONE => array(0 => array(
   
'amount' => $price * 100,
   
'currency_code' => commerce_default_currency(),
)));
$form_state['values']['commerce_price'] = $price;

// custom fields
$form_state['values']['field_quantity']     = array(LANGUAGE_NONE => array(0 => array('value' => $quantity)));
$unit_price = array(LANGUAGE_NONE => array(0 => array(
   
'amount' => $unit_price * 100,
   
'currency_code' => commerce_default_currency(),
)));
$form_state['values']['field_unit_price']     = $unit_price;
field_attach_submit('commerce_product', $cp, $form, $form_state);

commerce_product_save($cp);
?>

購物車:

<?php
$order
= commerce_order_new($uid, 'cart');
commerce_order_save($order);

$line_item = commerce_product_line_item_new($cp, 1, $order->order_id);
// Save the line item to get its ID.
commerce_line_item_save($line_item);

// Add the line item to the order using fago's rockin' wrapper.
$order_wrapper = entity_metadata_wrapper('commerce_order', $order);
$order_wrapper->commerce_line_items[] = $line_item;

// Save the order again to update its line item reference field.
commerce_order_save($order);
?>

Drupal feeds module: FeedsProcessor with ubercart Order

使用者情境:批量使用 CSV 匯入 ubercart 訂單

Feeds 模組已經有一個 CSV 格式讀取 parser,又有批量 batch 支持,能處理大檔案 CSV ,Feeds 的代碼也有很多使用的經驗,而且最重要的是,模組自己已經有四種內容可以匯入:(nodes, users, terms, feednode?),有更高的機會支持其他內容 (ubercart orders)

先從 “The developer's guide to Feeds” 開始研究:(https://www.drupal.org/node/622700)
有三種 plugin: fetcher,parser 和 processor

  • Fetcher 是「獲得匯入源的方法」feeds module 已經有一個檔案上載的 fetcher
  • Parser 是「如何分析,讀取匯入源」feeds module 已經有一個 CSV parser
  • Processer 的工作是將處理過的資料儲存成為 Drupal 的內容

建立一個新模組 ubercart_order_feeds.module:

<?php
/**
* Implementation of hook_feeds_plugins().
* http://drupalcontrib.org/api/drupal/contributions%21feeds%21feeds.api.php/function/hook_feeds_plugins/6
*/
function ubercart_order_feeds_feeds_plugins() {
 
$path = drupal_get_path('module', 'ubercart_order_feeds');
 
$info = array();
 
$info['FeedsOrderProcessor'] = array(
   
'name' => 'Order processor',
   
'description' => 'Create orders.',
   
'help' => 'Create orders from parsed content.',
   
'handler' => array(
     
'parent' => 'FeedsProcessor',
     
'class' => 'FeedsOrderProcessor',
     
'file' => 'FeedsOrderProcessor.inc',
     
'path' => $path,
    ),
  );
  return
$info;
}
?>

建立一個新檔案 FeedsOrderProcessor.inc:
<?php
class FeedsOrderProcessor extends FeedsProcessor {
}
?>

首先在 getMappingTargets() 定義 CSV 中會用到的欄位:

<?php
 
/**
   * Return available mapping targets.
   */
 
public function getMappingTargets() {
   
$targets = array(
     
'sku' => array(
       
'name' => t('Product SKU'),
       
'description' => t('SKU of the product.'),
      ),
     
'quantity' => array(
       
'name' => t('Quantity'),
       
'description' => t(''),
      ),
     
'cost' => array(
       
'name' => t('Cost'),
       
'description' => t(''),
      ),
     
'delivery_first_name' => array(
       
'name' => t('Delivery: First name'),
       
'description' => t(''),
      ),
     
'delivery_last_name' => array(
       
'name' => t('Delivery: Last name'),
       
'description' => t(''),
      ),
   
//....
?>

在 CSV 內的資料處理過之後,資料的值會根據 Array 的 key 放到 object 之內.

我輸出了一個原生的 order object,找出原本就有的預設欄名,定義 mappings 的時候重用它們,以省卻一點點麻煩 (i.e. delivery_first_name, delivery_last_name above and more)

process() 是主要的程式進入點,複製一點 NodeProcessor 的代碼:

<?php
 
public function process(FeedsImportBatch $batch, FeedsSource $source) {
   
// Count number of created and updated nodes.
   
$created  = $updated = $failed = 0;

    while (
$item = $batch->shiftItem()) {
     
// Map item to a term.
     
$order = $this->map($batch);
     
//.......
?>

來到這一點出現一個新的函數 map() 來返回一個全新的 ubercart order object:

<?php
 
/**
   * Execute mapping on an item.
   */
 
protected function map(FeedsImportBatch $batch, $target_order = NULL) {
   
// Prepare user account object.
   
if (empty($target_order)) {
     
$order = uc_order_new($user->uid, 'completed');
    }

   
// Have parent class do the iterating.
   
return parent::map($batch, $order);
  }
?>

parent::map() 函數會將 CSV 內的資料處理過之後,根據 mapping array 的 key 放到 object

程式化的建立 order

process() 的下一步是將 product 加到 order,並儲存之

使用 vid 載入一個 product: <?php uc_product_load() ?>
http://drupalcontrib.org/api/drupal/contributions!ubercart!uc_product!uc...

加 product 到 order:
<?php $order->products[] = $product; ?>

再填入一些 order 的基本 mail 和 payment method 之後,儲存,完成 sale:

<?php
uc_order_save
($order);
uc_cart_complete_sale($order, TRUE);
?>

你亦可以加入 payment method
<?php uc_payment_enter($order->order_id, 'bank_transfer', 0, 0, NULL, t('Checkout completed for a free order.')); ?>

Github 源碼 repo: https://github.com/joetsuihk/ubercart_order_feeds

One more thing

Processor 可以有自己的設定頁面
在我的情境,因為客戶會將同一個模組放到不同的網站,而他們有不同的 webform node ID
所以我便將 ID 放到設定頁面令客戶可以更方便的修改設定了

<?php
 
/**
   * Override parent::configDefaults().
   */
 
public function configDefaults() {
    return array(
     
'card_form_id' => '',
     
'mappings' => array(),
    );
  }

  public function
configForm(&$form_state) {
   
$form = array();
   
$form['card_form_id'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Card webform node ID'),
     
'#default_value' => $this->config['card_form_id'],
    );
    return
$form;
  }
?>

Audi Home of Quattro 技術回顧

https://www.audiquattro.hk/

這是一個在維港海上的車展
展示最新的 S3 Sportback, RS6 Avant 和讓用戶可以分享他們最喜愛的駕駛路線

在桌面和手持裝置上,有活力的頁面設計和動畫過場都能將品牌的形象展現
在互動性的地圖上帶出享受的體驗,不論在船上或網站上

基本架構: PHP codeigniter, jQuery mobile,

Mobile site

這次沒有使用 responsive design 而使用一個獨立的手機版網頁 (https://www.audiquattro.hk/?device=mobile 直接在桌面看手機版)
然後在每一個頁面根據裝置跳轉
使用Mobile detect做裝置檢測
並使瀏覽器跳轉到正確的 URL

Google map 樣式客製

我們使用了自定義樣式的地圖
Google map 本身有提供一個強大的樣式功能 (https://developers.google.com/maps/documentation/javascript/styling?csw=1)
可修改道路,海面,公園等的顏色
令我們可以將地圖完美配合到網站的色調

Google map 路徑API

這個網站中一個互動的功能是用戶可以建立並分享他們覺得有趣和有挑戰的行車路線
我們使用自定的錨點和 Google directions API (https://developers.google.com/maps/documentation/directions/)
令用戶可以點擊兩個地點而劃出行車路線

也對應手機版本
我們使用手機上的 html5 geolocation API 令用戶可以使用用戶現在的位置設計路線

這一次我們深入地客製google maps 和使用它的數個週邊 API
是一個全新的挑戰
而且得出的效果也非常好,也體驗到 Google map API 的強大
我們可以在上面建立順暢的過場動畫,錨點,markers,overlays等等
就像是一個客製的地圖一樣

Video playing

每一個知名的車手都有4段視頻,車手們之間使用 slideshow 的形式表現
我們使用的是 html5 video plugin http://www.videojs.com/ 播放伺服器上的 mp4, ogg
所以 html 上會有12段視頻
但我們發覺 browser 是有一個 video 的緩存限制
令 video 的 buffer ready event, onload event 不會啟動
(我們只會在 4段 video 都 buffer 完成的時候才會播放)
但這個情況只會在一個頁面放多於 10 個 video 的時候出現
而我們又找不到 browser 相關的技術文件
所以我們只好在 slideshow event 後 insert/remove video tags 來處理「一個頁面不多於 10 個視頻」的限制

Hennessy Artistry 2013 技術回顧

Hennessy Artistry 2013

這是一個送門票的活動,游戲旳玩法如下:

  1. 用戶甲參加游戲
  2. 在三十分鐘之內邀請另外兩個朋友 like 和參加游戲
  3. 他們一共會得到三張門票

一個看似簡單但其實複雜的流程,果難在於:

  • 一天之內只可以有十組人得到門票
  • 20 日的活動其間, 一個人只可以拿到一張門票
  • 三十分鐘的時限一過,用戶便會失去位置,其他人便可以「搶」這三個位置
  • 邀請方和被邀請方的顯示方式,文字都不一樣

最後的一點令我們底估了這個案子的難度和所需的時間。我們沒有預估到測試邀請方和被邀請方的互動是會如此困難。除此之外,還有幾個技術性的問題可以談談:

DNS 問題

我們發覺頁面的載入時間非常長,大約需要5秒的時間,不論是轉換語言,或者是 AJAX 請求都一樣。我們明白這個 tab 有比較多和 facebook 的互動,用戶的資料, signed request,access token,extended access token 等,所以有一些載入的時間是正常的,但5秒真的是太長了。所以我們立即做了一些基本的伺服器優化。一個 Redhat 的伺服器,簡單的 APC, mysqltuner.pl 之後,快了 5ms ,太好了。

這個應用也有很多查詢,有多少空位置,多少個可以參加的朋友,那個朋友已經參加了等等,所以增加了 innodb 緩存,table cache 加大等等⋯⋯無用

然後發覺開發的環境只需要大約一秒便可以返回頁面,就將問題指向網路問題了。但直存最伺服器上的圖片不發覺有問題,速度很快, ping facebook.com: 200ms 看來也是正常的範圍 graph.facebook.com? 200ms. 等一下,本機的 ping 是: 2.8ms !?!?

直接修改 /etc/hosts:
31.13.68.49 graph.facebook.com www.facebook.com

BLOOM! 返回時間由 5s 變成 1s.

Facebook notification api

https://developers.facebook.com/docs/games/notifications/ 看來是簡單到不行。POST 到一個指定的路徑,帶上用戶的 access token,返回 JSON 結果,簡單又容易。你需要產生一個 app access token,放在代碼內,一直重複使用便可。

發佈到用戶的的牆上,使用 extended access token

傳統上, Facebook 提供一個 share dialog: https://developers.facebook.com/docs/reference/dialogs/feed/. 可以照顧到一大部份的要求,但:
縮圖只有一個小方格 75x75
需要用戶的動作才可
所以 facebook 有一個新的權限 “publish_actions” ,可以使用 api 發佈到用戶的牆上, API ref: https://developers.facebook.com/docs/reference/api/post/ 例子: https://developers.facebook.com/docs/php/howto/postwithgraphapi/

提一下,這裡使用的是用戶自已的 access token,和 access token 是會過期的,而預設返回的 access token 很快便會愈期,或者用戶登出都會做成 access token 愈期。你可以使用setExtendedAccessToken() https://developers.facebook.com/docs/reference/php/facebook-setExtendedA... 拿到一個大約兩個月愈期的 token,和用戶的資料一起儲這個長度無上限的 token。

關於 apprequest

Screenshot: https://developers.facebook.com/docs/reference/dialogs/requests/ 是一個有用戶的朋友可以選取的 popup,但有一個容易被人忽略的重點:這是一個 app 的邀請,而非一個 fan page 的邀請。被邀請的用戶會在他的通知欄看到一個邀請,點擊之後會連到 app 的頁面 (app.facebook.com),而非粉絲頁,並不可以修改。這個重要的不同令這裡需要多一個跳轉的機制,由 app 頁跳到粉絲頁面。

實際的操作是如果使用者是透過通知欄跳轉,會有一個沒文檔的 URL parameters fb_source=notification ,檢測存在後使用 window.top.location.href 跳轉到粉絲頁,完全是一片混沌。

Facebook tab + mobile

最後,在移動裝置上的粉絲頁是看不到 Facebook tab 的。Facebook 提供一個選項 “mobile URL” ,但直到現在,我仍然不知道這個選項上的路徑會在什麼時候使用到。但我們仍然做了一個手機版,推廣的同事就可以對應移動裝置使用另一個 URL

Snapshot: 

Pages

Google