taiwan-payment-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 +197 -0
- package/assets/taiwan-payment/CLAUDE.md +297 -0
- package/assets/taiwan-payment/EXAMPLES.md +1425 -0
- package/assets/taiwan-payment/README.md +306 -0
- package/assets/taiwan-payment/SKILL.md +857 -0
- package/assets/taiwan-payment/data/error-codes.csv +20 -0
- package/assets/taiwan-payment/data/field-mappings.csv +15 -0
- package/assets/taiwan-payment/data/operations.csv +8 -0
- package/assets/taiwan-payment/data/payment-methods.csv +24 -0
- package/assets/taiwan-payment/data/providers.csv +4 -0
- package/assets/taiwan-payment/data/reasoning.csv +32 -0
- package/assets/taiwan-payment/data/troubleshooting.csv +18 -0
- package/assets/taiwan-payment/references/ecpay-payment-api.md +880 -0
- package/assets/taiwan-payment/references/newebpay-payment-api.md +677 -0
- package/assets/taiwan-payment/references/payuni-payment-api.md +997 -0
- package/assets/taiwan-payment/scripts/core.py +288 -0
- package/assets/taiwan-payment/scripts/recommend.py +269 -0
- package/assets/taiwan-payment/scripts/search.py +185 -0
- package/assets/taiwan-payment/scripts/test_payment.py +358 -0
- package/assets/templates/base/quick-reference.md +370 -0
- package/assets/templates/base/skill-content.md +851 -0
- package/assets/templates/platforms/antigravity.json +25 -0
- package/assets/templates/platforms/claude.json +26 -0
- package/assets/templates/platforms/codebuddy.json +25 -0
- package/assets/templates/platforms/codex.json +25 -0
- package/assets/templates/platforms/continue.json +25 -0
- package/assets/templates/platforms/copilot.json +25 -0
- package/assets/templates/platforms/cursor.json +25 -0
- package/assets/templates/platforms/gemini.json +25 -0
- package/assets/templates/platforms/kiro.json +25 -0
- package/assets/templates/platforms/opencode.json +25 -0
- package/assets/templates/platforms/qoder.json +25 -0
- package/assets/templates/platforms/roocode.json +25 -0
- package/assets/templates/platforms/trae.json +25 -0
- package/assets/templates/platforms/windsurf.json +25 -0
- package/dist/index.js +17095 -0
- package/package.json +58 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Taiwan Payment 搜索 CLI
|
|
4
|
+
|
|
5
|
+
命令行搜索工具,支援多種輸出格式
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
python search.py "信用卡" # 自動偵測域
|
|
9
|
+
python search.py "10100058" --domain error # 指定域
|
|
10
|
+
python search.py "金額" --format json # JSON 輸出
|
|
11
|
+
python search.py "ECPay" --domain all # 全域搜索
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import json
|
|
18
|
+
from typing import List, Dict, Tuple
|
|
19
|
+
|
|
20
|
+
# 添加父目錄到 path
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
22
|
+
|
|
23
|
+
from core import search, search_all, CSV_CONFIG
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_ascii_box(title: str, content: List[str], width: int = 80, style: str = 'double') -> str:
|
|
27
|
+
"""繪製 ASCII 邊框"""
|
|
28
|
+
if style == 'double':
|
|
29
|
+
top = '╔' + '═' * (width - 2) + '╗'
|
|
30
|
+
mid = '╠' + '═' * (width - 2) + '╣'
|
|
31
|
+
bot = '╚' + '═' * (width - 2) + '╝'
|
|
32
|
+
side = '║'
|
|
33
|
+
else:
|
|
34
|
+
top = '┌' + '─' * (width - 2) + '┐'
|
|
35
|
+
mid = '├' + '─' * (width - 2) + '┤'
|
|
36
|
+
bot = '└' + '─' * (width - 2) + '┘'
|
|
37
|
+
side = '│'
|
|
38
|
+
|
|
39
|
+
lines = [top]
|
|
40
|
+
lines.append(f'{side} {title.center(width - 4)} {side}')
|
|
41
|
+
lines.append(mid)
|
|
42
|
+
|
|
43
|
+
for line in content:
|
|
44
|
+
# 分割長行
|
|
45
|
+
if len(line) > width - 4:
|
|
46
|
+
words = line.split()
|
|
47
|
+
current_line = ''
|
|
48
|
+
for word in words:
|
|
49
|
+
if len(current_line) + len(word) + 1 < width - 4:
|
|
50
|
+
current_line += word + ' '
|
|
51
|
+
else:
|
|
52
|
+
lines.append(f'{side} {current_line.ljust(width - 4)} {side}')
|
|
53
|
+
current_line = word + ' '
|
|
54
|
+
if current_line:
|
|
55
|
+
lines.append(f'{side} {current_line.ljust(width - 4)} {side}')
|
|
56
|
+
else:
|
|
57
|
+
lines.append(f'{side} {line.ljust(width - 4)} {side}')
|
|
58
|
+
|
|
59
|
+
lines.append(bot)
|
|
60
|
+
return '\n'.join(lines)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_result_ascii(results: List[Dict], domain: str) -> str:
|
|
64
|
+
"""ASCII 格式輸出"""
|
|
65
|
+
if not results:
|
|
66
|
+
return f'查無結果 (域: {domain})'
|
|
67
|
+
|
|
68
|
+
output = []
|
|
69
|
+
output.append('')
|
|
70
|
+
output.append(f'搜索域: {domain}')
|
|
71
|
+
output.append(f'找到 {len(results)} 筆結果')
|
|
72
|
+
output.append('=' * 80)
|
|
73
|
+
|
|
74
|
+
for i, result in enumerate(results, 1):
|
|
75
|
+
score = result.pop('_score', 0)
|
|
76
|
+
result.pop('_domain', None)
|
|
77
|
+
|
|
78
|
+
output.append(f'\n[結果 #{i}] 評分: {score}')
|
|
79
|
+
output.append('-' * 80)
|
|
80
|
+
|
|
81
|
+
for key, value in result.items():
|
|
82
|
+
if value:
|
|
83
|
+
key_display = key.replace('_', ' ').title()
|
|
84
|
+
output.append(f'{key_display:20s}: {value}')
|
|
85
|
+
|
|
86
|
+
return '\n'.join(output)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_all_results_ascii(all_results: Dict[str, List]) -> str:
|
|
90
|
+
"""全域搜索 ASCII 輸出"""
|
|
91
|
+
if not all_results:
|
|
92
|
+
return '查無結果'
|
|
93
|
+
|
|
94
|
+
output = []
|
|
95
|
+
output.append('')
|
|
96
|
+
output.append(f'全域搜索 - 找到 {len(all_results)} 個域有結果')
|
|
97
|
+
output.append('=' * 80)
|
|
98
|
+
|
|
99
|
+
for domain, results in all_results.items():
|
|
100
|
+
output.append(f'\n【{domain.upper()}】 ({len(results)} 筆)')
|
|
101
|
+
output.append('-' * 80)
|
|
102
|
+
|
|
103
|
+
for i, result in enumerate(results, 1):
|
|
104
|
+
score = result.pop('_score', 0)
|
|
105
|
+
result.pop('_domain', None)
|
|
106
|
+
|
|
107
|
+
output.append(f' [{i}] 評分: {score}')
|
|
108
|
+
|
|
109
|
+
# 顯示關鍵欄位
|
|
110
|
+
if domain == 'provider':
|
|
111
|
+
output.append(f' 服務商: {result.get("display_name", "")}')
|
|
112
|
+
output.append(f' 加密: {result.get("auth_method", "")}')
|
|
113
|
+
elif domain == 'error':
|
|
114
|
+
output.append(f' 錯誤碼: {result.get("code", "")} - {result.get("message_zh", "")}')
|
|
115
|
+
output.append(f' 解決: {result.get("solution", "")}')
|
|
116
|
+
elif domain == 'payment_method':
|
|
117
|
+
output.append(f' 方式: {result.get("name_zh", "")}')
|
|
118
|
+
output.append(f' 說明: {result.get("description", "")}')
|
|
119
|
+
else:
|
|
120
|
+
# 顯示前 3 個欄位
|
|
121
|
+
for key, value in list(result.items())[:3]:
|
|
122
|
+
if value:
|
|
123
|
+
output.append(f' {key}: {value}')
|
|
124
|
+
|
|
125
|
+
output.append('')
|
|
126
|
+
|
|
127
|
+
return '\n'.join(output)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main():
|
|
131
|
+
parser = argparse.ArgumentParser(
|
|
132
|
+
description='台灣金流搜索工具',
|
|
133
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
134
|
+
epilog='''
|
|
135
|
+
範例:
|
|
136
|
+
python search.py "信用卡" # 自動偵測域
|
|
137
|
+
python search.py "10100058" --domain error # 搜索錯誤碼
|
|
138
|
+
python search.py "ECPay" --format json # JSON 輸出
|
|
139
|
+
python search.py "金額" --domain all # 全域搜索
|
|
140
|
+
|
|
141
|
+
可用域:
|
|
142
|
+
provider, operation, error, field, payment_method, troubleshoot, reasoning, all
|
|
143
|
+
'''
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
parser.add_argument('query', type=str, help='搜索查詢')
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
'--domain', '-d',
|
|
149
|
+
type=str,
|
|
150
|
+
choices=list(CSV_CONFIG.keys()) + ['all'],
|
|
151
|
+
help='搜索域 (不指定則自動偵測)'
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
'--format', '-f',
|
|
155
|
+
choices=['ascii', 'json'],
|
|
156
|
+
default='ascii',
|
|
157
|
+
help='輸出格式'
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
'--max', '-m',
|
|
161
|
+
type=int,
|
|
162
|
+
default=5,
|
|
163
|
+
help='最大結果數'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
|
|
168
|
+
# 執行搜索
|
|
169
|
+
if args.domain == 'all':
|
|
170
|
+
results = search_all(args.query, max_per_domain=args.max)
|
|
171
|
+
if args.format == 'json':
|
|
172
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
173
|
+
else:
|
|
174
|
+
print(format_all_results_ascii(results))
|
|
175
|
+
else:
|
|
176
|
+
results = search(args.query, domain=args.domain, max_results=args.max)
|
|
177
|
+
if args.format == 'json':
|
|
178
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
179
|
+
else:
|
|
180
|
+
domain = results[0]['_domain'] if results else (args.domain or 'unknown')
|
|
181
|
+
print(format_result_ascii(results, domain))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
main()
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
台灣金流 API 連線測試腳本
|
|
4
|
+
|
|
5
|
+
支援平台: ECPay, NewebPay, PayUNi
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
python test_payment.py # 測試 ECPay 連線
|
|
9
|
+
python test_payment.py --platform newebpay # 測試 NewebPay 連線
|
|
10
|
+
python test_payment.py --platform payuni # 測試 PayUNi 連線
|
|
11
|
+
python test_payment.py --list # 列出支援平台
|
|
12
|
+
python test_payment.py --create # 建立測試訂單
|
|
13
|
+
python test_payment.py --query ORDER123 # 查詢訂單
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import time
|
|
19
|
+
import sys
|
|
20
|
+
import argparse
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import requests
|
|
25
|
+
HAS_REQUESTS = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_REQUESTS = False
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from Crypto.Cipher import AES
|
|
31
|
+
from Crypto.Util.Padding import pad
|
|
32
|
+
HAS_CRYPTO = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_CRYPTO = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 平台設定
|
|
38
|
+
PLATFORMS = {
|
|
39
|
+
'ecpay': {
|
|
40
|
+
'name': '綠界科技 ECPay',
|
|
41
|
+
'merchant_id': '3002607',
|
|
42
|
+
'hash_key': 'pwFHCqoQZGmho4w6',
|
|
43
|
+
'hash_iv': 'EkRm7iFT261dpevs',
|
|
44
|
+
'api_url': 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5',
|
|
45
|
+
'query_url': 'https://payment-stage.ecpay.com.tw/Cashier/QueryTradeInfo/V5',
|
|
46
|
+
'test_url': 'https://payment-stage.ecpay.com.tw/',
|
|
47
|
+
'auth_method': 'SHA256',
|
|
48
|
+
'test_card': '4311-9522-2222-2222',
|
|
49
|
+
},
|
|
50
|
+
'newebpay': {
|
|
51
|
+
'name': '藍新金流 NewebPay',
|
|
52
|
+
'merchant_id': '請至後台申請',
|
|
53
|
+
'hash_key': '請至後台申請',
|
|
54
|
+
'hash_iv': '請至後台申請',
|
|
55
|
+
'api_url': 'https://ccore.newebpay.com/MPG/mpg_gateway',
|
|
56
|
+
'query_url': 'https://ccore.newebpay.com/API/QueryTradeInfo',
|
|
57
|
+
'test_url': 'https://ccore.newebpay.com/',
|
|
58
|
+
'auth_method': 'AES-256-CBC',
|
|
59
|
+
'test_card': '4000-2211-1111-1111',
|
|
60
|
+
},
|
|
61
|
+
'payuni': {
|
|
62
|
+
'name': '統一金流 PAYUNi',
|
|
63
|
+
'merchant_id': '請至後台申請',
|
|
64
|
+
'hash_key': '請至後台申請',
|
|
65
|
+
'hash_iv': '請至後台申請',
|
|
66
|
+
'api_url': 'https://sandbox-api.payuni.com.tw/api/upp',
|
|
67
|
+
'query_url': 'https://sandbox-api.payuni.com.tw/api/trade_query',
|
|
68
|
+
'test_url': 'https://sandbox-api.payuni.com.tw/',
|
|
69
|
+
'auth_method': 'AES-256-GCM',
|
|
70
|
+
'test_card': '4000-2211-1111-1111',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def generate_ecpay_mac(params: dict, hash_key: str, hash_iv: str) -> str:
|
|
76
|
+
"""ECPay CheckMacValue (SHA256)"""
|
|
77
|
+
sorted_params = sorted(params.items())
|
|
78
|
+
param_str = '&'.join(f'{k}={v}' for k, v in sorted_params)
|
|
79
|
+
raw = f'HashKey={hash_key}&{param_str}&HashIV={hash_iv}'
|
|
80
|
+
encoded = urllib.parse.quote_plus(raw).lower()
|
|
81
|
+
return hashlib.sha256(encoded.encode('utf-8')).hexdigest().upper()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate_newebpay_trade_info(params: dict, hash_key: str, hash_iv: str) -> str:
|
|
85
|
+
"""NewebPay TradeInfo (AES-256-CBC)"""
|
|
86
|
+
if not HAS_CRYPTO:
|
|
87
|
+
return "需要 pycryptodome 套件"
|
|
88
|
+
query_string = urllib.parse.urlencode(params)
|
|
89
|
+
cipher = AES.new(hash_key.encode('utf-8'), AES.MODE_CBC, hash_iv.encode('utf-8'))
|
|
90
|
+
padded = pad(query_string.encode('utf-8'), AES.block_size)
|
|
91
|
+
encrypted = cipher.encrypt(padded)
|
|
92
|
+
return encrypted.hex()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_newebpay_sha(trade_info: str, hash_key: str, hash_iv: str) -> str:
|
|
96
|
+
"""NewebPay TradeSha (SHA256)"""
|
|
97
|
+
raw = f'HashKey={hash_key}&{trade_info}&HashIV={hash_iv}'
|
|
98
|
+
return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_payuni_encrypt(params: dict, hash_key: str, hash_iv: str) -> str:
|
|
102
|
+
"""PayUNi EncryptInfo (AES-256-GCM)"""
|
|
103
|
+
if not HAS_CRYPTO:
|
|
104
|
+
return "需要 pycryptodome 套件"
|
|
105
|
+
query_string = urllib.parse.urlencode(params)
|
|
106
|
+
cipher = AES.new(hash_key.encode('utf-8'), AES.MODE_GCM, nonce=hash_iv.encode('utf-8'))
|
|
107
|
+
encrypted, tag = cipher.encrypt_and_digest(query_string.encode('utf-8'))
|
|
108
|
+
return (encrypted + tag).hex()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_payuni_hash(encrypt_info: str, hash_key: str, hash_iv: str) -> str:
|
|
112
|
+
"""PayUNi HashInfo (SHA256)"""
|
|
113
|
+
raw = encrypt_info + hash_key + hash_iv
|
|
114
|
+
return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_connection(platform: str):
|
|
118
|
+
"""測試 API 連線"""
|
|
119
|
+
config = PLATFORMS.get(platform)
|
|
120
|
+
if not config:
|
|
121
|
+
print(f'錯誤: 不支援的平台 "{platform}"')
|
|
122
|
+
print(f'支援的平台: {", ".join(PLATFORMS.keys())}')
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
print('=' * 60)
|
|
126
|
+
print(f'{config["name"]} API 連線測試')
|
|
127
|
+
print('=' * 60)
|
|
128
|
+
print()
|
|
129
|
+
|
|
130
|
+
# 測試環境資訊
|
|
131
|
+
print('[測試環境]')
|
|
132
|
+
print(f' 平台: {config["name"]}')
|
|
133
|
+
print(f' 商店代號: {config["merchant_id"]}')
|
|
134
|
+
print(f' 加密方式: {config["auth_method"]}')
|
|
135
|
+
print(f' 測試網址: {config["test_url"]}')
|
|
136
|
+
print()
|
|
137
|
+
|
|
138
|
+
# 測試加密計算
|
|
139
|
+
print('[加密計算測試]')
|
|
140
|
+
if platform == 'ecpay':
|
|
141
|
+
test_params = {
|
|
142
|
+
'MerchantID': config['merchant_id'],
|
|
143
|
+
'MerchantTradeNo': 'TEST123456',
|
|
144
|
+
'TotalAmount': 100,
|
|
145
|
+
}
|
|
146
|
+
mac = generate_ecpay_mac(test_params, config['hash_key'], config['hash_iv'])
|
|
147
|
+
print(f' CheckMacValue: {mac}')
|
|
148
|
+
print(f' 長度: {len(mac)} (應為 64)')
|
|
149
|
+
print(f' 格式: {"正確" if len(mac) == 64 else "錯誤"}')
|
|
150
|
+
|
|
151
|
+
elif platform == 'newebpay':
|
|
152
|
+
if HAS_CRYPTO and config['hash_key'] != '請至後台申請':
|
|
153
|
+
test_params = {
|
|
154
|
+
'MerchantID': config['merchant_id'],
|
|
155
|
+
'MerchantOrderNo': 'TEST123456',
|
|
156
|
+
'Amt': 100,
|
|
157
|
+
}
|
|
158
|
+
trade_info = generate_newebpay_trade_info(test_params, config['hash_key'], config['hash_iv'])
|
|
159
|
+
trade_sha = generate_newebpay_sha(trade_info, config['hash_key'], config['hash_iv'])
|
|
160
|
+
print(f' TradeInfo: {trade_info[:50]}...')
|
|
161
|
+
print(f' TradeSha: {trade_sha}')
|
|
162
|
+
else:
|
|
163
|
+
print(' (需要設定 hash_key/hash_iv 及 pycryptodome 套件)')
|
|
164
|
+
|
|
165
|
+
elif platform == 'payuni':
|
|
166
|
+
if HAS_CRYPTO and config['hash_key'] != '請至後台申請':
|
|
167
|
+
test_params = {
|
|
168
|
+
'MerID': config['merchant_id'],
|
|
169
|
+
'MerTradeNo': 'TEST123456',
|
|
170
|
+
'TradeAmt': 100,
|
|
171
|
+
}
|
|
172
|
+
encrypt_info = generate_payuni_encrypt(test_params, config['hash_key'], config['hash_iv'])
|
|
173
|
+
hash_info = generate_payuni_hash(encrypt_info, config['hash_key'], config['hash_iv'])
|
|
174
|
+
print(f' EncryptInfo: {encrypt_info[:50]}...')
|
|
175
|
+
print(f' HashInfo: {hash_info}')
|
|
176
|
+
else:
|
|
177
|
+
print(' (需要設定 hash_key/hash_iv 及 pycryptodome 套件)')
|
|
178
|
+
print()
|
|
179
|
+
|
|
180
|
+
# 測試網路連線
|
|
181
|
+
if HAS_REQUESTS:
|
|
182
|
+
print('[網路連線測試]')
|
|
183
|
+
try:
|
|
184
|
+
response = requests.head(config['test_url'], timeout=5)
|
|
185
|
+
print(f' 狀態碼: {response.status_code}')
|
|
186
|
+
print(f' 連線: {"成功" if response.status_code < 500 else "失敗"}')
|
|
187
|
+
except requests.RequestException as e:
|
|
188
|
+
print(f' 連線失敗: {e}')
|
|
189
|
+
else:
|
|
190
|
+
print('[網路連線測試]')
|
|
191
|
+
print(' (需要 requests 套件: pip install requests)')
|
|
192
|
+
print()
|
|
193
|
+
|
|
194
|
+
# 顯示測試信用卡
|
|
195
|
+
print('[測試信用卡]')
|
|
196
|
+
print(f' 卡號: {config["test_card"]}')
|
|
197
|
+
print(' 有效期: 任意未過期日期')
|
|
198
|
+
print(' CVV: 任意三碼')
|
|
199
|
+
print()
|
|
200
|
+
|
|
201
|
+
print('=' * 60)
|
|
202
|
+
print('測試完成')
|
|
203
|
+
print('=' * 60)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_test_order(platform: str):
|
|
207
|
+
"""建立測試訂單"""
|
|
208
|
+
config = PLATFORMS.get(platform)
|
|
209
|
+
if not config:
|
|
210
|
+
print(f'錯誤: 不支援的平台 "{platform}"')
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
if config['merchant_id'] == '請至後台申請':
|
|
214
|
+
print(f'錯誤: 請先設定 {config["name"]} 的測試帳號')
|
|
215
|
+
print('請編輯此腳本,填入您的測試商店資訊')
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
|
|
218
|
+
order_id = f'TEST{int(time.time())}'
|
|
219
|
+
trade_date = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
|
|
220
|
+
|
|
221
|
+
print('=' * 60)
|
|
222
|
+
print(f'建立 {config["name"]} 測試訂單')
|
|
223
|
+
print('=' * 60)
|
|
224
|
+
print()
|
|
225
|
+
print(f'訂單編號: {order_id}')
|
|
226
|
+
print(f'交易時間: {trade_date}')
|
|
227
|
+
print(f'金額: 100 TWD')
|
|
228
|
+
print()
|
|
229
|
+
|
|
230
|
+
if platform == 'ecpay':
|
|
231
|
+
params = {
|
|
232
|
+
'MerchantID': config['merchant_id'],
|
|
233
|
+
'MerchantTradeNo': order_id,
|
|
234
|
+
'MerchantTradeDate': trade_date,
|
|
235
|
+
'PaymentType': 'aio',
|
|
236
|
+
'TotalAmount': 100,
|
|
237
|
+
'TradeDesc': urllib.parse.quote('測試訂單'),
|
|
238
|
+
'ItemName': '測試商品 x 1',
|
|
239
|
+
'ReturnURL': 'https://example.com/callback',
|
|
240
|
+
'ChoosePayment': 'Credit',
|
|
241
|
+
'EncryptType': 1,
|
|
242
|
+
}
|
|
243
|
+
params['CheckMacValue'] = generate_ecpay_mac(params, config['hash_key'], config['hash_iv'])
|
|
244
|
+
|
|
245
|
+
html = f'''<!DOCTYPE html>
|
|
246
|
+
<html>
|
|
247
|
+
<head><meta charset="UTF-8"><title>ECPay 測試</title></head>
|
|
248
|
+
<body>
|
|
249
|
+
<h1>ECPay 測試訂單 {order_id}</h1>
|
|
250
|
+
<form method="post" action="{config['api_url']}">
|
|
251
|
+
'''
|
|
252
|
+
for k, v in params.items():
|
|
253
|
+
html += f'<input type="hidden" name="{k}" value="{v}">\n'
|
|
254
|
+
html += '<button type="submit">前往付款</button>\n</form>\n</body></html>'
|
|
255
|
+
|
|
256
|
+
filename = f'ecpay_test_{order_id}.html'
|
|
257
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
258
|
+
f.write(html)
|
|
259
|
+
|
|
260
|
+
print(f'已產生測試頁面: {filename}')
|
|
261
|
+
print('請在瀏覽器中開啟此檔案進行付款測試')
|
|
262
|
+
print()
|
|
263
|
+
print(f'測試信用卡: {config["test_card"]}')
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
print(f'{platform} 的測試訂單建立功能需要 pycryptodome 套件')
|
|
267
|
+
print('請執行: pip install pycryptodome')
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def query_order(platform: str, order_id: str):
|
|
271
|
+
"""查詢訂單狀態"""
|
|
272
|
+
if not HAS_REQUESTS:
|
|
273
|
+
print('錯誤: 需要 requests 套件')
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
|
|
276
|
+
config = PLATFORMS.get(platform)
|
|
277
|
+
if not config:
|
|
278
|
+
print(f'錯誤: 不支援的平台 "{platform}"')
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
|
|
281
|
+
print('=' * 60)
|
|
282
|
+
print(f'查詢 {config["name"]} 訂單: {order_id}')
|
|
283
|
+
print('=' * 60)
|
|
284
|
+
print()
|
|
285
|
+
|
|
286
|
+
if platform == 'ecpay':
|
|
287
|
+
params = {
|
|
288
|
+
'MerchantID': config['merchant_id'],
|
|
289
|
+
'MerchantTradeNo': order_id,
|
|
290
|
+
'TimeStamp': int(time.time()),
|
|
291
|
+
}
|
|
292
|
+
params['CheckMacValue'] = generate_ecpay_mac(params, config['hash_key'], config['hash_iv'])
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
response = requests.post(config['query_url'], data=params, timeout=10)
|
|
296
|
+
print(f'HTTP 狀態碼: {response.status_code}')
|
|
297
|
+
print('回應內容:')
|
|
298
|
+
print(response.text)
|
|
299
|
+
except requests.RequestException as e:
|
|
300
|
+
print(f'查詢失敗: {e}')
|
|
301
|
+
else:
|
|
302
|
+
print(f'{platform} 的查詢功能需要額外設定')
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def list_platforms():
|
|
306
|
+
"""列出支援的平台"""
|
|
307
|
+
print('=' * 60)
|
|
308
|
+
print('支援的金流平台')
|
|
309
|
+
print('=' * 60)
|
|
310
|
+
print()
|
|
311
|
+
for key, config in PLATFORMS.items():
|
|
312
|
+
print(f' {key:12} - {config["name"]}')
|
|
313
|
+
print(f' 加密: {config["auth_method"]}')
|
|
314
|
+
print(f' 測試: {config["test_url"]}')
|
|
315
|
+
print()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def main():
|
|
319
|
+
parser = argparse.ArgumentParser(
|
|
320
|
+
description='台灣金流 API 測試工具 (支援 ECPay/NewebPay/PayUNi)'
|
|
321
|
+
)
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
'--platform', '-p',
|
|
324
|
+
type=str,
|
|
325
|
+
default='ecpay',
|
|
326
|
+
help='金流平台 (ecpay/newebpay/payuni)'
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
'--list', '-l',
|
|
330
|
+
action='store_true',
|
|
331
|
+
help='列出支援的平台'
|
|
332
|
+
)
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
'--create',
|
|
335
|
+
action='store_true',
|
|
336
|
+
help='建立測試訂單'
|
|
337
|
+
)
|
|
338
|
+
parser.add_argument(
|
|
339
|
+
'--query',
|
|
340
|
+
type=str,
|
|
341
|
+
metavar='ORDER_ID',
|
|
342
|
+
help='查詢訂單狀態'
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
args = parser.parse_args()
|
|
346
|
+
|
|
347
|
+
if args.list:
|
|
348
|
+
list_platforms()
|
|
349
|
+
elif args.create:
|
|
350
|
+
create_test_order(args.platform)
|
|
351
|
+
elif args.query:
|
|
352
|
+
query_order(args.platform, args.query)
|
|
353
|
+
else:
|
|
354
|
+
test_connection(args.platform)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if __name__ == '__main__':
|
|
358
|
+
main()
|