taiwan-logistics-skill 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,712 +1,712 @@
1
- # PayUni Logistics API Reference
2
-
3
- 統一金流 (PAYUNi) 物流 API 完整參考文件。
4
-
5
- ---
6
-
7
- ## 目錄
8
-
9
- 1. [API 端點總覽](#api-端點總覽)
10
- 2. [測試環境](#測試環境)
11
- 3. [加密機制](#加密機制)
12
- 4. [物流類型](#物流類型)
13
- 5. [7-11 超商取貨](#7-11-超商取貨)
14
- 6. [黑貓宅配](#黑貓宅配)
15
- 7. [物流狀態通知](#物流狀態通知)
16
- 8. [物流狀態查詢](#物流狀態查詢)
17
- 9. [錯誤碼對照表](#錯誤碼對照表)
18
- 10. [常見問題排解](#常見問題排解)
19
-
20
- ---
21
-
22
- ## API 端點總覽
23
-
24
- ### 基礎 API 路徑
25
-
26
- | 環境 | 基礎路徑 |
27
- |------|----------|
28
- | **測試環境** | `https://sandbox-api.payuni.com.tw/api/` |
29
- | **正式環境** | `https://api.payuni.com.tw/api/` |
30
-
31
- ### 物流相關端點
32
-
33
- | 功能 | 端點路徑 | 說明 |
34
- |------|----------|------|
35
- | 建立物流訂單 | `/logistics/create` | 建立物流託運單 |
36
- | 物流狀態查詢 | `/logistics/query` | 查詢物流狀態 |
37
- | 取消物流訂單 | `/logistics/cancel` | 取消物流訂單 |
38
-
39
- ---
40
-
41
- ## 測試環境
42
-
43
- ### 測試帳號
44
-
45
- 測試帳號請至 PayUni 後台申請:
46
-
47
- ```
48
- 後台網址: https://www.payuni.com.tw/
49
- 路徑: 會員 > 商店清單 > 指定商店名稱 > 串接設定
50
- ```
51
-
52
- 取得以下資訊:
53
- - **商店代號 (MerID)**
54
- - **Hash Key**
55
- - **Hash IV**
56
-
57
- ### 測試環境端點
58
-
59
- ```
60
- https://sandbox-api.payuni.com.tw/api/{endpoint}
61
- ```
62
-
63
- ### 注意事項
64
-
65
- 1. 物流幕後 API 需向 PayUni 申請開通
66
- 2. 建議使用固定 IP 主機,避免 IP 變動造成功能失效
67
- 3. 非即時付款 (超商代碼、虛擬帳號) 需等付款完成才會建立物流單
68
-
69
- ---
70
-
71
- ## 加密機制
72
-
73
- PayUni 採用 **AES-256-GCM** 加密與 **SHA256 HMAC** 驗證。
74
-
75
- ### 加密流程
76
-
77
- 1. **準備參數** - 組合所有請求參數
78
- 2. **URL Encode** - 將參數轉為 Query String
79
- 3. **AES-256-GCM 加密** - 使用 Hash Key 和 Hash IV 加密
80
- 4. **產生 HashInfo** - 使用 SHA256 計算驗證碼
81
- 5. **發送請求** - 將加密資料 POST 至 API
82
-
83
- ### PHP 加密範例
84
-
85
- ```php
86
- <?php
87
-
88
- class PayuniEncryption
89
- {
90
- private string $merKey;
91
- private string $merIV;
92
-
93
- public function __construct(string $merKey, string $merIV)
94
- {
95
- $this->merKey = $merKey;
96
- $this->merIV = $merIV;
97
- }
98
-
99
- /**
100
- * AES-256-GCM 加密
101
- */
102
- public function encrypt(array $params): string
103
- {
104
- // 1. 組合 Query String
105
- $queryString = http_build_query($params);
106
-
107
- // 2. AES-256-GCM 加密
108
- $tag = '';
109
- $encrypted = openssl_encrypt(
110
- $queryString,
111
- 'aes-256-gcm',
112
- $this->merKey,
113
- OPENSSL_RAW_DATA,
114
- $this->merIV,
115
- $tag,
116
- '',
117
- 16
118
- );
119
-
120
- // 3. 組合加密結果 (加密資料 + tag)
121
- $encryptInfo = bin2hex($encrypted . $tag);
122
-
123
- return $encryptInfo;
124
- }
125
-
126
- /**
127
- * AES-256-GCM 解密
128
- */
129
- public function decrypt(string $encryptInfo): array
130
- {
131
- // 1. Hex 轉 Binary
132
- $data = hex2bin($encryptInfo);
133
-
134
- // 2. 分離加密資料和 tag
135
- $encrypted = substr($data, 0, -16);
136
- $tag = substr($data, -16);
137
-
138
- // 3. AES-256-GCM 解密
139
- $decrypted = openssl_decrypt(
140
- $encrypted,
141
- 'aes-256-gcm',
142
- $this->merKey,
143
- OPENSSL_RAW_DATA,
144
- $this->merIV,
145
- $tag
146
- );
147
-
148
- // 4. 解析 Query String
149
- parse_str($decrypted, $result);
150
-
151
- return $result;
152
- }
153
-
154
- /**
155
- * 產生 HashInfo (SHA256)
156
- */
157
- public function hashInfo(string $encryptInfo): string
158
- {
159
- $raw = $encryptInfo . $this->merKey . $this->merIV;
160
- return strtoupper(hash('sha256', $raw));
161
- }
162
- }
163
- ```
164
-
165
- ### Python 加密範例
166
-
167
- ```python
168
- """PayUni AES-256-GCM 加密"""
169
-
170
- from Crypto.Cipher import AES
171
- from urllib.parse import urlencode, parse_qs
172
- import hashlib
173
-
174
-
175
- class PayuniEncryption:
176
- def __init__(self, mer_key: str, mer_iv: str):
177
- self.mer_key = mer_key.encode('utf-8')
178
- self.mer_iv = mer_iv.encode('utf-8')
179
-
180
- def encrypt(self, params: dict) -> str:
181
- """AES-256-GCM 加密"""
182
- # 1. 組合 Query String
183
- query_string = urlencode(params)
184
-
185
- # 2. AES-256-GCM 加密
186
- cipher = AES.new(self.mer_key, AES.MODE_GCM, nonce=self.mer_iv)
187
- encrypted, tag = cipher.encrypt_and_digest(query_string.encode('utf-8'))
188
-
189
- # 3. 組合加密結果
190
- encrypt_info = (encrypted + tag).hex()
191
-
192
- return encrypt_info
193
-
194
- def decrypt(self, encrypt_info: str) -> dict:
195
- """AES-256-GCM 解密"""
196
- # 1. Hex 轉 Binary
197
- data = bytes.fromhex(encrypt_info)
198
-
199
- # 2. 分離加密資料和 tag
200
- encrypted = data[:-16]
201
- tag = data[-16:]
202
-
203
- # 3. AES-256-GCM 解密
204
- cipher = AES.new(self.mer_key, AES.MODE_GCM, nonce=self.mer_iv)
205
- decrypted = cipher.decrypt_and_verify(encrypted, tag)
206
-
207
- # 4. 解析 Query String
208
- result = dict(parse_qs(decrypted.decode('utf-8')))
209
- return {k: v[0] for k, v in result.items()}
210
-
211
- def hash_info(self, encrypt_info: str) -> str:
212
- """產生 HashInfo (SHA256)"""
213
- raw = encrypt_info + self.mer_key.decode() + self.mer_iv.decode()
214
- return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
215
- ```
216
-
217
- ---
218
-
219
- ## 物流類型
220
-
221
- ### 支援的物流服務
222
-
223
- | 物流類型 | 代碼 | 溫層 | 說明 |
224
- |----------|------|------|------|
225
- | 7-11 店到店 (C2C) | `PAYUNi_Logistic_711` | 常溫 | 消費者自行寄件 |
226
- | 7-11 店到店冷凍 | `PAYUNi_Logistic_711_Freeze` | 冷凍 | 冷凍店到店 |
227
- | 7-11 大宗寄倉 (B2C) | `PAYUNi_Logistic_711_B2C` | 常溫 | 商家寄倉 |
228
- | 黑貓宅配常溫 | `PAYUNi_Logistic_Tcat` | 常溫 | 宅配到府 |
229
- | 黑貓宅配冷凍 | `PAYUNi_Logistic_Tcat_Freeze` | 冷凍 | 冷凍宅配 |
230
- | 黑貓宅配冷藏 | `PAYUNi_Logistic_Tcat_Cold` | 冷藏 | 冷藏宅配 |
231
-
232
- ### GoodsType 溫層代碼
233
-
234
- | 代碼 | 溫層 | 適用物流 |
235
- |------|------|----------|
236
- | `1` | 常溫 | 全部 |
237
- | `2` | 冷凍 | 7-11 冷凍、黑貓冷凍 |
238
- | `3` | 冷藏 | 僅黑貓宅配 |
239
-
240
- ### 撥款時間
241
-
242
- | 物流類型 | 撥款時間 |
243
- |----------|----------|
244
- | 超商取貨 | 取貨日 + 7 天 |
245
- | 黑貓宅配 | 取貨日 + 15 天 |
246
-
247
- ---
248
-
249
- ## 7-11 超商取貨
250
-
251
- ### 建立物流訂單
252
-
253
- #### 端點
254
-
255
- ```
256
- POST /api/logistics/create
257
- ```
258
-
259
- #### EncryptInfo 參數
260
-
261
- | 參數 | 類型 | 長度 | 必填 | 說明 |
262
- |------|------|------|------|------|
263
- | `MerID` | String | 20 | | 商店代號 |
264
- | `MerTradeNo` | String | 50 | | 商店訂單編號 |
265
- | `LogisticsType` | String | 50 | | 物流類型代碼 |
266
- | `GoodsType` | Integer | - | | 溫層 `1`:常溫 `2`:冷凍 |
267
- | `GoodsAmount` | Integer | - | | 商品金額 |
268
- | `GoodsName` | String | 50 | | 商品名稱 |
269
- | `SenderName` | String | 10 | | 寄件人姓名 |
270
- | `SenderPhone` | String | 20 | | 寄件人電話 |
271
- | `SenderStoreID` | String | 10 | 否 | 寄件門市代號 (C2C 必填) |
272
- | `ReceiverName` | String | 10 | | 收件人姓名 |
273
- | `ReceiverPhone` | String | 20 | | 收件人電話 |
274
- | `ReceiverStoreID` | String | 10 | | 收件門市代號 |
275
- | `NotifyURL` | String | 500 | | 物流狀態通知網址 |
276
- | `Timestamp` | Integer | - | | Unix 時間戳 |
277
-
278
- ### C2C vs B2C 差異
279
-
280
- | 項目 | C2C 店到店 | B2C 大宗寄倉 |
281
- |------|-----------|-------------|
282
- | 寄件方式 | 消費者自行至門市寄件 | 商家統一寄倉 |
283
- | SenderStoreID | 必填 | 不需要 |
284
- | 適用場景 | 個人賣家 | 企業電商 |
285
-
286
- ### 門市查詢
287
-
288
- 7-11 門市代號可透過以下方式取得:
289
-
290
- ```
291
- 7-11 電子地圖: https://emap.pcsc.com.tw/
292
- ```
293
-
294
- ### PHP 範例
295
-
296
- ```php
297
- <?php
298
-
299
- $encryption = new PayuniEncryption($merKey, $merIV);
300
-
301
- $params = [
302
- 'MerID' => 'YOUR_MER_ID',
303
- 'MerTradeNo' => 'LOG' . time(),
304
- 'LogisticsType' => 'PAYUNi_Logistic_711',
305
- 'GoodsType' => 1,
306
- 'GoodsAmount' => 500,
307
- 'GoodsName' => '測試商品',
308
- 'SenderName' => '寄件人',
309
- 'SenderPhone' => '0912345678',
310
- 'SenderStoreID' => '123456', // C2C 必填
311
- 'ReceiverName' => '收件人',
312
- 'ReceiverPhone' => '0987654321',
313
- 'ReceiverStoreID' => '654321',
314
- 'NotifyURL' => 'https://your-site.com/payuni_shipping_711_notify',
315
- 'Timestamp' => time(),
316
- ];
317
-
318
- $encryptInfo = $encryption->encrypt($params);
319
- $hashInfo = $encryption->hashInfo($encryptInfo);
320
-
321
- $ch = curl_init();
322
- curl_setopt_array($ch, [
323
- CURLOPT_URL => 'https://api.payuni.com.tw/api/logistics/create',
324
- CURLOPT_POST => true,
325
- CURLOPT_POSTFIELDS => http_build_query([
326
- 'MerID' => $params['MerID'],
327
- 'Version' => '1.0',
328
- 'EncryptInfo' => $encryptInfo,
329
- 'HashInfo' => $hashInfo,
330
- ]),
331
- CURLOPT_RETURNTRANSFER => true,
332
- ]);
333
-
334
- $response = curl_exec($ch);
335
- curl_close($ch);
336
-
337
- $result = json_decode($response, true);
338
-
339
- if ($result['Status'] === 'SUCCESS') {
340
- $data = $encryption->decrypt($result['EncryptInfo']);
341
- // $data['LogisticsID'] - 物流編號
342
- // $data['CVSPaymentNo'] - 超商繳費代碼
343
- print_r($data);
344
- }
345
- ```
346
-
347
- ### 回應參數 (解密後)
348
-
349
- | 參數 | 說明 |
350
- |------|------|
351
- | `LogisticsID` | PayUni 物流編號 |
352
- | `MerTradeNo` | 商店訂單編號 |
353
- | `CVSPaymentNo` | 超商繳費代碼 |
354
- | `CVSValidationNo` | 超商驗證碼 |
355
- | `ExpireDate` | 取貨期限 |
356
-
357
- ---
358
-
359
- ## 黑貓宅配
360
-
361
- ### 建立物流訂單
362
-
363
- #### 端點
364
-
365
- ```
366
- POST /api/logistics/create
367
- ```
368
-
369
- #### EncryptInfo 參數
370
-
371
- | 參數 | 類型 | 長度 | 必填 | 說明 |
372
- |------|------|------|------|------|
373
- | `MerID` | String | 20 | | 商店代號 |
374
- | `MerTradeNo` | String | 50 | | 商店訂單編號 |
375
- | `LogisticsType` | String | 50 | | 物流類型代碼 |
376
- | `GoodsType` | Integer | - | | 溫層 `1`:常溫 `2`:冷凍 `3`:冷藏 |
377
- | `GoodsAmount` | Integer | - | | 商品金額 |
378
- | `GoodsName` | String | 50 | | 商品名稱 |
379
- | `GoodsWeight` | Integer | - | 否 | 商品重量 (g) |
380
- | `SenderName` | String | 10 | | 寄件人姓名 |
381
- | `SenderPhone` | String | 20 | | 寄件人電話 |
382
- | `SenderZipCode` | String | 5 | | 寄件人郵遞區號 |
383
- | `SenderAddress` | String | 200 | | 寄件人地址 |
384
- | `ReceiverName` | String | 10 | | 收件人姓名 |
385
- | `ReceiverPhone` | String | 20 | | 收件人電話 |
386
- | `ReceiverZipCode` | String | 5 | | 收件人郵遞區號 |
387
- | `ReceiverAddress` | String | 200 | | 收件人地址 |
388
- | `ScheduledPickupDate` | String | 10 | 否 | 預定取貨日期 `yyyy/MM/dd` |
389
- | `ScheduledDeliveryDate` | String | 10 | 否 | 預定配達日期 `yyyy/MM/dd` |
390
- | `ScheduledDeliveryTime` | String | 2 | 否 | 預定配達時段 |
391
- | `NotifyURL` | String | 500 | | 物流狀態通知網址 |
392
- | `Timestamp` | Integer | - | | Unix 時間戳 |
393
-
394
- ### ScheduledDeliveryTime 配達時段
395
-
396
- | 代碼 | 時段 |
397
- |------|------|
398
- | `01` | 13:00 前 |
399
- | `02` | 14:00 - 18:00 |
400
- | `03` | 不指定 |
401
-
402
- ### 尺寸與重量限制
403
-
404
- | 溫層 | 材積 | 重量 |
405
- |------|------|------|
406
- | 常溫 | 150cm | 20kg |
407
- | 冷凍/冷藏 | 120cm | 15kg |
408
-
409
- **材積計算**: 長 + 寬 + 高 ≤ 限制
410
-
411
- ### PHP 範例
412
-
413
- ```php
414
- <?php
415
-
416
- $encryption = new PayuniEncryption($merKey, $merIV);
417
-
418
- $params = [
419
- 'MerID' => 'YOUR_MER_ID',
420
- 'MerTradeNo' => 'LOG' . time(),
421
- 'LogisticsType' => 'PAYUNi_Logistic_Tcat',
422
- 'GoodsType' => 1, // 常溫
423
- 'GoodsAmount' => 1000,
424
- 'GoodsName' => '測試商品',
425
- 'GoodsWeight' => 500, // 500g
426
- 'SenderName' => '寄件人',
427
- 'SenderPhone' => '0912345678',
428
- 'SenderZipCode' => '100',
429
- 'SenderAddress' => '台北市中正區某某路1號',
430
- 'ReceiverName' => '收件人',
431
- 'ReceiverPhone' => '0987654321',
432
- 'ReceiverZipCode' => '300',
433
- 'ReceiverAddress' => '新竹市東區某某路2號',
434
- 'ScheduledDeliveryTime' => '02', // 14:00-18:00
435
- 'NotifyURL' => 'https://your-site.com/payuni_shipping_tcat_notify',
436
- 'Timestamp' => time(),
437
- ];
438
-
439
- $encryptInfo = $encryption->encrypt($params);
440
- $hashInfo = $encryption->hashInfo($encryptInfo);
441
-
442
- $ch = curl_init();
443
- curl_setopt_array($ch, [
444
- CURLOPT_URL => 'https://api.payuni.com.tw/api/logistics/create',
445
- CURLOPT_POST => true,
446
- CURLOPT_POSTFIELDS => http_build_query([
447
- 'MerID' => $params['MerID'],
448
- 'Version' => '1.0',
449
- 'EncryptInfo' => $encryptInfo,
450
- 'HashInfo' => $hashInfo,
451
- ]),
452
- CURLOPT_RETURNTRANSFER => true,
453
- ]);
454
-
455
- $response = curl_exec($ch);
456
- curl_close($ch);
457
-
458
- $result = json_decode($response, true);
459
-
460
- if ($result['Status'] === 'SUCCESS') {
461
- $data = $encryption->decrypt($result['EncryptInfo']);
462
- // $data['LogisticsID'] - 物流編號
463
- // $data['ShipmentNo'] - 託運單號
464
- print_r($data);
465
- }
466
- ```
467
-
468
- ### 回應參數 (解密後)
469
-
470
- | 參數 | 說明 |
471
- |------|------|
472
- | `LogisticsID` | PayUni 物流編號 |
473
- | `MerTradeNo` | 商店訂單編號 |
474
- | `ShipmentNo` | 黑貓託運單號 |
475
- | `BookingNote` | 取貨編號 |
476
-
477
- ---
478
-
479
- ## 物流狀態通知
480
-
481
- ### Notify URL 設定
482
-
483
- | 物流類型 | Notify URL 格式建議 |
484
- |----------|---------------------|
485
- | 超商物流 | `https://your-site.com/payuni_shipping_711_notify` |
486
- | 黑貓宅配 | `https://your-site.com/payuni_shipping_tcat_notify` |
487
-
488
- ### 通知流程
489
-
490
- ```
491
- ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
492
- │ 物流商 │────▶│ PayUni │────▶│ 商店 │
493
- │ 狀態更新 │ │ 處理 │ │ NotifyURL │
494
- └─────────────┘ └─────────────┘ └─────────────┘
495
-
496
- │ POST (加密資料)
497
-
498
- ┌─────────────┐
499
- │ 商店 │
500
- │ 解密處理 │
501
- └─────────────┘
502
-
503
- │ 回應 "SUCCESS"
504
-
505
- ┌─────────────┐
506
- │ PayUni │
507
- │ 確認收到 │
508
- └─────────────┘
509
- ```
510
-
511
- ### 通知參數
512
-
513
- PayUni 會 POST 加密資料到 `NotifyURL`:
514
-
515
- | 參數 | 說明 |
516
- |------|------|
517
- | `MerID` | 商店代號 |
518
- | `EncryptInfo` | 加密的物流狀態 |
519
- | `HashInfo` | SHA256 驗證碼 |
520
-
521
- ### 解密後的通知內容
522
-
523
- | 參數 | 說明 |
524
- |------|------|
525
- | `MerID` | 商店代號 |
526
- | `MerTradeNo` | 商店訂單編號 |
527
- | `LogisticsID` | PayUni 物流編號 |
528
- | `LogisticsType` | 物流類型 |
529
- | `LogisticsStatus` | 物流狀態碼 |
530
- | `LogisticsStatusMsg` | 物流狀態訊息 |
531
- | `UpdateTime` | 狀態更新時間 |
532
-
533
- ### 處理範例
534
-
535
- ```php
536
- <?php
537
-
538
- // 接收通知
539
- $encryptInfo = $_POST['EncryptInfo'] ?? '';
540
- $hashInfo = $_POST['HashInfo'] ?? '';
541
- $merID = $_POST['MerID'] ?? '';
542
-
543
- // 驗證 HashInfo
544
- $encryption = new PayuniEncryption($merKey, $merIV);
545
- $calculatedHash = $encryption->hashInfo($encryptInfo);
546
-
547
- if ($hashInfo !== $calculatedHash) {
548
- echo 'HashInfo Error';
549
- exit;
550
- }
551
-
552
- // 解密
553
- $data = $encryption->decrypt($encryptInfo);
554
-
555
- // 根據物流狀態更新訂單
556
- switch ($data['LogisticsStatus']) {
557
- case '11':
558
- // 已出貨
559
- updateOrderLogisticsStatus($data['MerTradeNo'], 'shipped');
560
- break;
561
- case '21':
562
- // 已到店
563
- updateOrderLogisticsStatus($data['MerTradeNo'], 'arrived');
564
- break;
565
- case '22':
566
- // 已取貨
567
- updateOrderLogisticsStatus($data['MerTradeNo'], 'picked_up');
568
- break;
569
- case '31':
570
- case '32':
571
- // 退貨
572
- updateOrderLogisticsStatus($data['MerTradeNo'], 'returned');
573
- break;
574
- }
575
-
576
- // 回應 SUCCESS
577
- echo 'SUCCESS';
578
- ```
579
-
580
- ---
581
-
582
- ## 物流狀態查詢
583
-
584
- ### 端點
585
-
586
- ```
587
- POST /api/logistics/query
588
- ```
589
-
590
- ### EncryptInfo 參數
591
-
592
- | 參數 | 類型 | 必填 | 說明 |
593
- |------|------|------|------|
594
- | `MerID` | String | | 商店代號 |
595
- | `MerTradeNo` | String | | 商店訂單編號 |
596
- | `Timestamp` | Integer | | Unix 時間戳 |
597
-
598
- ### 回應參數 (解密後)
599
-
600
- | 參數 | 說明 |
601
- |------|------|
602
- | `LogisticsID` | PayUni 物流編號 |
603
- | `MerTradeNo` | 商店訂單編號 |
604
- | `LogisticsType` | 物流類型 |
605
- | `LogisticsStatus` | 物流狀態碼 |
606
- | `LogisticsStatusMsg` | 物流狀態訊息 |
607
- | `ShipmentNo` | 託運單號 |
608
- | `ReceiverStoreID` | 收件門市代號 (超商) |
609
- | `UpdateTime` | 狀態更新時間 |
610
-
611
- ### 貨態即時查詢
612
-
613
- | 物流類型 | 查詢網址 |
614
- |----------|----------|
615
- | 7-11 | https://eservice.7-11.com.tw/E-Tracking/search.aspx |
616
- | 黑貓宅配 | https://www.t-cat.com.tw/inquire/trace.aspx |
617
-
618
- ---
619
-
620
- ## 錯誤碼對照表
621
-
622
- ### 物流狀態碼 (LogisticsStatus)
623
-
624
- | 狀態碼 | 說明 |
625
- |--------|------|
626
- | `11` | 已出貨 |
627
- | `21` | 已到店 (超商) / 配達中 (宅配) |
628
- | `22` | 已取貨 / 配達完成 |
629
- | `31` | 退貨中 |
630
- | `32` | 退貨完成 |
631
-
632
- ### 詳細物流狀態
633
-
634
- #### 7-11 超商
635
-
636
- | 狀態碼 | 說明 |
637
- |--------|------|
638
- | `11` | 已出貨 (寄件門市已收件) |
639
- | `21` | 已到店 (到達取件門市) |
640
- | `22` | 已取貨 (消費者已取件) |
641
- | `31` | 退貨中 (超過取貨期限) |
642
- | `32` | 退貨完成 |
643
-
644
- #### 黑貓宅配
645
-
646
- | 狀態碼 | 說明 |
647
- |--------|------|
648
- | `11` | 已出貨 (黑貓已收件) |
649
- | `21` | 配達中 |
650
- | `22` | 配達完成 |
651
- | `31` | 退貨中 (配達失敗) |
652
- | `32` | 退貨完成 |
653
-
654
- ### 常見錯誤訊息
655
-
656
- | 錯誤訊息 | 說明 | 處理方式 |
657
- |----------|------|----------|
658
- | `參數錯誤` | 必填參數缺失或格式錯誤 | 檢查參數格式 |
659
- | `商店代號錯誤` | MerID 不存在 | 確認商店代號 |
660
- | `門市代號錯誤` | StoreID 無效 | 重新查詢門市代號 |
661
- | `物流類型錯誤` | LogisticsType 無效 | 確認物流類型代碼 |
662
- | `HashInfo 驗證失敗` | 加密資料不正確 | 重新計算 HashInfo |
663
- | `超過尺寸限制` | 材積/重量超過限制 | 調整商品包裝 |
664
-
665
- ---
666
-
667
- ## 常見問題排解
668
-
669
- ### 門市代號無效
670
-
671
- **問題**: 收到 `門市代號錯誤`
672
-
673
- **解決**:
674
- 1. 至 7-11 電子地圖重新查詢門市代號
675
- 2. 確認門市是否仍在營運
676
- 3. 確認門市是否支援店到店服務
677
-
678
- ### 物流狀態通知未收到
679
-
680
- **問題**: 物流狀態變更但沒收到通知
681
-
682
- **檢查項目**:
683
- 1. NotifyURL 是否為 HTTPS
684
- 2. 伺服器是否能被外網存取
685
- 3. 是否正確回應 `SUCCESS`
686
- 4. 防火牆是否阻擋 PayUni IP
687
-
688
- ### 黑貓取貨時間
689
-
690
- **問題**: 如何安排黑貓取貨時間
691
-
692
- **說明**:
693
- 1. 使用 `ScheduledPickupDate` 指定取貨日期
694
- 2. 黑貓會在指定日期至寄件地址取貨
695
- 3. 取貨時段通常為 9:00-18:00
696
-
697
- ### 超商取貨期限
698
-
699
- **問題**: 超商取貨期限是多久
700
-
701
- **說明**:
702
- - 7-11: 7 天
703
- - 超過期限未取件會自動退貨
704
-
705
- ---
706
-
707
- ## 官方資源
708
-
709
- - **官方網站**: https://www.payuni.com.tw/
710
- - **物流服務**: https://www.payuni.com.tw/shipping
711
- - **API 文件**: https://www.payuni.com.tw/docs/web/
712
- - **GitHub**: https://github.com/payuni
1
+ # PayUni Logistics API Reference
2
+
3
+ 統一金流 (PAYUNi) 物流 API 完整參考文件。
4
+
5
+ ---
6
+
7
+ ## 目錄
8
+
9
+ 1. [API 端點總覽](#api-端點總覽)
10
+ 2. [測試環境](#測試環境)
11
+ 3. [加密機制](#加密機制)
12
+ 4. [物流類型](#物流類型)
13
+ 5. [7-11 超商取貨](#7-11-超商取貨)
14
+ 6. [黑貓宅配](#黑貓宅配)
15
+ 7. [物流狀態通知](#物流狀態通知)
16
+ 8. [物流狀態查詢](#物流狀態查詢)
17
+ 9. [錯誤碼對照表](#錯誤碼對照表)
18
+ 10. [常見問題排解](#常見問題排解)
19
+
20
+ ---
21
+
22
+ ## API 端點總覽
23
+
24
+ ### 基礎 API 路徑
25
+
26
+ | 環境 | 基礎路徑 |
27
+ |------|----------|
28
+ | **測試環境** | `https://sandbox-api.payuni.com.tw/api/` |
29
+ | **正式環境** | `https://api.payuni.com.tw/api/` |
30
+
31
+ ### 物流相關端點
32
+
33
+ | 功能 | 端點路徑 | 說明 |
34
+ |------|----------|------|
35
+ | 建立物流訂單 | `/logistics/create` | 建立物流託運單 |
36
+ | 物流狀態查詢 | `/logistics/query` | 查詢物流狀態 |
37
+ | 取消物流訂單 | `/logistics/cancel` | 取消物流訂單 |
38
+
39
+ ---
40
+
41
+ ## 測試環境
42
+
43
+ ### 測試帳號
44
+
45
+ 測試帳號請至 PayUni 後台申請:
46
+
47
+ ```
48
+ 後台網址: https://www.payuni.com.tw/
49
+ 路徑: 會員 > 商店清單 > 指定商店名稱 > 串接設定
50
+ ```
51
+
52
+ 取得以下資訊:
53
+ - **商店代號 (MerID)**
54
+ - **Hash Key**
55
+ - **Hash IV**
56
+
57
+ ### 測試環境端點
58
+
59
+ ```
60
+ https://sandbox-api.payuni.com.tw/api/{endpoint}
61
+ ```
62
+
63
+ ### 注意事項
64
+
65
+ 1. 物流幕後 API 需向 PayUni 申請開通
66
+ 2. 建議使用固定 IP 主機,避免 IP 變動造成功能失效
67
+ 3. 非即時付款 (超商代碼、虛擬帳號) 需等付款完成才會建立物流單
68
+
69
+ ---
70
+
71
+ ## 加密機制
72
+
73
+ PayUni 採用 **AES-256-GCM** 加密與 **SHA256 HMAC** 驗證。
74
+
75
+ ### 加密流程
76
+
77
+ 1. **準備參數** - 組合所有請求參數
78
+ 2. **URL Encode** - 將參數轉為 Query String
79
+ 3. **AES-256-GCM 加密** - 使用 Hash Key 和 Hash IV 加密
80
+ 4. **產生 HashInfo** - 使用 SHA256 計算驗證碼
81
+ 5. **發送請求** - 將加密資料 POST 至 API
82
+
83
+ ### PHP 加密範例
84
+
85
+ ```php
86
+ <?php
87
+
88
+ class PayuniEncryption
89
+ {
90
+ private string $merKey;
91
+ private string $merIV;
92
+
93
+ public function __construct(string $merKey, string $merIV)
94
+ {
95
+ $this->merKey = $merKey;
96
+ $this->merIV = $merIV;
97
+ }
98
+
99
+ /**
100
+ * AES-256-GCM 加密
101
+ */
102
+ public function encrypt(array $params): string
103
+ {
104
+ // 1. 組合 Query String
105
+ $queryString = http_build_query($params);
106
+
107
+ // 2. AES-256-GCM 加密
108
+ $tag = '';
109
+ $encrypted = openssl_encrypt(
110
+ $queryString,
111
+ 'aes-256-gcm',
112
+ $this->merKey,
113
+ OPENSSL_RAW_DATA,
114
+ $this->merIV,
115
+ $tag,
116
+ '',
117
+ 16
118
+ );
119
+
120
+ // 3. 組合加密結果 (加密資料 + tag)
121
+ $encryptInfo = bin2hex($encrypted . $tag);
122
+
123
+ return $encryptInfo;
124
+ }
125
+
126
+ /**
127
+ * AES-256-GCM 解密
128
+ */
129
+ public function decrypt(string $encryptInfo): array
130
+ {
131
+ // 1. Hex 轉 Binary
132
+ $data = hex2bin($encryptInfo);
133
+
134
+ // 2. 分離加密資料和 tag
135
+ $encrypted = substr($data, 0, -16);
136
+ $tag = substr($data, -16);
137
+
138
+ // 3. AES-256-GCM 解密
139
+ $decrypted = openssl_decrypt(
140
+ $encrypted,
141
+ 'aes-256-gcm',
142
+ $this->merKey,
143
+ OPENSSL_RAW_DATA,
144
+ $this->merIV,
145
+ $tag
146
+ );
147
+
148
+ // 4. 解析 Query String
149
+ parse_str($decrypted, $result);
150
+
151
+ return $result;
152
+ }
153
+
154
+ /**
155
+ * 產生 HashInfo (SHA256)
156
+ */
157
+ public function hashInfo(string $encryptInfo): string
158
+ {
159
+ $raw = $encryptInfo . $this->merKey . $this->merIV;
160
+ return strtoupper(hash('sha256', $raw));
161
+ }
162
+ }
163
+ ```
164
+
165
+ ### Python 加密範例
166
+
167
+ ```python
168
+ """PayUni AES-256-GCM 加密"""
169
+
170
+ from Crypto.Cipher import AES
171
+ from urllib.parse import urlencode, parse_qs
172
+ import hashlib
173
+
174
+
175
+ class PayuniEncryption:
176
+ def __init__(self, mer_key: str, mer_iv: str):
177
+ self.mer_key = mer_key.encode('utf-8')
178
+ self.mer_iv = mer_iv.encode('utf-8')
179
+
180
+ def encrypt(self, params: dict) -> str:
181
+ """AES-256-GCM 加密"""
182
+ # 1. 組合 Query String
183
+ query_string = urlencode(params)
184
+
185
+ # 2. AES-256-GCM 加密
186
+ cipher = AES.new(self.mer_key, AES.MODE_GCM, nonce=self.mer_iv)
187
+ encrypted, tag = cipher.encrypt_and_digest(query_string.encode('utf-8'))
188
+
189
+ # 3. 組合加密結果
190
+ encrypt_info = (encrypted + tag).hex()
191
+
192
+ return encrypt_info
193
+
194
+ def decrypt(self, encrypt_info: str) -> dict:
195
+ """AES-256-GCM 解密"""
196
+ # 1. Hex 轉 Binary
197
+ data = bytes.fromhex(encrypt_info)
198
+
199
+ # 2. 分離加密資料和 tag
200
+ encrypted = data[:-16]
201
+ tag = data[-16:]
202
+
203
+ # 3. AES-256-GCM 解密
204
+ cipher = AES.new(self.mer_key, AES.MODE_GCM, nonce=self.mer_iv)
205
+ decrypted = cipher.decrypt_and_verify(encrypted, tag)
206
+
207
+ # 4. 解析 Query String
208
+ result = dict(parse_qs(decrypted.decode('utf-8')))
209
+ return {k: v[0] for k, v in result.items()}
210
+
211
+ def hash_info(self, encrypt_info: str) -> str:
212
+ """產生 HashInfo (SHA256)"""
213
+ raw = encrypt_info + self.mer_key.decode() + self.mer_iv.decode()
214
+ return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
215
+ ```
216
+
217
+ ---
218
+
219
+ ## 物流類型
220
+
221
+ ### 支援的物流服務
222
+
223
+ | 物流類型 | 代碼 | 溫層 | 說明 |
224
+ |----------|------|------|------|
225
+ | 7-11 店到店 (C2C) | `PAYUNi_Logistic_711` | 常溫 | 消費者自行寄件 |
226
+ | 7-11 店到店冷凍 | `PAYUNi_Logistic_711_Freeze` | 冷凍 | 冷凍店到店 |
227
+ | 7-11 大宗寄倉 (B2C) | `PAYUNi_Logistic_711_B2C` | 常溫 | 商家寄倉 |
228
+ | 黑貓宅配常溫 | `PAYUNi_Logistic_Tcat` | 常溫 | 宅配到府 |
229
+ | 黑貓宅配冷凍 | `PAYUNi_Logistic_Tcat_Freeze` | 冷凍 | 冷凍宅配 |
230
+ | 黑貓宅配冷藏 | `PAYUNi_Logistic_Tcat_Cold` | 冷藏 | 冷藏宅配 |
231
+
232
+ ### GoodsType 溫層代碼
233
+
234
+ | 代碼 | 溫層 | 適用物流 |
235
+ |------|------|----------|
236
+ | `1` | 常溫 | 全部 |
237
+ | `2` | 冷凍 | 7-11 冷凍、黑貓冷凍 |
238
+ | `3` | 冷藏 | 僅黑貓宅配 |
239
+
240
+ ### 撥款時間
241
+
242
+ | 物流類型 | 撥款時間 |
243
+ |----------|----------|
244
+ | 超商取貨 | 取貨日 + 7 天 |
245
+ | 黑貓宅配 | 取貨日 + 15 天 |
246
+
247
+ ---
248
+
249
+ ## 7-11 超商取貨
250
+
251
+ ### 建立物流訂單
252
+
253
+ #### 端點
254
+
255
+ ```
256
+ POST /api/logistics/create
257
+ ```
258
+
259
+ #### EncryptInfo 參數
260
+
261
+ | 參數 | 類型 | 長度 | 必填 | 說明 |
262
+ |------|------|------|------|------|
263
+ | `MerID` | String | 20 | | 商店代號 |
264
+ | `MerTradeNo` | String | 50 | | 商店訂單編號 |
265
+ | `LogisticsType` | String | 50 | | 物流類型代碼 |
266
+ | `GoodsType` | Integer | - | | 溫層 `1`:常溫 `2`:冷凍 |
267
+ | `GoodsAmount` | Integer | - | | 商品金額 |
268
+ | `GoodsName` | String | 50 | | 商品名稱 |
269
+ | `SenderName` | String | 10 | | 寄件人姓名 |
270
+ | `SenderPhone` | String | 20 | | 寄件人電話 |
271
+ | `SenderStoreID` | String | 10 | 否 | 寄件門市代號 (C2C 必填) |
272
+ | `ReceiverName` | String | 10 | | 收件人姓名 |
273
+ | `ReceiverPhone` | String | 20 | | 收件人電話 |
274
+ | `ReceiverStoreID` | String | 10 | | 收件門市代號 |
275
+ | `NotifyURL` | String | 500 | | 物流狀態通知網址 |
276
+ | `Timestamp` | Integer | - | | Unix 時間戳 |
277
+
278
+ ### C2C vs B2C 差異
279
+
280
+ | 項目 | C2C 店到店 | B2C 大宗寄倉 |
281
+ |------|-----------|-------------|
282
+ | 寄件方式 | 消費者自行至門市寄件 | 商家統一寄倉 |
283
+ | SenderStoreID | 必填 | 不需要 |
284
+ | 適用場景 | 個人賣家 | 企業電商 |
285
+
286
+ ### 門市查詢
287
+
288
+ 7-11 門市代號可透過以下方式取得:
289
+
290
+ ```
291
+ 7-11 電子地圖: https://emap.pcsc.com.tw/
292
+ ```
293
+
294
+ ### PHP 範例
295
+
296
+ ```php
297
+ <?php
298
+
299
+ $encryption = new PayuniEncryption($merKey, $merIV);
300
+
301
+ $params = [
302
+ 'MerID' => 'YOUR_MER_ID',
303
+ 'MerTradeNo' => 'LOG' . time(),
304
+ 'LogisticsType' => 'PAYUNi_Logistic_711',
305
+ 'GoodsType' => 1,
306
+ 'GoodsAmount' => 500,
307
+ 'GoodsName' => '測試商品',
308
+ 'SenderName' => '寄件人',
309
+ 'SenderPhone' => '0912345678',
310
+ 'SenderStoreID' => '123456', // C2C 必填
311
+ 'ReceiverName' => '收件人',
312
+ 'ReceiverPhone' => '0987654321',
313
+ 'ReceiverStoreID' => '654321',
314
+ 'NotifyURL' => 'https://your-site.com/payuni_shipping_711_notify',
315
+ 'Timestamp' => time(),
316
+ ];
317
+
318
+ $encryptInfo = $encryption->encrypt($params);
319
+ $hashInfo = $encryption->hashInfo($encryptInfo);
320
+
321
+ $ch = curl_init();
322
+ curl_setopt_array($ch, [
323
+ CURLOPT_URL => 'https://api.payuni.com.tw/api/logistics/create',
324
+ CURLOPT_POST => true,
325
+ CURLOPT_POSTFIELDS => http_build_query([
326
+ 'MerID' => $params['MerID'],
327
+ 'Version' => '1.0',
328
+ 'EncryptInfo' => $encryptInfo,
329
+ 'HashInfo' => $hashInfo,
330
+ ]),
331
+ CURLOPT_RETURNTRANSFER => true,
332
+ ]);
333
+
334
+ $response = curl_exec($ch);
335
+ curl_close($ch);
336
+
337
+ $result = json_decode($response, true);
338
+
339
+ if ($result['Status'] === 'SUCCESS') {
340
+ $data = $encryption->decrypt($result['EncryptInfo']);
341
+ // $data['LogisticsID'] - 物流編號
342
+ // $data['CVSPaymentNo'] - 超商繳費代碼
343
+ print_r($data);
344
+ }
345
+ ```
346
+
347
+ ### 回應參數 (解密後)
348
+
349
+ | 參數 | 說明 |
350
+ |------|------|
351
+ | `LogisticsID` | PayUni 物流編號 |
352
+ | `MerTradeNo` | 商店訂單編號 |
353
+ | `CVSPaymentNo` | 超商繳費代碼 |
354
+ | `CVSValidationNo` | 超商驗證碼 |
355
+ | `ExpireDate` | 取貨期限 |
356
+
357
+ ---
358
+
359
+ ## 黑貓宅配
360
+
361
+ ### 建立物流訂單
362
+
363
+ #### 端點
364
+
365
+ ```
366
+ POST /api/logistics/create
367
+ ```
368
+
369
+ #### EncryptInfo 參數
370
+
371
+ | 參數 | 類型 | 長度 | 必填 | 說明 |
372
+ |------|------|------|------|------|
373
+ | `MerID` | String | 20 | | 商店代號 |
374
+ | `MerTradeNo` | String | 50 | | 商店訂單編號 |
375
+ | `LogisticsType` | String | 50 | | 物流類型代碼 |
376
+ | `GoodsType` | Integer | - | | 溫層 `1`:常溫 `2`:冷凍 `3`:冷藏 |
377
+ | `GoodsAmount` | Integer | - | | 商品金額 |
378
+ | `GoodsName` | String | 50 | | 商品名稱 |
379
+ | `GoodsWeight` | Integer | - | 否 | 商品重量 (g) |
380
+ | `SenderName` | String | 10 | | 寄件人姓名 |
381
+ | `SenderPhone` | String | 20 | | 寄件人電話 |
382
+ | `SenderZipCode` | String | 5 | | 寄件人郵遞區號 |
383
+ | `SenderAddress` | String | 200 | | 寄件人地址 |
384
+ | `ReceiverName` | String | 10 | | 收件人姓名 |
385
+ | `ReceiverPhone` | String | 20 | | 收件人電話 |
386
+ | `ReceiverZipCode` | String | 5 | | 收件人郵遞區號 |
387
+ | `ReceiverAddress` | String | 200 | | 收件人地址 |
388
+ | `ScheduledPickupDate` | String | 10 | 否 | 預定取貨日期 `yyyy/MM/dd` |
389
+ | `ScheduledDeliveryDate` | String | 10 | 否 | 預定配達日期 `yyyy/MM/dd` |
390
+ | `ScheduledDeliveryTime` | String | 2 | 否 | 預定配達時段 |
391
+ | `NotifyURL` | String | 500 | | 物流狀態通知網址 |
392
+ | `Timestamp` | Integer | - | | Unix 時間戳 |
393
+
394
+ ### ScheduledDeliveryTime 配達時段
395
+
396
+ | 代碼 | 時段 |
397
+ |------|------|
398
+ | `01` | 13:00 前 |
399
+ | `02` | 14:00 - 18:00 |
400
+ | `03` | 不指定 |
401
+
402
+ ### 尺寸與重量限制
403
+
404
+ | 溫層 | 材積 | 重量 |
405
+ |------|------|------|
406
+ | 常溫 | 150cm | 20kg |
407
+ | 冷凍/冷藏 | 120cm | 15kg |
408
+
409
+ **材積計算**: 長 + 寬 + 高 ≤ 限制
410
+
411
+ ### PHP 範例
412
+
413
+ ```php
414
+ <?php
415
+
416
+ $encryption = new PayuniEncryption($merKey, $merIV);
417
+
418
+ $params = [
419
+ 'MerID' => 'YOUR_MER_ID',
420
+ 'MerTradeNo' => 'LOG' . time(),
421
+ 'LogisticsType' => 'PAYUNi_Logistic_Tcat',
422
+ 'GoodsType' => 1, // 常溫
423
+ 'GoodsAmount' => 1000,
424
+ 'GoodsName' => '測試商品',
425
+ 'GoodsWeight' => 500, // 500g
426
+ 'SenderName' => '寄件人',
427
+ 'SenderPhone' => '0912345678',
428
+ 'SenderZipCode' => '100',
429
+ 'SenderAddress' => '台北市中正區某某路1號',
430
+ 'ReceiverName' => '收件人',
431
+ 'ReceiverPhone' => '0987654321',
432
+ 'ReceiverZipCode' => '300',
433
+ 'ReceiverAddress' => '新竹市東區某某路2號',
434
+ 'ScheduledDeliveryTime' => '02', // 14:00-18:00
435
+ 'NotifyURL' => 'https://your-site.com/payuni_shipping_tcat_notify',
436
+ 'Timestamp' => time(),
437
+ ];
438
+
439
+ $encryptInfo = $encryption->encrypt($params);
440
+ $hashInfo = $encryption->hashInfo($encryptInfo);
441
+
442
+ $ch = curl_init();
443
+ curl_setopt_array($ch, [
444
+ CURLOPT_URL => 'https://api.payuni.com.tw/api/logistics/create',
445
+ CURLOPT_POST => true,
446
+ CURLOPT_POSTFIELDS => http_build_query([
447
+ 'MerID' => $params['MerID'],
448
+ 'Version' => '1.0',
449
+ 'EncryptInfo' => $encryptInfo,
450
+ 'HashInfo' => $hashInfo,
451
+ ]),
452
+ CURLOPT_RETURNTRANSFER => true,
453
+ ]);
454
+
455
+ $response = curl_exec($ch);
456
+ curl_close($ch);
457
+
458
+ $result = json_decode($response, true);
459
+
460
+ if ($result['Status'] === 'SUCCESS') {
461
+ $data = $encryption->decrypt($result['EncryptInfo']);
462
+ // $data['LogisticsID'] - 物流編號
463
+ // $data['ShipmentNo'] - 託運單號
464
+ print_r($data);
465
+ }
466
+ ```
467
+
468
+ ### 回應參數 (解密後)
469
+
470
+ | 參數 | 說明 |
471
+ |------|------|
472
+ | `LogisticsID` | PayUni 物流編號 |
473
+ | `MerTradeNo` | 商店訂單編號 |
474
+ | `ShipmentNo` | 黑貓託運單號 |
475
+ | `BookingNote` | 取貨編號 |
476
+
477
+ ---
478
+
479
+ ## 物流狀態通知
480
+
481
+ ### Notify URL 設定
482
+
483
+ | 物流類型 | Notify URL 格式建議 |
484
+ |----------|---------------------|
485
+ | 超商物流 | `https://your-site.com/payuni_shipping_711_notify` |
486
+ | 黑貓宅配 | `https://your-site.com/payuni_shipping_tcat_notify` |
487
+
488
+ ### 通知流程
489
+
490
+ ```
491
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
492
+ │ 物流商 │────▶│ PayUni │────▶│ 商店 │
493
+ │ 狀態更新 │ │ 處理 │ │ NotifyURL │
494
+ └─────────────┘ └─────────────┘ └─────────────┘
495
+
496
+ │ POST (加密資料)
497
+
498
+ ┌─────────────┐
499
+ │ 商店 │
500
+ │ 解密處理 │
501
+ └─────────────┘
502
+
503
+ │ 回應 "SUCCESS"
504
+
505
+ ┌─────────────┐
506
+ │ PayUni │
507
+ │ 確認收到 │
508
+ └─────────────┘
509
+ ```
510
+
511
+ ### 通知參數
512
+
513
+ PayUni 會 POST 加密資料到 `NotifyURL`:
514
+
515
+ | 參數 | 說明 |
516
+ |------|------|
517
+ | `MerID` | 商店代號 |
518
+ | `EncryptInfo` | 加密的物流狀態 |
519
+ | `HashInfo` | SHA256 驗證碼 |
520
+
521
+ ### 解密後的通知內容
522
+
523
+ | 參數 | 說明 |
524
+ |------|------|
525
+ | `MerID` | 商店代號 |
526
+ | `MerTradeNo` | 商店訂單編號 |
527
+ | `LogisticsID` | PayUni 物流編號 |
528
+ | `LogisticsType` | 物流類型 |
529
+ | `LogisticsStatus` | 物流狀態碼 |
530
+ | `LogisticsStatusMsg` | 物流狀態訊息 |
531
+ | `UpdateTime` | 狀態更新時間 |
532
+
533
+ ### 處理範例
534
+
535
+ ```php
536
+ <?php
537
+
538
+ // 接收通知
539
+ $encryptInfo = $_POST['EncryptInfo'] ?? '';
540
+ $hashInfo = $_POST['HashInfo'] ?? '';
541
+ $merID = $_POST['MerID'] ?? '';
542
+
543
+ // 驗證 HashInfo
544
+ $encryption = new PayuniEncryption($merKey, $merIV);
545
+ $calculatedHash = $encryption->hashInfo($encryptInfo);
546
+
547
+ if ($hashInfo !== $calculatedHash) {
548
+ echo 'HashInfo Error';
549
+ exit;
550
+ }
551
+
552
+ // 解密
553
+ $data = $encryption->decrypt($encryptInfo);
554
+
555
+ // 根據物流狀態更新訂單
556
+ switch ($data['LogisticsStatus']) {
557
+ case '11':
558
+ // 已出貨
559
+ updateOrderLogisticsStatus($data['MerTradeNo'], 'shipped');
560
+ break;
561
+ case '21':
562
+ // 已到店
563
+ updateOrderLogisticsStatus($data['MerTradeNo'], 'arrived');
564
+ break;
565
+ case '22':
566
+ // 已取貨
567
+ updateOrderLogisticsStatus($data['MerTradeNo'], 'picked_up');
568
+ break;
569
+ case '31':
570
+ case '32':
571
+ // 退貨
572
+ updateOrderLogisticsStatus($data['MerTradeNo'], 'returned');
573
+ break;
574
+ }
575
+
576
+ // 回應 SUCCESS
577
+ echo 'SUCCESS';
578
+ ```
579
+
580
+ ---
581
+
582
+ ## 物流狀態查詢
583
+
584
+ ### 端點
585
+
586
+ ```
587
+ POST /api/logistics/query
588
+ ```
589
+
590
+ ### EncryptInfo 參數
591
+
592
+ | 參數 | 類型 | 必填 | 說明 |
593
+ |------|------|------|------|
594
+ | `MerID` | String | | 商店代號 |
595
+ | `MerTradeNo` | String | | 商店訂單編號 |
596
+ | `Timestamp` | Integer | | Unix 時間戳 |
597
+
598
+ ### 回應參數 (解密後)
599
+
600
+ | 參數 | 說明 |
601
+ |------|------|
602
+ | `LogisticsID` | PayUni 物流編號 |
603
+ | `MerTradeNo` | 商店訂單編號 |
604
+ | `LogisticsType` | 物流類型 |
605
+ | `LogisticsStatus` | 物流狀態碼 |
606
+ | `LogisticsStatusMsg` | 物流狀態訊息 |
607
+ | `ShipmentNo` | 託運單號 |
608
+ | `ReceiverStoreID` | 收件門市代號 (超商) |
609
+ | `UpdateTime` | 狀態更新時間 |
610
+
611
+ ### 貨態即時查詢
612
+
613
+ | 物流類型 | 查詢網址 |
614
+ |----------|----------|
615
+ | 7-11 | https://eservice.7-11.com.tw/E-Tracking/search.aspx |
616
+ | 黑貓宅配 | https://www.t-cat.com.tw/inquire/trace.aspx |
617
+
618
+ ---
619
+
620
+ ## 錯誤碼對照表
621
+
622
+ ### 物流狀態碼 (LogisticsStatus)
623
+
624
+ | 狀態碼 | 說明 |
625
+ |--------|------|
626
+ | `11` | 已出貨 |
627
+ | `21` | 已到店 (超商) / 配達中 (宅配) |
628
+ | `22` | 已取貨 / 配達完成 |
629
+ | `31` | 退貨中 |
630
+ | `32` | 退貨完成 |
631
+
632
+ ### 詳細物流狀態
633
+
634
+ #### 7-11 超商
635
+
636
+ | 狀態碼 | 說明 |
637
+ |--------|------|
638
+ | `11` | 已出貨 (寄件門市已收件) |
639
+ | `21` | 已到店 (到達取件門市) |
640
+ | `22` | 已取貨 (消費者已取件) |
641
+ | `31` | 退貨中 (超過取貨期限) |
642
+ | `32` | 退貨完成 |
643
+
644
+ #### 黑貓宅配
645
+
646
+ | 狀態碼 | 說明 |
647
+ |--------|------|
648
+ | `11` | 已出貨 (黑貓已收件) |
649
+ | `21` | 配達中 |
650
+ | `22` | 配達完成 |
651
+ | `31` | 退貨中 (配達失敗) |
652
+ | `32` | 退貨完成 |
653
+
654
+ ### 常見錯誤訊息
655
+
656
+ | 錯誤訊息 | 說明 | 處理方式 |
657
+ |----------|------|----------|
658
+ | `參數錯誤` | 必填參數缺失或格式錯誤 | 檢查參數格式 |
659
+ | `商店代號錯誤` | MerID 不存在 | 確認商店代號 |
660
+ | `門市代號錯誤` | StoreID 無效 | 重新查詢門市代號 |
661
+ | `物流類型錯誤` | LogisticsType 無效 | 確認物流類型代碼 |
662
+ | `HashInfo 驗證失敗` | 加密資料不正確 | 重新計算 HashInfo |
663
+ | `超過尺寸限制` | 材積/重量超過限制 | 調整商品包裝 |
664
+
665
+ ---
666
+
667
+ ## 常見問題排解
668
+
669
+ ### 門市代號無效
670
+
671
+ **問題**: 收到 `門市代號錯誤`
672
+
673
+ **解決**:
674
+ 1. 至 7-11 電子地圖重新查詢門市代號
675
+ 2. 確認門市是否仍在營運
676
+ 3. 確認門市是否支援店到店服務
677
+
678
+ ### 物流狀態通知未收到
679
+
680
+ **問題**: 物流狀態變更但沒收到通知
681
+
682
+ **檢查項目**:
683
+ 1. NotifyURL 是否為 HTTPS
684
+ 2. 伺服器是否能被外網存取
685
+ 3. 是否正確回應 `SUCCESS`
686
+ 4. 防火牆是否阻擋 PayUni IP
687
+
688
+ ### 黑貓取貨時間
689
+
690
+ **問題**: 如何安排黑貓取貨時間
691
+
692
+ **說明**:
693
+ 1. 使用 `ScheduledPickupDate` 指定取貨日期
694
+ 2. 黑貓會在指定日期至寄件地址取貨
695
+ 3. 取貨時段通常為 9:00-18:00
696
+
697
+ ### 超商取貨期限
698
+
699
+ **問題**: 超商取貨期限是多久
700
+
701
+ **說明**:
702
+ - 7-11: 7 天
703
+ - 超過期限未取件會自動退貨
704
+
705
+ ---
706
+
707
+ ## 官方資源
708
+
709
+ - **官方網站**: https://www.payuni.com.tw/
710
+ - **物流服務**: https://www.payuni.com.tw/shipping
711
+ - **API 文件**: https://www.payuni.com.tw/docs/web/
712
+ - **GitHub**: https://github.com/payuni