taiwan-logistics-skill 1.0.0
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.
- package/README.md +186 -0
- package/assets/taiwan-logistics/EXAMPLES.md +3183 -0
- package/assets/taiwan-logistics/README.md +33 -0
- package/assets/taiwan-logistics/SKILL.md +827 -0
- package/assets/taiwan-logistics/data/field-mappings.csv +27 -0
- package/assets/taiwan-logistics/data/logistics-types.csv +11 -0
- package/assets/taiwan-logistics/data/operations.csv +8 -0
- package/assets/taiwan-logistics/data/providers.csv +9 -0
- package/assets/taiwan-logistics/data/status-codes.csv +14 -0
- package/assets/taiwan-logistics/examples/newebpay-logistics-cvs-example.py +524 -0
- package/assets/taiwan-logistics/examples/payuni-logistics-cvs-example.py +605 -0
- package/assets/taiwan-logistics/references/NEWEBPAY_LOGISTICS_REFERENCE.md +774 -0
- package/assets/taiwan-logistics/references/ecpay-logistics-api.md +536 -0
- package/assets/taiwan-logistics/references/payuni-logistics-api.md +712 -0
- package/assets/taiwan-logistics/scripts/core.py +276 -0
- package/assets/taiwan-logistics/scripts/search.py +127 -0
- package/assets/taiwan-logistics/scripts/test_logistics.py +236 -0
- package/dist/index.js +16377 -0
- package/package.json +58 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
field_name,field_zh,ecpay_name,type,required,max_length,format,notes
|
|
2
|
+
merchant_id,商店代號,MerchantID,string,true,10,,
|
|
3
|
+
order_id,訂單編號,MerchantTradeNo,string,true,20,英數字,需唯一
|
|
4
|
+
order_date,訂單日期,MerchantTradeDate,string,true,20,yyyy/MM/dd HH:mm:ss,
|
|
5
|
+
logistics_type,物流類型,LogisticsType,string,true,20,CVS/Home,
|
|
6
|
+
logistics_sub_type,物流子類型,LogisticsSubType,string,true,20,,見代碼表
|
|
7
|
+
ecpay_logistics_id,綠界物流編號,AllPayLogisticsID,string,false,20,,ECPay回傳
|
|
8
|
+
goods_amount,商品金額,GoodsAmount,integer,true,,,
|
|
9
|
+
goods_name,商品名稱,GoodsName,string,true,50,,
|
|
10
|
+
sender_name,寄件人姓名,SenderName,string,true,10,,
|
|
11
|
+
sender_phone,寄件人電話,SenderPhone,string,true,20,,
|
|
12
|
+
sender_cell_phone,寄件人手機,SenderCellPhone,string,false,20,,
|
|
13
|
+
sender_zipcode,寄件人郵遞區號,SenderZipCode,string,false,6,,宅配必填
|
|
14
|
+
sender_address,寄件人地址,SenderAddress,string,false,200,,宅配必填
|
|
15
|
+
receiver_name,收件人姓名,ReceiverName,string,true,10,,
|
|
16
|
+
receiver_phone,收件人電話,ReceiverPhone,string,true,20,,
|
|
17
|
+
receiver_cell_phone,收件人手機,ReceiverCellPhone,string,false,20,,
|
|
18
|
+
receiver_zipcode,收件人郵遞區號,ReceiverZipCode,string,false,6,,宅配必填
|
|
19
|
+
receiver_address,收件人地址,ReceiverAddress,string,false,200,,宅配必填
|
|
20
|
+
receiver_store_id,收件門市代號,ReceiverStoreID,string,false,6,,超商必填
|
|
21
|
+
return_store_id,退貨門市代號,ReturnStoreID,string,false,6,,
|
|
22
|
+
is_collection,是否代收,IsCollection,string,false,1,Y/N,超商適用
|
|
23
|
+
collection_amount,代收金額,CollectionAmount,integer,false,,,
|
|
24
|
+
temperature,溫層,Temperature,string,false,4,0001/0002/0003,宅配適用
|
|
25
|
+
distance,距離,Distance,string,false,2,00/01,00同縣市 01外縣市
|
|
26
|
+
specification,規格,Specification,string,false,4,,包裹尺寸
|
|
27
|
+
server_reply_url,通知網址,ServerReplyURL,string,true,200,URL,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
logistics_type,logistics_sub_type,name_zh,provider,type,collection_available,max_amount,size_limit,temperature,notes
|
|
2
|
+
CVS,UNIMART,7-11 B2C,7-11,cvs_b2c,true,20000,45x30x30cm,常溫,大宗寄倉
|
|
3
|
+
CVS,UNIMARTC2C,7-11 店到店,7-11,cvs_c2c,true,20000,45x30x30cm,常溫,C2C 交貨便
|
|
4
|
+
CVS,FAMI,全家 B2C,全家,cvs_b2c,true,20000,45x30x30cm,常溫,大宗寄倉
|
|
5
|
+
CVS,FAMIC2C,全家店到店,全家,cvs_c2c,true,20000,45x30x30cm,常溫,C2C 店到店
|
|
6
|
+
CVS,HILIFE,萊爾富 B2C,萊爾富,cvs_b2c,true,20000,45x30x30cm,常溫,大宗寄倉
|
|
7
|
+
CVS,HILIFEC2C,萊爾富店到店,萊爾富,cvs_c2c,true,20000,45x30x30cm,常溫,C2C 店到店
|
|
8
|
+
CVS,OKMART,OK B2C,OK,cvs_b2c,true,20000,45x30x30cm,常溫,大宗寄倉
|
|
9
|
+
CVS,OKMARTC2C,OK 店到店,OK,cvs_c2c,true,20000,45x30x30cm,常溫,C2C 店到店
|
|
10
|
+
Home,TCAT,黑貓宅急便,黑貓,home,false,200000,150cm,常溫/冷藏/冷凍,宅配到府
|
|
11
|
+
Home,ECAN,宅配通,宅配通,home,false,100000,120cm,常溫,常溫宅配
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
operation,operation_zh,ecpay_endpoint,method,required_fields,optional_fields,notes
|
|
2
|
+
create_cvs,建立超商取貨訂單,/Express/Create,POST,"MerchantID,MerchantTradeNo,LogisticsType,LogisticsSubType,GoodsAmount,GoodsName,SenderName,SenderPhone,ReceiverName,ReceiverPhone,ReceiverStoreID,ServerReplyURL","IsCollection,CollectionAmount,ReturnStoreID",LogisticsType=CVS
|
|
3
|
+
create_home,建立宅配訂單,/Express/Create,POST,"MerchantID,MerchantTradeNo,LogisticsType,LogisticsSubType,GoodsAmount,GoodsName,SenderName,SenderPhone,SenderAddress,ReceiverName,ReceiverPhone,ReceiverAddress,ServerReplyURL","Temperature,Distance,Specification,ScheduledDeliveryTime",LogisticsType=Home
|
|
4
|
+
map,超商電子地圖,/Express/map,POST,"MerchantID,LogisticsType,LogisticsSubType,ServerReplyURL","IsCollection,ExtraData",開啟門市選擇頁面
|
|
5
|
+
query,查詢物流訂單,/Helper/QueryLogisticsTradeInfo/V2,POST,"MerchantID,TimeStamp","AllPayLogisticsID,MerchantTradeNo",二擇一
|
|
6
|
+
print,列印託運單,/helper/printTradeDocument,POST,"MerchantID,AllPayLogisticsID","LogisticsType,LogisticsSubType",需先建立訂單
|
|
7
|
+
return,逆物流訂單,/Express/ReturnHome,POST,"MerchantID,AllPayLogisticsID,ServerReplyURL","GoodsAmount,SenderName",退貨/換貨
|
|
8
|
+
update_store,更新門市,/Express/UpdateStoreInfo,POST,"MerchantID,AllPayLogisticsID,ReceiverStoreID","CVSPaymentNo,CVSValidationNo",僅限超商取貨
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
provider,name_zh,name_en,type,api_available,test_merchant_id,test_hash_key,test_hash_iv,test_url,prod_url,features,coverage
|
|
2
|
+
ecpay,綠界物流,ECPay Logistics,aggregator,true,2000132,5294y06JbISpM5x9,v77hoKGq4kWxNNIS,https://logistics-stage.ecpay.com.tw,https://logistics.ecpay.com.tw,"7-11,全家,萊爾富,OK,黑貓,宅配通",全台
|
|
3
|
+
unimart,7-11 交貨便,7-11 ibon,cvs,true,,,,,https://emap.pcsc.com.tw,店到店,全台 5000+ 門市
|
|
4
|
+
fami,全家店到店,FamilyMart,cvs,true,,,,,https://www.famiport.com.tw,店到店,全台 3500+ 門市
|
|
5
|
+
hilife,萊爾富,Hi-Life,cvs,true,,,,,https://www.hilife.com.tw,店到店,全台 1500+ 門市
|
|
6
|
+
okmart,OK 便利商店,OK Mart,cvs,true,,,,,,店到店,全台 900+ 門市
|
|
7
|
+
tcat,黑貓宅急便,t-cat,home,true,,,,https://www.t-cat.com.tw,https://www.t-cat.com.tw,"常溫,冷藏,冷凍",全台
|
|
8
|
+
hct,新竹物流,HCT,home,true,,,,https://www.hct.com.tw,https://www.hct.com.tw,常溫,全台
|
|
9
|
+
pelican,宅配通,Pelican Express,home,true,,,,https://www.pelican.com.tw,https://www.pelican.com.tw,常溫,全台
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
code,status_zh,status_en,category,description,next_action
|
|
2
|
+
300,訂單建立成功,Order Created,success,物流訂單已成功建立,等待出貨
|
|
3
|
+
310,訂單取消,Order Cancelled,cancelled,訂單已取消,
|
|
4
|
+
2030,已交寄,Package Shipped,in_transit,商品已交給物流商,等待配送
|
|
5
|
+
2063,配達完成,Delivered,delivered,宅配已送達收件人,
|
|
6
|
+
2067,消費者取貨完成,Pickup Completed,completed,消費者已至超商取貨,
|
|
7
|
+
2073,退貨中,Return in Progress,returning,退貨流程進行中,
|
|
8
|
+
2074,退貨完成,Return Completed,returned,退貨流程已完成,
|
|
9
|
+
2030,包裹已到店,Arrived at Store,at_store,包裹已送達指定門市,通知取貨
|
|
10
|
+
2031,包裹已取件,Picked Up by Carrier,picked_up,物流商已取件,
|
|
11
|
+
2032,轉運中,In Transit,in_transit,包裹配送中,
|
|
12
|
+
2061,配達失敗,Delivery Failed,failed,配送失敗,聯繫收件人
|
|
13
|
+
2070,包裹已退回,Package Returned,returned,包裹已退回寄件人,
|
|
14
|
+
3001,系統錯誤,System Error,error,系統發生錯誤,聯繫客服
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NewebPay 藍新物流 CVS 超商物流 Python 完整範例
|
|
4
|
+
|
|
5
|
+
依照 taiwan-logistics-skill 最高規範撰寫
|
|
6
|
+
支援: C2C 店到店 (7-11, 全家, 萊爾富, OK)、B2C 大宗寄倉
|
|
7
|
+
|
|
8
|
+
API 文件: https://www.newebpay.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import hashlib
|
|
13
|
+
import time
|
|
14
|
+
import urllib.parse
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Dict, Literal, Optional
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from Crypto.Cipher import AES
|
|
21
|
+
from Crypto.Util.Padding import pad, unpad
|
|
22
|
+
import requests
|
|
23
|
+
HAS_DEPENDENCIES = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_DEPENDENCIES = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CVSShipmentData:
|
|
30
|
+
"""CVS 超商物流訂單資料"""
|
|
31
|
+
merchant_order_no: str
|
|
32
|
+
lgs_type: Literal['B2C', 'C2C']
|
|
33
|
+
ship_type: Literal['1', '2', '3', '4'] # 1=7-11, 2=FamilyMart, 3=Hi-Life, 4=OK
|
|
34
|
+
receiver_store_code: str
|
|
35
|
+
receiver_name: str
|
|
36
|
+
receiver_cell_phone: str
|
|
37
|
+
receiver_tel: Optional[str] = None
|
|
38
|
+
goods_amount: int = 0
|
|
39
|
+
goods_name: str = ''
|
|
40
|
+
sender_name: Optional[str] = None
|
|
41
|
+
sender_cell_phone: Optional[str] = None
|
|
42
|
+
is_collection: Literal['Y', 'N'] = 'N'
|
|
43
|
+
collection_amount: int = 0
|
|
44
|
+
note: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CVSShipmentResponse:
|
|
49
|
+
"""CVS 超商物流訂單回應"""
|
|
50
|
+
success: bool
|
|
51
|
+
status: str
|
|
52
|
+
message: str
|
|
53
|
+
merchant_order_no: str
|
|
54
|
+
logistics_no: Optional[str] = None
|
|
55
|
+
cvs_payment_no: Optional[str] = None
|
|
56
|
+
cvs_validation_no: Optional[str] = None
|
|
57
|
+
booking_note: Optional[str] = None
|
|
58
|
+
raw: Dict = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class StoreMapData:
|
|
63
|
+
"""電子地圖門市查詢資料"""
|
|
64
|
+
merchant_order_no: str
|
|
65
|
+
lgs_type: Literal['B2C', 'C2C']
|
|
66
|
+
ship_type: Literal['1', '2', '3', '4']
|
|
67
|
+
return_url: str
|
|
68
|
+
extra_data: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class StoreMapCallback:
|
|
73
|
+
"""電子地圖門市選擇回傳"""
|
|
74
|
+
lgs_type: str
|
|
75
|
+
ship_type: str
|
|
76
|
+
merchant_order_no: str
|
|
77
|
+
store_id: str
|
|
78
|
+
store_name: str
|
|
79
|
+
store_addr: str
|
|
80
|
+
store_tel: str
|
|
81
|
+
extra_data: str
|
|
82
|
+
raw: Dict = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class NewebPayCVSLogistics:
|
|
86
|
+
"""
|
|
87
|
+
NewebPay 藍新物流 CVS 超商物流服務
|
|
88
|
+
|
|
89
|
+
認證方式: AES-256-CBC + SHA256
|
|
90
|
+
加密方式: JSON + AES-256-CBC 加密 + SHA256 驗證
|
|
91
|
+
|
|
92
|
+
支援超商:
|
|
93
|
+
- 1: 7-ELEVEN 統一超商
|
|
94
|
+
- 2: FamilyMart 全家便利商店
|
|
95
|
+
- 3: Hi-Life 萊爾富便利商店
|
|
96
|
+
- 4: OK Mart OK 便利商店
|
|
97
|
+
|
|
98
|
+
物流類型:
|
|
99
|
+
- C2C: 店到店 (店取、店寄)
|
|
100
|
+
- B2C: 大宗寄倉 (僅 7-11)
|
|
101
|
+
|
|
102
|
+
測試環境:
|
|
103
|
+
- 需至藍新金流申請測試帳號
|
|
104
|
+
- API URL: https://ccore.newebpay.com/API/Logistic
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# 測試環境
|
|
108
|
+
TEST_BASE_URL = 'https://ccore.newebpay.com/API/Logistic'
|
|
109
|
+
|
|
110
|
+
# 正式環境
|
|
111
|
+
PROD_BASE_URL = 'https://core.newebpay.com/API/Logistic'
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
merchant_id: str,
|
|
116
|
+
hash_key: str,
|
|
117
|
+
hash_iv: str,
|
|
118
|
+
is_production: bool = False
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
初始化 NewebPay CVS 物流服務
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
merchant_id: 商店代號
|
|
125
|
+
hash_key: HashKey (32 字元)
|
|
126
|
+
hash_iv: HashIV (16 字元)
|
|
127
|
+
is_production: 是否為正式環境 (預設 False)
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ImportError: 缺少必要套件
|
|
131
|
+
"""
|
|
132
|
+
if not HAS_DEPENDENCIES:
|
|
133
|
+
raise ImportError(
|
|
134
|
+
'需要安裝必要套件:\n'
|
|
135
|
+
' pip install pycryptodome requests'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.merchant_id = merchant_id
|
|
139
|
+
self.hash_key = hash_key.encode('utf-8')
|
|
140
|
+
self.hash_iv = hash_iv.encode('utf-8')
|
|
141
|
+
self.base_url = self.PROD_BASE_URL if is_production else self.TEST_BASE_URL
|
|
142
|
+
|
|
143
|
+
def aes_encrypt(self, data: str) -> str:
|
|
144
|
+
"""
|
|
145
|
+
AES-256-CBC 加密
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
data: 明文字串
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: 加密後的 hex 字串
|
|
152
|
+
"""
|
|
153
|
+
cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
|
|
154
|
+
padded_data = pad(data.encode('utf-8'), AES.block_size)
|
|
155
|
+
encrypted = cipher.encrypt(padded_data)
|
|
156
|
+
return encrypted.hex()
|
|
157
|
+
|
|
158
|
+
def aes_decrypt(self, encrypted_data: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
AES-256-CBC 解密
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
encrypted_data: 加密的 hex 字串
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
str: 解密後的明文
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: 解密失敗
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
|
|
173
|
+
decrypted = cipher.decrypt(bytes.fromhex(encrypted_data))
|
|
174
|
+
unpadded = unpad(decrypted, AES.block_size)
|
|
175
|
+
return unpadded.decode('utf-8')
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise ValueError(f'解密失敗: {str(e)}')
|
|
178
|
+
|
|
179
|
+
def generate_hash_data(self, encrypt_data: str) -> str:
|
|
180
|
+
"""
|
|
181
|
+
產生 HashData (SHA256)
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
encrypt_data: 加密後的資料
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
str: SHA256 雜湊值 (大寫)
|
|
188
|
+
"""
|
|
189
|
+
raw = f"{self.hash_key.decode('utf-8')}{encrypt_data}{self.hash_iv.decode('utf-8')}"
|
|
190
|
+
return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
|
|
191
|
+
|
|
192
|
+
def verify_hash_data(self, encrypt_data: str, hash_data: str) -> bool:
|
|
193
|
+
"""驗證 HashData"""
|
|
194
|
+
calculated_hash = self.generate_hash_data(encrypt_data)
|
|
195
|
+
return calculated_hash == hash_data.upper()
|
|
196
|
+
|
|
197
|
+
def query_store_map(
|
|
198
|
+
self,
|
|
199
|
+
data: StoreMapData,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""
|
|
202
|
+
查詢電子地圖 (門市選擇)
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
data: 門市查詢資料 (StoreMapData)
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
str: 重導向網址 (用戶選擇門市)
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> store_data = StoreMapData(
|
|
212
|
+
... merchant_order_no='ORD123',
|
|
213
|
+
... lgs_type='C2C',
|
|
214
|
+
... ship_type='1',
|
|
215
|
+
... return_url='https://your-site.com/callback',
|
|
216
|
+
... )
|
|
217
|
+
>>> redirect_url = service.query_store_map(store_data)
|
|
218
|
+
"""
|
|
219
|
+
# 準備加密資料
|
|
220
|
+
encrypt_data_obj = {
|
|
221
|
+
'MerchantOrderNo': data.merchant_order_no,
|
|
222
|
+
'LgsType': data.lgs_type,
|
|
223
|
+
'ShipType': data.ship_type,
|
|
224
|
+
'ReturnURL': data.return_url,
|
|
225
|
+
'TimeStamp': str(int(time.time())),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if data.extra_data:
|
|
229
|
+
encrypt_data_obj['ExtraData'] = data.extra_data
|
|
230
|
+
|
|
231
|
+
# JSON 序列化並加密
|
|
232
|
+
json_str = json.dumps(encrypt_data_obj, ensure_ascii=False)
|
|
233
|
+
encrypt_data = self.aes_encrypt(json_str)
|
|
234
|
+
hash_data = self.generate_hash_data(encrypt_data)
|
|
235
|
+
|
|
236
|
+
# 準備 API 請求
|
|
237
|
+
api_data = {
|
|
238
|
+
'UID_': self.merchant_id,
|
|
239
|
+
'EncryptData_': encrypt_data,
|
|
240
|
+
'HashData_': hash_data,
|
|
241
|
+
'Version_': '1.0',
|
|
242
|
+
'RespondType_': 'JSON',
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# 發送請求 (NewebPay 會重導向到門市選擇頁面)
|
|
246
|
+
response = requests.post(
|
|
247
|
+
f'{self.base_url}/storeMap',
|
|
248
|
+
data=api_data,
|
|
249
|
+
allow_redirects=False,
|
|
250
|
+
timeout=30,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# 回傳重導向網址
|
|
254
|
+
if response.status_code in [301, 302, 303]:
|
|
255
|
+
return response.headers.get('Location', '')
|
|
256
|
+
|
|
257
|
+
return response.url
|
|
258
|
+
|
|
259
|
+
def parse_store_map_callback(
|
|
260
|
+
self,
|
|
261
|
+
encrypt_data: str,
|
|
262
|
+
hash_data: str,
|
|
263
|
+
) -> StoreMapCallback:
|
|
264
|
+
"""
|
|
265
|
+
解析電子地圖回傳資料
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
encrypt_data: 加密資料
|
|
269
|
+
hash_data: 雜湊驗證值
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
StoreMapCallback: 門市選擇結果
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: 驗證失敗
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> callback = service.parse_store_map_callback(
|
|
279
|
+
... request.form['EncryptData_'],
|
|
280
|
+
... request.form['HashData_']
|
|
281
|
+
... )
|
|
282
|
+
>>> print(f"選擇門市: {callback.store_name}")
|
|
283
|
+
"""
|
|
284
|
+
# 驗證 HashData
|
|
285
|
+
if not self.verify_hash_data(encrypt_data, hash_data):
|
|
286
|
+
raise ValueError('HashData 驗證失敗')
|
|
287
|
+
|
|
288
|
+
# 解密資料
|
|
289
|
+
decrypted_json = self.aes_decrypt(encrypt_data)
|
|
290
|
+
decrypted = json.loads(decrypted_json)
|
|
291
|
+
|
|
292
|
+
return StoreMapCallback(
|
|
293
|
+
lgs_type=decrypted.get('LgsType', ''),
|
|
294
|
+
ship_type=decrypted.get('ShipType', ''),
|
|
295
|
+
merchant_order_no=decrypted.get('MerchantOrderNo', ''),
|
|
296
|
+
store_id=decrypted.get('StoreID', ''),
|
|
297
|
+
store_name=decrypted.get('StoreName', ''),
|
|
298
|
+
store_addr=decrypted.get('StoreAddr', ''),
|
|
299
|
+
store_tel=decrypted.get('StoreTel', ''),
|
|
300
|
+
extra_data=decrypted.get('ExtraData', ''),
|
|
301
|
+
raw=decrypted,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def create_shipment(
|
|
305
|
+
self,
|
|
306
|
+
data: CVSShipmentData,
|
|
307
|
+
) -> CVSShipmentResponse:
|
|
308
|
+
"""
|
|
309
|
+
建立 CVS 超商物流訂單
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
data: CVS 物流訂單資料 (CVSShipmentData)
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
CVSShipmentResponse: 物流訂單回應
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
ValueError: 參數驗證失敗
|
|
319
|
+
Exception: API 請求失敗
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> shipment_data = CVSShipmentData(
|
|
323
|
+
... merchant_order_no='SHIP123',
|
|
324
|
+
... lgs_type='C2C',
|
|
325
|
+
... ship_type='1',
|
|
326
|
+
... receiver_store_code='123456',
|
|
327
|
+
... receiver_name='王小明',
|
|
328
|
+
... receiver_cell_phone='0912345678',
|
|
329
|
+
... goods_amount=500,
|
|
330
|
+
... goods_name='測試商品',
|
|
331
|
+
... is_collection='N',
|
|
332
|
+
... )
|
|
333
|
+
>>> result = service.create_shipment(shipment_data)
|
|
334
|
+
>>> print(result.logistics_no)
|
|
335
|
+
"""
|
|
336
|
+
# 參數驗證
|
|
337
|
+
if len(data.merchant_order_no) > 30:
|
|
338
|
+
raise ValueError('訂單編號不可超過 30 字元')
|
|
339
|
+
|
|
340
|
+
# 準備加密資料
|
|
341
|
+
encrypt_data_obj = {
|
|
342
|
+
'MerchantOrderNo': data.merchant_order_no,
|
|
343
|
+
'LgsType': data.lgs_type,
|
|
344
|
+
'ShipType': data.ship_type,
|
|
345
|
+
'ReceiverStoreCode': data.receiver_store_code,
|
|
346
|
+
'ReceiverName': data.receiver_name,
|
|
347
|
+
'ReceiverCellPhone': data.receiver_cell_phone,
|
|
348
|
+
'TimeStamp': str(int(time.time())),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# 可選參數
|
|
352
|
+
if data.receiver_tel:
|
|
353
|
+
encrypt_data_obj['ReceiverTel'] = data.receiver_tel
|
|
354
|
+
if data.goods_amount:
|
|
355
|
+
encrypt_data_obj['GoodsAmount'] = data.goods_amount
|
|
356
|
+
if data.goods_name:
|
|
357
|
+
encrypt_data_obj['GoodsName'] = data.goods_name
|
|
358
|
+
if data.sender_name:
|
|
359
|
+
encrypt_data_obj['SenderName'] = data.sender_name
|
|
360
|
+
if data.sender_cell_phone:
|
|
361
|
+
encrypt_data_obj['SenderCellPhone'] = data.sender_cell_phone
|
|
362
|
+
if data.is_collection == 'Y':
|
|
363
|
+
encrypt_data_obj['IsCollection'] = 'Y'
|
|
364
|
+
encrypt_data_obj['CollectionAmount'] = data.collection_amount
|
|
365
|
+
if data.note:
|
|
366
|
+
encrypt_data_obj['Note'] = data.note
|
|
367
|
+
|
|
368
|
+
# JSON 序列化並加密
|
|
369
|
+
json_str = json.dumps(encrypt_data_obj, ensure_ascii=False)
|
|
370
|
+
encrypt_data = self.aes_encrypt(json_str)
|
|
371
|
+
hash_data = self.generate_hash_data(encrypt_data)
|
|
372
|
+
|
|
373
|
+
# 準備 API 請求
|
|
374
|
+
api_data = {
|
|
375
|
+
'UID_': self.merchant_id,
|
|
376
|
+
'EncryptData_': encrypt_data,
|
|
377
|
+
'HashData_': hash_data,
|
|
378
|
+
'Version_': '1.0',
|
|
379
|
+
'RespondType_': 'JSON',
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# 發送 API 請求
|
|
383
|
+
try:
|
|
384
|
+
response = requests.post(
|
|
385
|
+
f'{self.base_url}/createShipment',
|
|
386
|
+
data=api_data,
|
|
387
|
+
timeout=30,
|
|
388
|
+
)
|
|
389
|
+
response.raise_for_status()
|
|
390
|
+
result = response.json()
|
|
391
|
+
except Exception as e:
|
|
392
|
+
raise Exception(f'API 請求失敗: {str(e)}')
|
|
393
|
+
|
|
394
|
+
# 解析回應
|
|
395
|
+
if result.get('Status') != 'SUCCESS':
|
|
396
|
+
return CVSShipmentResponse(
|
|
397
|
+
success=False,
|
|
398
|
+
status=result.get('Status', 'ERROR'),
|
|
399
|
+
message=result.get('Message', '未知錯誤'),
|
|
400
|
+
merchant_order_no=data.merchant_order_no,
|
|
401
|
+
raw=result,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# 解密回應資料
|
|
405
|
+
response_encrypt_data = result.get('EncryptData_', '')
|
|
406
|
+
response_hash_data = result.get('HashData_', '')
|
|
407
|
+
|
|
408
|
+
if response_encrypt_data and response_hash_data:
|
|
409
|
+
try:
|
|
410
|
+
if not self.verify_hash_data(response_encrypt_data, response_hash_data):
|
|
411
|
+
raise ValueError('回應 HashData 驗證失敗')
|
|
412
|
+
|
|
413
|
+
decrypted_json = self.aes_decrypt(response_encrypt_data)
|
|
414
|
+
decrypted = json.loads(decrypted_json)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
return CVSShipmentResponse(
|
|
417
|
+
success=False,
|
|
418
|
+
status='ERROR',
|
|
419
|
+
message=f'解密失敗: {str(e)}',
|
|
420
|
+
merchant_order_no=data.merchant_order_no,
|
|
421
|
+
raw=result,
|
|
422
|
+
)
|
|
423
|
+
else:
|
|
424
|
+
decrypted = {}
|
|
425
|
+
|
|
426
|
+
return CVSShipmentResponse(
|
|
427
|
+
success=True,
|
|
428
|
+
status=result.get('Status', ''),
|
|
429
|
+
message=result.get('Message', ''),
|
|
430
|
+
merchant_order_no=data.merchant_order_no,
|
|
431
|
+
logistics_no=decrypted.get('LogisticsNo'),
|
|
432
|
+
cvs_payment_no=decrypted.get('CVSPaymentNo'),
|
|
433
|
+
cvs_validation_no=decrypted.get('CVSValidationNo'),
|
|
434
|
+
booking_note=decrypted.get('BookingNote'),
|
|
435
|
+
raw=result,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# Usage Example
|
|
440
|
+
if __name__ == '__main__':
|
|
441
|
+
print('=' * 60)
|
|
442
|
+
print('NewebPay 藍新物流 CVS - Python 範例')
|
|
443
|
+
print('=' * 60)
|
|
444
|
+
print()
|
|
445
|
+
|
|
446
|
+
# 檢查依賴套件
|
|
447
|
+
if not HAS_DEPENDENCIES:
|
|
448
|
+
print('✗ 錯誤: 需要安裝必要套件')
|
|
449
|
+
print(' 請執行: pip install pycryptodome requests')
|
|
450
|
+
exit(1)
|
|
451
|
+
|
|
452
|
+
print('[注意] 請先至藍新金流申請測試帳號')
|
|
453
|
+
print('並將以下參數替換為您的測試環境資訊')
|
|
454
|
+
print()
|
|
455
|
+
|
|
456
|
+
# 初始化服務 (使用測試環境)
|
|
457
|
+
service = NewebPayCVSLogistics(
|
|
458
|
+
merchant_id='YOUR_MERCHANT_ID',
|
|
459
|
+
hash_key='YOUR_HASH_KEY', # 32 字元
|
|
460
|
+
hash_iv='YOUR_HASH_IV', # 16 字元
|
|
461
|
+
is_production=False,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# 範例 1: 查詢電子地圖 (門市選擇)
|
|
465
|
+
print('[範例 1] 查詢電子地圖 (門市選擇)')
|
|
466
|
+
print('-' * 60)
|
|
467
|
+
|
|
468
|
+
store_data = StoreMapData(
|
|
469
|
+
merchant_order_no=f'MAP{int(time.time())}',
|
|
470
|
+
lgs_type='C2C',
|
|
471
|
+
ship_type='1', # 7-ELEVEN
|
|
472
|
+
return_url='https://your-site.com/callback/store-map',
|
|
473
|
+
extra_data='order_id=123',
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
redirect_url = service.query_store_map(store_data)
|
|
478
|
+
print(f'✓ 電子地圖網址: {redirect_url}')
|
|
479
|
+
print('請將使用者導向此網址選擇門市')
|
|
480
|
+
except Exception as e:
|
|
481
|
+
print(f'✗ 發生例外: {str(e)}')
|
|
482
|
+
|
|
483
|
+
print()
|
|
484
|
+
print('-' * 60)
|
|
485
|
+
|
|
486
|
+
# 範例 2: 建立 C2C 物流訂單
|
|
487
|
+
print('[範例 2] 建立 C2C 物流訂單')
|
|
488
|
+
print('-' * 60)
|
|
489
|
+
|
|
490
|
+
shipment_data = CVSShipmentData(
|
|
491
|
+
merchant_order_no=f'SHIP{int(time.time())}',
|
|
492
|
+
lgs_type='C2C',
|
|
493
|
+
ship_type='1', # 7-ELEVEN
|
|
494
|
+
receiver_store_code='123456', # 收件門市代號 (從電子地圖取得)
|
|
495
|
+
receiver_name='王小明',
|
|
496
|
+
receiver_cell_phone='0912345678',
|
|
497
|
+
goods_amount=500,
|
|
498
|
+
goods_name='測試商品',
|
|
499
|
+
sender_name='賣家',
|
|
500
|
+
sender_cell_phone='0987654321',
|
|
501
|
+
is_collection='N', # 不代收貨款
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
result = service.create_shipment(shipment_data)
|
|
506
|
+
|
|
507
|
+
if result.success:
|
|
508
|
+
print(f'✓ 物流訂單建立成功')
|
|
509
|
+
print(f' 訂單編號: {result.merchant_order_no}')
|
|
510
|
+
print(f' 物流編號: {result.logistics_no}')
|
|
511
|
+
print(f' 寄貨編號: {result.cvs_payment_no}')
|
|
512
|
+
print(f' 驗證碼: {result.cvs_validation_no}')
|
|
513
|
+
print(f' 托運單號: {result.booking_note}')
|
|
514
|
+
else:
|
|
515
|
+
print(f'✗ 物流訂單建立失敗')
|
|
516
|
+
print(f' 狀態: {result.status}')
|
|
517
|
+
print(f' 訊息: {result.message}')
|
|
518
|
+
except Exception as e:
|
|
519
|
+
print(f'✗ 發生例外: {str(e)}')
|
|
520
|
+
|
|
521
|
+
print()
|
|
522
|
+
print('=' * 60)
|
|
523
|
+
print('範例執行完成')
|
|
524
|
+
print('=' * 60)
|