綠界B2C超商物流串接

寫在前面

最近手上要處理的事情偏多,不管是公事還是私事都一起跑出來,所以就有些發懶沒有維持寫筆記的習慣。
找了個空檔,把前陣子串接綠界超商物流的東西整理起來,這個作法是不依賴官方提供的sdk 或是網路上其他開源套件,
用比較基本的方法來進行實做,好處是不用擔心版本支援還是其他異動,缺點則是重複造輪子。

但是在造輪子的過程,也是可以學到蠻多東西,而且這次的內容,會整併到公司的框架中,未來就可以直接使用,就不覺得浪費時間了。

相關資訊:
綠界超商物流api文件
開發者測試後台

特店編號(MerchantID):2000132
廠商後台登入帳號:stagetest1234
廠商後台登入密碼:test1234
廠商後台登入統一編號:53538851
串接金鑰HashKey:5294y06JbISpM5x9
串接金鑰HashIV:v77hoKGq4kWxNNIS

使用情境

這次的情況是我們有一個購物車網站,站方希望提供超商取貨的服務,但要先完成結帳付款。
所以我們預計串接的內容為綠界超商B2C,綠界提供的B2C的超商有統一超商(7-ELEVEN)、全家以及萊爾富。

流程為:
消費者在購物車網站結帳頁面選擇超商取貨->根據消費者選擇的超商,開啟綠界對應的超商地圖->
綠界告知我們消費者選擇的超商資訊->成立訂單之後到綠界成立物流訂單

暫時不提供站方在我們這裡列印托運單、逆物流等功能,以下程式碼說明:

經過綠界超商地圖選擇指定寄送的超商

  1. 結帳付款頁面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 選擇超商按鈕
    $("button").on("click", function() {
    window.open("cvs-map?cvs=UNIMART&IsCollection=N", "", "width=800,height=800");
    // 開啟跳台頁面,這邊除了帶超商之外,我們將是否代收也提出來,方便未來可以在其他站使用代收的機制
    });

    // 準備一個全域函式,待取得超商資訊後,供另開視窗的頁面將資訊傳進來,放到要顯示的位置
    window.map_return = function(info) {
    info = JSON.parse(info); // 我們預期傳進來的參數會是被json_encode的超商資訊
    $("#cvs_title").value(info.CVSStore);
    };

  2. 跳台頁面 cvs-map

這邊先看到綠界的api文件,其要求傳送過去條件為
Accept:text/html
Content Type:application/x-www-form-urlencoded
HTTP Method:POST
這邊可以有兩種方法:

  1. 是將資料帶到前端,利用form post
  2. 在server端透過php 的方法去呼叫api

因為這次的內容會有兩次需要呼叫綠界的api,所以我們分別用這兩個方法來實做看看。

  • 利用html form post 的方式去綠界開啟超商地圖選擇頁面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    <!DOCTYPE HTML>
    <html>
    <body>
    <form action="綠界超商地圖endpoint" id="form" method="POST">
    <input name="MerchantID" type="hidden" value="綠界MerchantID" />
    <input name="LogisticsType" type="hidden" value="CVS" />
    <input name="LogisticsSubType" type="hidden" value="UNIMART" /> <!-- 根據前一頁傳遞進來的超商 -->
    <input name="IsCollection" type="hidden" value="N" /><!-- 根據前一頁傳遞進來的是否代收 -->
    <input name="ServerReplyURL" type="hidden" value="your_call_back_url" />
    </form>
    <script>
    document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("form").submit();
    });
    </script>
    </body>
    </html>
  1. ServerReplyURL (callback)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php //>

    // 確認有收到來自綠界的通知($form)後,可以將資訊json_encode塞在網址送到前端去
    $info = json_encode($form);
    $redirect_url = "https://sample.com/ecpay/shipping/map-return?info={$info}";
    return "<script>window.location.href = {$redirect_url}";</script>;

    // 如果有使用樣版引擎,則只要將參數都回傳出去就可以了

  2. 收到通知頁面 (map-return)

1
2
3
4
5
6
7
8
// 從網址取下的info
const info = "網址列上的?info='超商資訊'";

// 或是樣版引擎傳出來的參數

// 因為是另開頁面,所以可以呼叫opener的全域變數,也就是我們原本結帳頁面準備好要接收資訊的function,將info 傳回去並關閉此頁
window.opener.ecpayShippingCvsMapReturn(info);
window.close();

成立綠界物流門市訂單

根據我們的前提,站方不提供超商取貨時才付款的功能,所以會先在我們的網站成立一筆訂單,並引導消費者進行付款,
付款完成後,待站方確認出貨時,才會告訴綠界我要建立這筆門市訂單。

  1. 建立門市訂單
  • 利用php http_build_query() 以及 file_get_contents() 方法,呼叫綠界api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php //>

$cfg = "取出存放的綠界資訊";
$order = "此次建立的訂單";

$data = [
'MerchantID' => $cfg['MerchantID'],
'MerchantTradeNo' => $order['order_no'],
'MerchantTradeDate' => date('Y/m/d H:i:s'),
'LogisticsType' => 'CVS',
'LogisticsSubType' => $order['shipment'], // 前台選擇的運送方式
'GoodsAmount' => round($order['amount'] + $order['shipping']),
'SenderName' => $cfg['SenderName'],
'SenderPhone' => $cfg['SenderPhone'],
'ReceiverName' => $order['name'],
'ReceiverCellPhone' => $order['phone'],
'ServerReplyURL' => 'your_call_back_url',
'ReceiverStoreID' => $order['store_id'], // 前一步驟收到的超商ID
];

$result = self::request('Create', $data, $cfg);

// 最後根據$result 是null 還是 false 還是參數($values) 進行處理
if ($result) {
$order['cvs_shipping_code'] = $result['RtnCode'];
$order['cvs_shipping_msg'] = $result['RtnMsg'];
}

// update $order

return $order;

private static function checksum($data, $key, $iv) {
unset($data['CheckMacValue']);

ksort($data);

$sign = 'HashKey=' . $key;

foreach ($data as $name => $value) {
$sign = "{$sign}&{$name}={$value}";
}

$sign = strtolower(urlencode($sign . '&HashIV=' . $iv));

$sign = str_replace('%20', '+', $sign);
$sign = str_replace('%21', '!', $sign);
$sign = str_replace('%28', '(', $sign);
$sign = str_replace('%29', ')', $sign);
$sign = str_replace('%2a', '*', $sign);
$sign = str_replace('%2d', '-', $sign);
$sign = str_replace('%2e', '.', $sign);
$sign = str_replace('%5f', '_', $sign);

return strtoupper(md5($sign));
}

private static function request($api, $data, $cfg) {
$data['CheckMacValue'] = self::checksum($data, $cfg['HashKey'], $cfg['HashIV']); // 綠界提供的檢查碼機制,雙方溝通會先確認組出來的檢查碼是否相同,正確才會繼續解析資料

$context = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query($data),
],
];

$response = file_get_contents("{$cfg['url']}{$api}", false, stream_context_create($context));

if ($response) {
$tokens = explode('|', $response);
// 成功的話,綠界會回 1|參數資料
if (@$tokens[0] === 1) {
parse_str($tokens[1], $values);
// 我們也要先確認檢查碼相同,才能確認此次的資料室可以使用的
if (@$values['CheckMacValue'] === self::checksum($values, $cfg['HashKey'], $cfg['HashIV'])) {
return $values;
}
}

return null;
}

return false;
}

  1. ServerReplyURL (callback)

這是綠界物流狀態改變時,會通知我們最新的物流狀態。
通知的內容與第一次建立門市訂單時response 差不多,流程就是

  • 檢查CheckMacValue
  • 利用MerchantTradeNo 找到資料庫中對應的訂單
  • 修改最新的物流狀態供消費者查詢

這邊就不放程式碼了。

結語

以上是這次串接綠界超商物流的筆記,呼叫綠界api 取得超商地圖的方法是用form post;
而後續建立門市訂單,因為是背景執行,所以我們使用php 的方法來達成api 呼叫。
兩種方法都可以實做,就看使用情境與流程怎麼搭配比較順暢。
後續如果完成托運單列印的部分再另外補上來,目前可以先從綠界的廠商後台進行列印。