2017年3月23日 星期四

網路購買機車與實體店家購買的差別

一直說要換個大一點的機車,卡了很久終於成行了。尋找的方向很簡單,機車車墊長一點、置物箱大一點,可以完成近程多載個小孩就行,於是乎,找了 YAMAHA 勁豪125。結果整天騎 100 的我,有點不適應大車。然後很大的置物箱卻還是擺不進一頂 3/4罩 安全帽(XL) 有點小失望,所幸已經很習慣都掛在車外 XD

聊點其他的,在找購買車子時,逛了 PCHOME/UDN 賣機車的,再去跟實體店家訊問價錢,發現一個很奇妙的地方:為何網路賣價跟實體店弄到好的價一樣(甚至更便宜),甚至網路還可以免息分24期,更別說刷信用卡還可以衝 1% 甚至 2% ,一台6萬的車,立馬少付 600、1200 等等

所幸實體店家給了我很多解惑:

  • 實體店家跟公司拿貨都有統一價
  • 便宜一點的機車,通常是屬於業績車
  • 網路上行銷總價便宜,但實體店家因批貨統一價而無法跟進
  • 網路上的賣價可能不是弄到好,還得付購車手續費(燃料費、強制險、手續費等,以125的車可能落在1000~2500之間)
  • 實體店刷卡,得再多信用卡手續費;分期付款得再跟銀行搭配,通常第一期會添加後續應繳的利息,以至於後面看起來像沒利息
至於網路購買,有的人會說機車店不管保固等事,追了一下比較像實體店沒賺,只是幫公司把車交給網購者,但車子若真的有問題,仍不至於不修啦,不修就告狀公司 XD

此外,實體店跟網購的金額差別,大概接近網路價 + 1000 ≦ 實體店辦到好的價格(若網路價不是辦到好的價格,實體店就會贏不少)。還有還會分去年跟今年出廠的車差別,通常去年的會便宜一千(網購的價錢是買到 2017 年,但實體店可能買到2016年),另外,實體店也會有已領牌的業績車,有機會還可以折1500-2000不等

總之,買車嫌麻煩就網購信用卡刷下去,若現金足,不如就去實體店家問問看有什麼優惠跟網購 PK 吧,目前還是覺得實體店優勢可能跟網購處於伯仲之間了,想著想著就去實體店買,也可以比較快拿到車,買完車未來也會常在同處維修,老闆也開心,自己也安心,至少不會碰到先拆座墊吧 XD (後來我才知道,有些車先拆座墊是為了做 FW/Device 檢測)

道不同,不相為謀

每隔一段期間,同溫層又會彼此檢視著近況,近來越來越有強烈的孤僻感,或許就是「道不同,不相為謀」吧!

前陣子得知一些強者同事離職,細細品味後,稱得上在錯的時間遇上對的人,出現高頻的內耗,不得已只好分別。畢竟大多數的公司只需要完美執行老闆的策略,而非不斷跳出來 challenge 老闆的人,想著想著也感慨了起來。

除此之外,漸漸進入 35 歲的人生,偶爾哈拉起同好,倒不是每個人有強烈的求生意識的,不少如溫水煮青蛙,停滯了進展。但,沒有什麼是絕對的好或絕對的壞,永遠的事後話!在對岸發展的強者學弟,隨著公司角色的轉換,開始不 coding 了,卻也開始感到內心空虛寂寞覺得弱 XD 彼此寒暄後,人生就是一場場賭注罷了!只待小公司,若沒起色,那段就變成空白人生,若成了肯定比在大公司獲得更多;只待大公司,CP值、未來好推算,但往上的位置早已被卡位,容易成了永遠的螺絲釘。所以,只能彼此期許著未來是賭對的,繼續走下去了。

回到標題,我想,漸漸地眼不見為淨吧,為那些不同道的人心煩也挺浪費光陰的,給予什麼回應也無法清醒,那就都別說了,大家找著自己的同溫層,繼續黑皮下去,人生也沒什麼不好囉

2017年3月7日 星期二

使用 Facebook Graph API 取得使用者資料:性別、年紀、地區、教育來做問卷調查

幾年前開發 sign in via Facebook,然後就停擺了好一陣子,最近市場研究想做個問卷,才發現當年收集的資料不怎齊全 XD 再加上 Facebook Graph api 也改版幾次,並且越來越重視個資保護,有很多權限必須額外取得才行。

請參考 https://developers.facebook.com/docs/graph-api/reference/user 定義最準。

而一般問卷常用的年紀,在 Facebook 只有 13/18/21 這三種數值可用,要再更仔細去請用 FB ads 去發了;而教育資訊跟地區資訊也得額外要求額外的權限 user_education_history 跟 user_location 才能得到

最後,在用 graph api 來取得這些資訊吧:

/me?fields=id,name,email,education,gender,location,age_range
{
  "id": "##",
  "name": "Yuan-Yi Chang",
  "email": "@gmail.com",
  "education": [
    {
      ...
      "type": "High School",
      ...
    },
    {
      ...
      "type": "College",
      ...
    },
    {
      ...
      "type": "Graduate School",
      ...
    }
  ],
  "gender": "male",
  "location": {
    "id": "110765362279102",
    "name": "Taipei, Taiwan"
  },
  "age_range": {
    "min": 21
  }
}


預計先這般儲存吧:

age_range => 13/18/21
gender => None/Male/Female/Unisex
education => None/HighSchool/College/GraduateSchool

2017年3月2日 星期四

[PHP] Facebook Graph API v2.2 升級提醒 - CodeIgniter 2.x 與 Facebook PHP SDK v5

這幾天一堆 fb app 被這種信轟炸:YourFBApp 的新開發人員重要通知,簡言之就是 facebook graph 舊版 api 即將在 2017/03/25 失效,請立即更新。

Facebook 開放平台變更紀錄 - https://developers.facebook.com/docs/apps/changelog

追了一下,主因是很多手上的服務是在 2014 年底開發,當時就是用 v2.2 graph api 沒錯,關鍵之處可以用搜尋:

facebook-php-sdk-v4 $ grep -r "v2.2" *
src/Facebook/FacebookRequest.php:  const GRAPH_API_VERSION = 'v2.2';


非常精準的地發現自己就是這個族群,也感謝 Facebook 賜予工作機會(誤),這樣每兩年就有新工作可以做,非常開心!接下來就是痛苦的開始,還是一口氣從 v4 升級到 v5 吧!

前提:

由於使用 PHP CodeIgniter 2.x 架構,再加上 HA 架構,因此需要處理 SESSION 的儲存機制,在此就用 DB 處理,而對 v4 SDK 中,是直接在 FacebookRedirectLoginHelper 改寫一份對於認證所需資料儲存的東西,但在 v5 架構就更漂亮了,可以把負責資料儲存的東西傳入,不需再改成 FacebookRedirectLoginHelper。若服務仍在單一機器上運行,可以略過此設計。

v4:

<?php
// FacebookRedirectLoginCIHelper
namespace Facebook;
use Facebook\FacebookRedirectLoginHelper;
class FacebookRedirectLoginCIHelper extends \Facebook\FacebookRedirectLoginHelper {
private $sessionPrefix = 'FBRLH_';
public function __construct($redirectUrl, $appId = null, $appSecret = null) {
parent::__construct($redirectUrl, $appId, $appSecret);
$this->ci = & get_instance();
}
protected function storeState($state) {
if ($this->ci->session->set_userdata($this->sessionPrefix . 'state', $state)) {
$this->state = $this->ci->session->set_userdata($this->sessionPrefix . 'state', $state);
return $this->state;
}
return NULL;
}
protected function loadState() {
return $this->state = $this->ci->session->userdata($this->sessionPrefix . 'state');
}
}

v5:

<?php
// FacebookSessionPersistentDataCIHandler
namespace Facebook\PersistentData;
use Facebook\Exceptions\FacebookSDKException;
class FacebookSessionPersistentDataCIHandler implements PersistentDataInterface {
protected $sessionPrefix = 'FBRLH_';
public function __construct($enableSessionCheck = true) {
$this->ci = & get_instance();
}
public function get($key) {
if ($this->ci)
return $this->ci->session->userdata($this->sessionPrefix . $key);
return null;
}
public function set($key, $value) {
if ($this->ci)
$this->ci->session->set_userdata($this->sessionPrefix . $key, $value);
}
}


初始化:

v4:

require 'path/facebook-php-sdk-v4/autoload.php';
use Facebook\FacebookRedirectLoginCIHelper;
use Facebook\FacebookRequest;
use Facebook\FacebookSession;
use Facebook\GraphUser;

FacebookSession::setDefaultApplication($this->config->item("api_key"), $this->config->item("secret_key"));

v5:

require 'path/php-graph-sdk-5.4.4/src/Facebook/autoload.php';
use Facebook\Facebook;
use Facebook\Authentication\AccessToken;
use Facebook\PersistentData\FacebookSessionPersistentDataCIHandler;
use Facebook\FacebookRequest;

$fb = new Facebook([
'app_id' => $this->config->item("api_key"),
'app_secret' => $this->config->item("secret_key"),
'persistent_data_handler' => new FacebookSessionPersistentDataCIHandler(),
//'default_graph_version' => 'v2.8',
]);


取得登入網址:

v4:

$helper = new FacebookRedirectLoginCIHelper($callback_url);
$login_url = $helper->getLoginUrl($this->config->item('fb_scope'));

v5:

$helper = $fb->getRedirectLoginHelper();
$login_url = $helper->getLoginUrl($callback_url, $this->config->item('fb_scope'));


登入完成取得 access_token:

v4:

$fb_session = $helper->getSessionFromRedirect();
$access_token = $fb_session->getAccessToken();

v5:

$accessToken = $helper->getAccessToken();


取得 longlived access_token:

v4:

$long_lived_token = $fb_session->getLongLivedSession()->getToken();

v5:
$long_lived_token = (string) $fb->getOAuth2Client()->getLongLivedAccessToken($access_token);


查詢個人資料:

v4:

$user_profile = (new FacebookRequest($fb_session, 'GET', '/me'))->execute()->getGraphObject(GraphUser::className());
$uid = $user_profile->getProperty('id');
$name = $user_profile->getProperty('name');
$email = $user_profile->getProperty('email');
$profile_url = $user_profile->getProperty('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";

v5:

$user_profile = $fb->get('/me', $accessToken)->getGraphNode();
$uid = $user_profile->getField('id');
$name = $user_profile->getField('name');
$email = $user_profile->getField('email');
$profile_url = $user_profile->getField('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";


從字串初始化並檢查 token 是否過期:

v4:

use Facebook\FacebookSession;

$session = new FacebookSession( $token );
return $session->validate();

v5:

return $fb->getOAuth2Client()->debugToken( $token )->getIsValid() == true;


api 查詢架構:

v4:

function query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
$session = new FacebookSession($token);
                 if (!$session->validate())
return false;
$response = ( new FacebookRequest( $session, $method, $api ) )->execute();
if ($array_mode)
return $response->getGraphObject()->asArray();
return $response->getGraphObject();
}
return false;
}

v5:

function _node_query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphNode()->asArray() : $this->fb->post($api, $token)->getGraphNode();

return $array_mode ? $this->fb->get($api, $token)->getGraphNode()->asArray() : $this->fb->get($api, $token)->getGraphNode();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}

function _edge_query($token, $method, $api, $array_mode = false) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphEdge()->asArray() : $this->fb->post($api, $token)->getGraphEdge();

return $array_mode ? $this->fb->get($api, $token)->getGraphEdge()->asArray() : $this->fb->get($api, $token)->getGraphEdge();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}


取得使用者權限清單:

v4:

query($token, 'GET', '/me/permissions');

v5:

_edge_query($token, 'GET', '/me/permissions');


查詢朋友清單:

v4:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page));
if(isset($data['data']) && ($cnt = count($data['data'])) > 0) {
if (isset($data['paging']) && is_object($data['paging']) && property_exists($data['paging'], 'next')) {
if ($cnt >= $item_per_page)
$have_next_page = true;
}
$output = array();
foreach($data['data'] as $user)
if (is_object($user) && property_exists($user, 'id'))
array_push($output, $user->id);
return $output;
}
return false;

}

v5:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->_edge_query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page), false);
$items = $data->asArray();
if(is_array($items) && ($cnt = count($items)) > 0) {
$paging = $data->getMetaData();
if (isset($paging['paging']) && isset($paging['paging']['next']))
if ($cnt >= $item_per_page)
$have_next_page = true;
$output = array();
foreach($items as $user)
if (isset($user['id']))
array_push($output, $user['id']);
return $output;
}
return false;
}


收工!

2017年3月1日 星期三

一打一的世界

趁著連假,扛著一個小孩返鄉,體驗了走路、公車、捷運和高鐵。挑戰最大的是高鐵,在一個壅擠的座位上哄著小孩,也算是個磨練了。還好我不用選台鐵,那肯定是瘋了!

這陣子咀嚼著又似看透一些世俗話題,從黃毛小子、強說愁,總算累積了一點社會經歷,只是如同雷軍創業的初始心態:財務自由了,再來談創業、再來談不為錢做事的方向。很多事的起頭,都是有前提的,也認清自己不夠本去做哪些事。

返鄉看著兩老和周邊的考驗,漸漸浮現出這類現實的感觸,不再像學生時代的低機會成本,不再是年少輕狂自傲能扛著天,我想,漸漸地就像練武的人,什麼一次打十個,還是逃命吧 XD 一次打一個吧!

我想,我還是會積極地把握機會,但需要更沉得住氣、需要更多耐心和命運磨合磨合。