taiwan-payment-skill 1.0.2 → 1.0.4
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 +2 -2
- package/assets/taiwan-payment/CLAUDE.md +297 -297
- package/assets/taiwan-payment/EXAMPLES.md +1425 -1425
- package/assets/taiwan-payment/README.md +2 -2
- package/assets/taiwan-payment/SKILL.md +857 -857
- package/assets/taiwan-payment/references/ecpay-payment-api.md +880 -880
- package/assets/taiwan-payment/references/newebpay-payment-api.md +677 -677
- package/assets/taiwan-payment/references/payuni-payment-api.md +997 -997
- package/assets/templates/base/quick-reference.md +60 -345
- package/assets/templates/base/skill-content.md +248 -738
- package/assets/templates/platforms/agent.json +26 -0
- package/assets/templates/platforms/claude.json +26 -26
- package/assets/templates/platforms/codebuddy.json +25 -25
- package/assets/templates/platforms/codex.json +26 -25
- package/assets/templates/platforms/continue.json +25 -25
- package/assets/templates/platforms/copilot.json +25 -25
- package/assets/templates/platforms/cursor.json +26 -25
- package/assets/templates/platforms/gemini.json +25 -25
- package/assets/templates/platforms/kiro.json +25 -25
- package/assets/templates/platforms/opencode.json +25 -25
- package/assets/templates/platforms/qoder.json +25 -25
- package/assets/templates/platforms/roocode.json +25 -25
- package/assets/templates/platforms/trae.json +25 -25
- package/assets/templates/platforms/windsurf.json +25 -25
- package/dist/index.js +127 -29
- package/package.json +1 -1
|
@@ -1,857 +1,857 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: taiwan-payment
|
|
3
|
-
description: Taiwan Payment API integration specialist for ECPay, NewebPay, and PAYUNi payment gateways. Use when developing payment systems, implementing credit card, ATM, CVS, or e-wallet payments, or working with Taiwan payment gateway APIs. Handles encryption (SHA256, AES-256-CBC, AES-256-GCM), API requests, and service provider differences.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Taiwan Payment Development Skill
|
|
8
|
-
|
|
9
|
-
> 此技能涵蓋台灣金流 API 整合開發,包含綠界科技 (ECPay)、藍新金流 (NewebPay)、統一金流 (PAYUNi) 三家服務商。
|
|
10
|
-
|
|
11
|
-
## 快速導覽
|
|
12
|
-
|
|
13
|
-
### 相關文件
|
|
14
|
-
使用此技能時,請參考專案中的 API 規格文件:
|
|
15
|
-
- `references/ECPAY_PAYMENT_REFERENCE.md` - 綠界金流 API 規格
|
|
16
|
-
- `references/NEWEBPAY_PAYMENT_REFERENCE.md` - 藍新金流 API 規格
|
|
17
|
-
- `references/PAYUNI_PAYMENT_REFERENCE.md` - 統一金流 API 規格
|
|
18
|
-
- [EXAMPLES.md](EXAMPLES.md) - 程式碼範例集
|
|
19
|
-
|
|
20
|
-
### 智能工具
|
|
21
|
-
- `scripts/search.py` - BM25 搜索引擎(查詢 API、錯誤碼、欄位映射、付款方式)
|
|
22
|
-
- `scripts/recommend.py` - 金流服務商推薦系統
|
|
23
|
-
- `scripts/test_payment.py` - 付款測試工具
|
|
24
|
-
- `data/` - CSV 數據檔(providers, operations, error-codes, field-mappings, payment-methods, troubleshooting, reasoning)
|
|
25
|
-
|
|
26
|
-
### 何時使用此技能
|
|
27
|
-
- 開發線上金流付款功能
|
|
28
|
-
- 整合台灣金流服務商 API
|
|
29
|
-
- 實作信用卡、ATM、超商、電子錢包等付款方式
|
|
30
|
-
- 處理訂單查詢、退款、定期定額扣款
|
|
31
|
-
- 處理加密簽章(SHA256、AES-256-CBC、AES-256-GCM)
|
|
32
|
-
- 解決金流 API 整合問題
|
|
33
|
-
|
|
34
|
-
## 智能搜索與推薦
|
|
35
|
-
|
|
36
|
-
### 搜索引擎 (search.py)
|
|
37
|
-
|
|
38
|
-
使用 BM25 算法在資料庫中搜索相關資訊:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
# 搜索服務商
|
|
42
|
-
python scripts/search.py "ecpay" --domain provider
|
|
43
|
-
|
|
44
|
-
# 搜索錯誤碼
|
|
45
|
-
python scripts/search.py "10100058" --domain error
|
|
46
|
-
|
|
47
|
-
# 搜索欄位映射
|
|
48
|
-
python scripts/search.py "MerchantTradeNo" --domain field
|
|
49
|
-
|
|
50
|
-
# 搜索付款方式
|
|
51
|
-
python scripts/search.py "信用卡" --domain payment_method
|
|
52
|
-
|
|
53
|
-
# 搜索疑難排解
|
|
54
|
-
python scripts/search.py "CheckMacValue 錯誤" --domain troubleshoot
|
|
55
|
-
|
|
56
|
-
# 全域搜索
|
|
57
|
-
python scripts/search.py "金額計算" --domain all
|
|
58
|
-
|
|
59
|
-
# JSON 輸出
|
|
60
|
-
python scripts/search.py "ATM" --format json
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**搜索域:**
|
|
64
|
-
| 域 | 說明 | CSV 檔案 |
|
|
65
|
-
|-----|------|----------|
|
|
66
|
-
| `provider` | 服務商比較 | providers.csv |
|
|
67
|
-
| `operation` | API 操作端點 | operations.csv |
|
|
68
|
-
| `error` | 錯誤碼查詢 | error-codes.csv |
|
|
69
|
-
| `field` | 欄位映射 | field-mappings.csv |
|
|
70
|
-
| `payment_method` | 付款方式 | payment-methods.csv |
|
|
71
|
-
| `troubleshoot` | 疑難排解 | troubleshooting.csv |
|
|
72
|
-
| `reasoning` | 推薦決策規則 | reasoning.csv |
|
|
73
|
-
|
|
74
|
-
**域自動偵測:**
|
|
75
|
-
搜索引擎會自動偵測查詢內容並選擇最適合的域:
|
|
76
|
-
- 錯誤碼格式(如 "10100058")→ error
|
|
77
|
-
- 服務商名稱(如 "ECPay")→ provider
|
|
78
|
-
- 付款方式(如 "信用卡"、"ATM")→ payment_method
|
|
79
|
-
- API 欄位(如 "MerchantID")→ field
|
|
80
|
-
|
|
81
|
-
### 推薦系統 (recommend.py)
|
|
82
|
-
|
|
83
|
-
根據需求自動推薦最適合的金流服務商:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# 高交易量電商
|
|
87
|
-
python scripts/recommend.py "高交易量 穩定 電商"
|
|
88
|
-
# → 推薦 ECPay (市佔率高,穩定性佳)
|
|
89
|
-
|
|
90
|
-
# 多元支付需求
|
|
91
|
-
python scripts/recommend.py "多元支付 LINE Pay Apple Pay"
|
|
92
|
-
# → 推薦 NewebPay (支援 13 種付款方式)
|
|
93
|
-
|
|
94
|
-
# API 設計優先
|
|
95
|
-
python scripts/recommend.py "API RESTful JSON"
|
|
96
|
-
# → 推薦 PAYUNi (RESTful JSON API)
|
|
97
|
-
|
|
98
|
-
# JSON 輸出
|
|
99
|
-
python scripts/recommend.py "新創公司 快速整合" --format json
|
|
100
|
-
|
|
101
|
-
# 簡單文字輸出
|
|
102
|
-
python scripts/recommend.py "會員制 定期扣款" --format simple
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**推薦關鍵字:**
|
|
106
|
-
- **ECPay**: 穩定、市佔、高交易量、電商、ATM、超商、定期、訂閱、分期、發票、物流
|
|
107
|
-
- **NewebPay**: 多元、支付方式、電子錢包、LINE、行動、記憶、會員、跨境
|
|
108
|
-
- **PAYUNi**: API、JSON、RESTful、統一、新創
|
|
109
|
-
|
|
110
|
-
**反模式警告:**
|
|
111
|
-
推薦系統會自動提示不建議的場景:
|
|
112
|
-
- ECPay: 無技術資源、極簡需求
|
|
113
|
-
- NewebPay: 簡單 API、單一支付
|
|
114
|
-
- PAYUNi: 大型專案、完整文檔
|
|
115
|
-
|
|
116
|
-
### 付款測試工具 (test_payment.py)
|
|
117
|
-
|
|
118
|
-
快速測試金流服務商連線:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
# 測試 ECPay 連線
|
|
122
|
-
python scripts/test_payment.py ecpay
|
|
123
|
-
|
|
124
|
-
# 測試 NewebPay 連線
|
|
125
|
-
python scripts/test_payment.py newebpay
|
|
126
|
-
|
|
127
|
-
# 測試 PAYUNi 連線
|
|
128
|
-
python scripts/test_payment.py payuni
|
|
129
|
-
|
|
130
|
-
# 測試所有服務商
|
|
131
|
-
python scripts/test_payment.py all
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## 付款方式說明
|
|
137
|
-
|
|
138
|
-
### 信用卡支付
|
|
139
|
-
- **一次付清**: 最常用的付款方式,2-3 天到帳
|
|
140
|
-
- **信用卡分期**: 3/6/12/18/24/30 期,需最低金額 1000 元
|
|
141
|
-
- **信用卡定期**: 週期扣款,適用訂閱制服務
|
|
142
|
-
- **信用卡紅利**: 紅利折抵功能
|
|
143
|
-
- **銀聯卡**: 需另外申請,支援中國銀聯
|
|
144
|
-
- **美國運通卡**: 需另外申請
|
|
145
|
-
|
|
146
|
-
### 電子錢包
|
|
147
|
-
- **Apple Pay**: 需申請,適合 iOS 用戶
|
|
148
|
-
- **Google Pay**: 需申請,適合 Android 用戶
|
|
149
|
-
- **Samsung Pay**: 需申請,三星手機專用
|
|
150
|
-
- **LINE Pay**: 需申請,LINE 生態系整合
|
|
151
|
-
- **玉山 Wallet**: 玉山銀行電子錢包
|
|
152
|
-
- **台灣 Pay**: 官方行動支付,最高 49,999 元
|
|
153
|
-
|
|
154
|
-
### ATM 轉帳
|
|
155
|
-
- **網路 ATM**: 即時轉帳,最高 49,999 元,1 天到帳
|
|
156
|
-
- **ATM 虛擬帳號**: 產生專屬繳費帳號,1-3 天到帳,最高 49,999 元
|
|
157
|
-
|
|
158
|
-
### 超商支付
|
|
159
|
-
- **超商代碼**: 至四大超商繳費,30-20,000 元,1-3 天到帳
|
|
160
|
-
- **超商條碼**: 產生繳費條碼,20-40,000 元,1-3 天到帳
|
|
161
|
-
|
|
162
|
-
### 其他支付方式
|
|
163
|
-
- **TWQR**: 台灣 Pay QR Code 掃碼支付
|
|
164
|
-
- **BNPL 無卡分期**: 先買後付,50-300,000 元
|
|
165
|
-
- **AFTEE**: PAYUNi 專屬先享後付
|
|
166
|
-
- **iCash**: PAYUNi 專屬愛金卡支付
|
|
167
|
-
- **簡單付支付寶/微信**: 跨境支付(中國市場)
|
|
168
|
-
|
|
169
|
-
## 三家服務商特性比較
|
|
170
|
-
|
|
171
|
-
| 特性 | 綠界 ECPay | 藍新 NewebPay | 統一 PAYUNi |
|
|
172
|
-
|------|-----------|--------------|------------|
|
|
173
|
-
| 加密方式 | URL Encode + SHA256 | AES-256-CBC + SHA256 雙層 | AES-256-GCM + SHA256 |
|
|
174
|
-
| API 風格 | Form POST | Form POST + AES | RESTful JSON |
|
|
175
|
-
| 內容格式 | application/x-www-form-urlencoded | application/x-www-form-urlencoded | application/json |
|
|
176
|
-
| 測試/正式 URL | 不同 URL | 不同 URL | 不同 URL |
|
|
177
|
-
| 市佔率 | 最高 | 高 | 中等 |
|
|
178
|
-
| 支付方式 | 11 種(含 BNPL、TWQR) | 13 種(含 LINE Pay、Apple Pay) | 8 種(含 AFTEE、iCash) |
|
|
179
|
-
| 特色功能 | 完整文檔、SDK、同時支援發票物流 | MPG 整合、信用卡記憶、多元電子錢包 | RESTful 設計、統一集團背景 |
|
|
180
|
-
| 適用場景 | 高交易量電商、傳統產業、PHP 開發 | 多元支付、會員制、行動 App | 新創公司、API 優先、Node.js 開發 |
|
|
181
|
-
|
|
182
|
-
### ECPay 特性
|
|
183
|
-
- **優勢**: 市佔率最高、穩定性最佳、文檔完整、社群資源豐富、測試帳號可用
|
|
184
|
-
- **加密**: URL Encode + SHA256(參數排序 + HashKey + HashIV)
|
|
185
|
-
- **傳輸**: Form POST,application/x-www-form-urlencoded
|
|
186
|
-
- **特色**: 同時支援金流、發票、物流三合一服務
|
|
187
|
-
|
|
188
|
-
### NewebPay 特性
|
|
189
|
-
- **優勢**: 支援最多支付方式(13 種)、MPG 整合、信用卡記憶功能、完整電子錢包
|
|
190
|
-
- **加密**: AES-256-CBC 加密 TradeInfo,再計算 SHA256 TradeSha
|
|
191
|
-
- **傳輸**: Form POST,雙層加密(AES + SHA256)
|
|
192
|
-
- **特色**: LINE Pay、Apple Pay、Google Pay 原生支援
|
|
193
|
-
|
|
194
|
-
### PAYUNi 特性
|
|
195
|
-
- **優勢**: RESTful JSON API、統一集團背景、AES-GCM 現代加密
|
|
196
|
-
- **加密**: AES-256-GCM 加密 + SHA256 簽章
|
|
197
|
-
- **傳輸**: RESTful JSON,application/json
|
|
198
|
-
- **特色**: AFTEE 先享後付、iCash 愛金卡(獨家)
|
|
199
|
-
|
|
200
|
-
## 開發實作步驟
|
|
201
|
-
|
|
202
|
-
### 1. 服務實作架構
|
|
203
|
-
|
|
204
|
-
創建服務時遵循以下結構:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
// lib/services/payment-provider.ts - 介面定義
|
|
208
|
-
export interface PaymentService {
|
|
209
|
-
createOrder(userId: string, data: PaymentOrderData): Promise<PaymentOrderResponse>
|
|
210
|
-
queryOrder(userId: string, merchantTradeNo: string): Promise<PaymentQueryResponse>
|
|
211
|
-
refundOrder(userId: string, tradeNo: string, amount: number): Promise<PaymentRefundResponse>
|
|
212
|
-
createPeriodic(userId: string, data: PeriodicPaymentData): Promise<PeriodicResponse>
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// lib/services/{provider}-payment-service.ts - 各服務商實作
|
|
216
|
-
export class ECPayPaymentService implements PaymentService {
|
|
217
|
-
private generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
|
|
218
|
-
// SHA256 簽章實作
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async createOrder(userId: string, data: PaymentOrderData) {
|
|
222
|
-
// 1. 取得使用者設定
|
|
223
|
-
// 2. 準備 API 資料
|
|
224
|
-
// 3. 計算 CheckMacValue
|
|
225
|
-
// 4. 生成表單 HTML
|
|
226
|
-
// 5. 回傳標準格式
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### 2. 加密實作
|
|
232
|
-
|
|
233
|
-
**綠界 (ECPay) - SHA256 簽章:**
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
import crypto from 'crypto'
|
|
237
|
-
|
|
238
|
-
function generateECPayCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
|
|
239
|
-
// 1. 移除 CheckMacValue 本身
|
|
240
|
-
const { CheckMacValue, ...cleanParams } = params
|
|
241
|
-
|
|
242
|
-
// 2. 依照 key 排序(字母順序)
|
|
243
|
-
const sortedKeys = Object.keys(cleanParams).sort()
|
|
244
|
-
|
|
245
|
-
// 3. 組合參數字串: key1=value1&key2=value2
|
|
246
|
-
const paramString = sortedKeys
|
|
247
|
-
.map(key => `${key}=${cleanParams[key]}`)
|
|
248
|
-
.join('&')
|
|
249
|
-
|
|
250
|
-
// 4. 前後加上 HashKey 和 HashIV
|
|
251
|
-
const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
|
|
252
|
-
|
|
253
|
-
// 5. URL Encode (lowercase)
|
|
254
|
-
const encoded = encodeURIComponent(rawString).toLowerCase()
|
|
255
|
-
|
|
256
|
-
// 6. SHA256 雜湊
|
|
257
|
-
const hash = crypto.createHash('sha256').update(encoded).digest('hex')
|
|
258
|
-
|
|
259
|
-
// 7. 轉大寫
|
|
260
|
-
return hash.toUpperCase()
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**藍新 (NewebPay) - AES-256-CBC 雙層加密:**
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string): {
|
|
268
|
-
TradeInfo: string,
|
|
269
|
-
TradeSha: string
|
|
270
|
-
} {
|
|
271
|
-
// 1. 轉換為查詢字串
|
|
272
|
-
const queryString = new URLSearchParams(data).toString()
|
|
273
|
-
|
|
274
|
-
// 2. AES-256-CBC 加密
|
|
275
|
-
const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
|
|
276
|
-
cipher.setAutoPadding(true)
|
|
277
|
-
let encrypted = cipher.update(queryString, 'utf8', 'hex')
|
|
278
|
-
encrypted += cipher.final('hex')
|
|
279
|
-
|
|
280
|
-
// 3. 計算 SHA256
|
|
281
|
-
const tradeSha = crypto
|
|
282
|
-
.createHash('sha256')
|
|
283
|
-
.update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
|
|
284
|
-
.digest('hex')
|
|
285
|
-
.toUpperCase()
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
TradeInfo: encrypted,
|
|
289
|
-
TradeSha: tradeSha
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function decryptNewebPay(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
|
|
294
|
-
const decipher = crypto.createDecipheriv('aes-256-cbc', hashKey, hashIV)
|
|
295
|
-
decipher.setAutoPadding(true)
|
|
296
|
-
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
|
297
|
-
decrypted += decipher.final('utf8')
|
|
298
|
-
|
|
299
|
-
return Object.fromEntries(new URLSearchParams(decrypted))
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**統一 (PAYUNi) - AES-256-GCM 加密:**
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string): {
|
|
307
|
-
EncryptInfo: string,
|
|
308
|
-
HashInfo: string
|
|
309
|
-
} {
|
|
310
|
-
// 1. JSON 字串化
|
|
311
|
-
const jsonString = JSON.stringify(data)
|
|
312
|
-
|
|
313
|
-
// 2. AES-256-GCM 加密
|
|
314
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
|
|
315
|
-
let encrypted = cipher.update(jsonString, 'utf8', 'hex')
|
|
316
|
-
encrypted += cipher.final('hex')
|
|
317
|
-
|
|
318
|
-
// 3. 取得 Auth Tag (16 bytes)
|
|
319
|
-
const authTag = cipher.getAuthTag().toString('hex')
|
|
320
|
-
|
|
321
|
-
// 4. 組合加密資料 (encrypted + tag)
|
|
322
|
-
const encryptInfo = encrypted + authTag
|
|
323
|
-
|
|
324
|
-
// 5. SHA256 簽章
|
|
325
|
-
const hashInfo = crypto
|
|
326
|
-
.createHash('sha256')
|
|
327
|
-
.update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
|
|
328
|
-
.digest('hex')
|
|
329
|
-
.toUpperCase()
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
EncryptInfo: encryptInfo,
|
|
333
|
-
HashInfo: hashInfo
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function decryptPAYUNi(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
|
|
338
|
-
// 1. 分離加密內容和 Auth Tag (最後 32 個字元 = 16 bytes hex)
|
|
339
|
-
const encryptedContent = encryptedData.slice(0, -32)
|
|
340
|
-
const authTag = Buffer.from(encryptedData.slice(-32), 'hex')
|
|
341
|
-
|
|
342
|
-
// 2. AES-256-GCM 解密
|
|
343
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', hashKey, hashIV)
|
|
344
|
-
decipher.setAuthTag(authTag)
|
|
345
|
-
let decrypted = decipher.update(encryptedContent, 'hex', 'utf8')
|
|
346
|
-
decrypted += decipher.final('utf8')
|
|
347
|
-
|
|
348
|
-
return JSON.parse(decrypted)
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### 3. 訂單建立流程
|
|
353
|
-
|
|
354
|
-
**關鍵:各服務商都使用 Form POST 導向付款頁**
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
// 後端產生付款表單
|
|
358
|
-
async function createPaymentOrder(provider: string, orderData: OrderData) {
|
|
359
|
-
const service = PaymentServiceFactory.getService(provider)
|
|
360
|
-
const result = await service.createOrder(userId, orderData)
|
|
361
|
-
|
|
362
|
-
// 儲存訂單資訊
|
|
363
|
-
await prisma.order.update({
|
|
364
|
-
where: { id: orderId },
|
|
365
|
-
data: {
|
|
366
|
-
merchantTradeNo: result.merchantTradeNo,
|
|
367
|
-
paymentProvider: provider, // **重要**:儲存使用的服務商
|
|
368
|
-
paymentMethod: orderData.paymentMethod,
|
|
369
|
-
status: 'PENDING'
|
|
370
|
-
}
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
// 回傳表單資料
|
|
374
|
-
return {
|
|
375
|
-
type: 'form',
|
|
376
|
-
action: result.formAction,
|
|
377
|
-
method: result.formMethod,
|
|
378
|
-
params: result.formParams
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// 前端提交表單
|
|
383
|
-
function submitPaymentForm(formData: PaymentFormData) {
|
|
384
|
-
const form = document.createElement('form')
|
|
385
|
-
form.method = formData.method
|
|
386
|
-
form.action = formData.action
|
|
387
|
-
form.target = '_self' // 整頁跳轉
|
|
388
|
-
|
|
389
|
-
// 添加隱藏欄位
|
|
390
|
-
Object.entries(formData.params).forEach(([key, value]) => {
|
|
391
|
-
const input = document.createElement('input')
|
|
392
|
-
input.type = 'hidden'
|
|
393
|
-
input.name = key
|
|
394
|
-
input.value = value
|
|
395
|
-
form.appendChild(input)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
document.body.appendChild(form)
|
|
399
|
-
form.submit()
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### 4. 付款通知處理
|
|
404
|
-
|
|
405
|
-
**ReturnURL 處理(付款完成後):**
|
|
406
|
-
|
|
407
|
-
```typescript
|
|
408
|
-
// app/api/payment/callback/route.ts
|
|
409
|
-
export async function POST(request: Request) {
|
|
410
|
-
const formData = await request.formData()
|
|
411
|
-
const params = Object.fromEntries(formData)
|
|
412
|
-
|
|
413
|
-
// 1. 偵測服務商(根據欄位判斷)
|
|
414
|
-
const provider = detectProvider(params)
|
|
415
|
-
const service = PaymentServiceFactory.getService(provider)
|
|
416
|
-
|
|
417
|
-
// 2. 驗證簽章
|
|
418
|
-
const isValid = service.verifyCallback(params)
|
|
419
|
-
if (!isValid) {
|
|
420
|
-
return new Response('0|CheckMacValue Error', { status: 400 })
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// 3. 更新訂單狀態
|
|
424
|
-
const merchantTradeNo = params.MerchantTradeNo || params.MerchantOrderNo
|
|
425
|
-
await prisma.order.update({
|
|
426
|
-
where: { merchantTradeNo },
|
|
427
|
-
data: {
|
|
428
|
-
status: params.RtnCode === '1' ? 'PAID' : 'FAILED',
|
|
429
|
-
paidAt: new Date(),
|
|
430
|
-
tradeNo: params.TradeNo, // **重要**:儲存金流商訂單號(退款時需要)
|
|
431
|
-
paymentDetails: params
|
|
432
|
-
}
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
// 4. 回應固定格式
|
|
436
|
-
return new Response('1|OK') // ECPay/NewebPay 要求
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### 5. 查詢訂單
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
async function queryPaymentOrder(merchantTradeNo: string) {
|
|
444
|
-
// 1. 查詢訂單取得服務商
|
|
445
|
-
const order = await prisma.order.findUnique({
|
|
446
|
-
where: { merchantTradeNo }
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
// 2. 使用訂單記錄的服務商查詢
|
|
450
|
-
const service = PaymentServiceFactory.getService(order.paymentProvider)
|
|
451
|
-
const result = await service.queryOrder(order.userId, merchantTradeNo)
|
|
452
|
-
|
|
453
|
-
return result
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
### 6. 退款處理
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
async function refundPaymentOrder(merchantTradeNo: string, refundAmount: number) {
|
|
461
|
-
const order = await prisma.order.findUnique({
|
|
462
|
-
where: { merchantTradeNo }
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
// **重要**:使用開立時的服務商和 TradeNo
|
|
466
|
-
const service = PaymentServiceFactory.getService(order.paymentProvider)
|
|
467
|
-
const result = await service.refundOrder(
|
|
468
|
-
order.userId,
|
|
469
|
-
order.tradeNo, // 金流商訂單號
|
|
470
|
-
refundAmount
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
// 更新訂單狀態
|
|
474
|
-
await prisma.order.update({
|
|
475
|
-
where: { merchantTradeNo },
|
|
476
|
-
data: {
|
|
477
|
-
status: 'REFUNDED',
|
|
478
|
-
refundAmount,
|
|
479
|
-
refundedAt: new Date()
|
|
480
|
-
}
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
return result
|
|
484
|
-
}
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
## 常見問題排除
|
|
488
|
-
|
|
489
|
-
### 問題 1: CheckMacValue 驗證失敗
|
|
490
|
-
|
|
491
|
-
**錯誤訊息:** ECPay 回傳 `10100058`,NewebPay 回傳 `CheckValue Error`
|
|
492
|
-
|
|
493
|
-
**常見原因:**
|
|
494
|
-
1. 參數排序錯誤(必須按照字母順序)
|
|
495
|
-
2. URL Encode 不正確(ECPay 需要 lowercase)
|
|
496
|
-
3. 編碼問題(UTF-8)
|
|
497
|
-
4. 忘記移除 CheckMacValue 本身
|
|
498
|
-
|
|
499
|
-
**解決方案:**
|
|
500
|
-
```typescript
|
|
501
|
-
//
|
|
502
|
-
function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
|
|
503
|
-
// 1. 移除 CheckMacValue
|
|
504
|
-
const { CheckMacValue, ...cleanParams } = params
|
|
505
|
-
|
|
506
|
-
// 2. 排序
|
|
507
|
-
const sortedKeys = Object.keys(cleanParams).sort()
|
|
508
|
-
|
|
509
|
-
// 3. 組合字串
|
|
510
|
-
const paramString = sortedKeys.map(k => `${k}=${cleanParams[k]}`).join('&')
|
|
511
|
-
|
|
512
|
-
// 4. 加上 HashKey/HashIV
|
|
513
|
-
const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
|
|
514
|
-
|
|
515
|
-
// 5. URL Encode (lowercase for ECPay)
|
|
516
|
-
const encoded = encodeURIComponent(rawString).toLowerCase()
|
|
517
|
-
|
|
518
|
-
// 6. SHA256
|
|
519
|
-
return crypto.createHash('sha256').update(encoded).digest('hex').toUpperCase()
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
//
|
|
523
|
-
const paramString = Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&')
|
|
524
|
-
|
|
525
|
-
//
|
|
526
|
-
const encoded = encodeURIComponent(rawString) // 應該用 toLowerCase()
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### 問題 2: 訂單編號重複
|
|
530
|
-
|
|
531
|
-
**錯誤訊息:** ECPay `10100003`,NewebPay/PAYUNi 訂單已存在
|
|
532
|
-
|
|
533
|
-
**原因:** 使用相同的 MerchantTradeNo
|
|
534
|
-
|
|
535
|
-
**解決方案:**
|
|
536
|
-
```typescript
|
|
537
|
-
//
|
|
538
|
-
function generateMerchantTradeNo(prefix: string = 'ORD') {
|
|
539
|
-
const timestamp = Date.now()
|
|
540
|
-
const random = Math.random().toString(36).substring(2, 8).toUpperCase()
|
|
541
|
-
return `${prefix}${timestamp}${random}`.substring(0, 20) // ECPay 限制 20 字元
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// 範例輸出: ORD1706512345A7B2
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
### 問題 3: 金額計算錯誤
|
|
548
|
-
|
|
549
|
-
**錯誤訊息:** 回傳金額驗算錯誤
|
|
550
|
-
|
|
551
|
-
**常見原因:**
|
|
552
|
-
1. 金額包含小數
|
|
553
|
-
2. 金額為負數或 0
|
|
554
|
-
3. 商品金額總和不等於訂單金額
|
|
555
|
-
|
|
556
|
-
**解決方案:**
|
|
557
|
-
```typescript
|
|
558
|
-
//
|
|
559
|
-
function validateAmount(amount: number): number {
|
|
560
|
-
if (amount <= 0) {
|
|
561
|
-
throw new Error('金額必須大於 0')
|
|
562
|
-
}
|
|
563
|
-
return Math.round(amount) // 移除小數
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
//
|
|
567
|
-
function validateItemsAmount(items: Item[], totalAmount: number) {
|
|
568
|
-
const itemsSum = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
|
569
|
-
if (Math.round(itemsSum) !== Math.round(totalAmount)) {
|
|
570
|
-
throw new Error(`商品金額總和 ${itemsSum} 不等於訂單金額 ${totalAmount}`)
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
### 問題 4: 收不到付款通知
|
|
576
|
-
|
|
577
|
-
**原因:**
|
|
578
|
-
1. ReturnURL 不是 HTTPS
|
|
579
|
-
2. 防火牆阻擋
|
|
580
|
-
3. 回應格式錯誤
|
|
581
|
-
|
|
582
|
-
**解決方案:**
|
|
583
|
-
```typescript
|
|
584
|
-
//
|
|
585
|
-
const returnURL = 'https://yourdomain.com/api/payment/callback' // 必須 HTTPS
|
|
586
|
-
|
|
587
|
-
//
|
|
588
|
-
export async function POST(request: Request) {
|
|
589
|
-
// ... 處理邏輯
|
|
590
|
-
|
|
591
|
-
// ECPay/NewebPay 需要回應 "1|OK"
|
|
592
|
-
return new Response('1|OK', {
|
|
593
|
-
status: 200,
|
|
594
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
595
|
-
})
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
return Response.json({ success: true }) // 不正確
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
### 問題 5: 測試卡無法付款
|
|
603
|
-
|
|
604
|
-
**原因:** 使用真實卡號或測試卡格式錯誤
|
|
605
|
-
|
|
606
|
-
**解決方案:**
|
|
607
|
-
```
|
|
608
|
-
# ECPay 測試卡
|
|
609
|
-
卡號: 4311-9522-2222-2222
|
|
610
|
-
有效期: 任意未來月年
|
|
611
|
-
CVV: 任意 3 碼
|
|
612
|
-
|
|
613
|
-
# NewebPay 測試卡
|
|
614
|
-
卡號: 4000-2211-1111-1111
|
|
615
|
-
有效期: 任意未來月年
|
|
616
|
-
CVV: 任意 3 碼
|
|
617
|
-
|
|
618
|
-
# PAYUNi 測試卡
|
|
619
|
-
請至後台查詢官方測試卡號
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
### 問題 6: AES 加密失敗
|
|
623
|
-
|
|
624
|
-
**NewebPay AES-256-CBC 加密錯誤:**
|
|
625
|
-
|
|
626
|
-
```typescript
|
|
627
|
-
//
|
|
628
|
-
const hashKey = 'your32BytesHashKeyHere123456' // 必須 32 bytes
|
|
629
|
-
const hashIV = 'your16BytesIV123' // 必須 16 bytes
|
|
630
|
-
|
|
631
|
-
//
|
|
632
|
-
const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
|
|
633
|
-
cipher.setAutoPadding(true) // PKCS7 padding
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
**PAYUNi AES-256-GCM 加密錯誤:**
|
|
637
|
-
|
|
638
|
-
```typescript
|
|
639
|
-
//
|
|
640
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
|
|
641
|
-
let encrypted = cipher.update(jsonString, 'utf8', 'hex')
|
|
642
|
-
encrypted += cipher.final('hex')
|
|
643
|
-
const authTag = cipher.getAuthTag().toString('hex') // **重要**
|
|
644
|
-
const encryptInfo = encrypted + authTag // 總長度 = encrypted + 32 chars (16 bytes hex)
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### 問題 7: ATM 虛擬帳號未產生
|
|
648
|
-
|
|
649
|
-
**原因:**
|
|
650
|
-
1. 未設定 ChoosePayment=ATM
|
|
651
|
-
2. ExpireDate 格式錯誤
|
|
652
|
-
3. ExpireDate 超過範圍(3-60 天)
|
|
653
|
-
|
|
654
|
-
**解決方案:**
|
|
655
|
-
```typescript
|
|
656
|
-
//
|
|
657
|
-
const params = {
|
|
658
|
-
ChoosePayment: 'ATM',
|
|
659
|
-
ExpireDate: 3, // 3-60 天
|
|
660
|
-
PaymentInfoURL: 'https://yourdomain.com/api/payment/atm-info', // 接收帳號通知
|
|
661
|
-
// ...
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
//
|
|
665
|
-
const params = {
|
|
666
|
-
VACC: 1, // 啟用 ATM
|
|
667
|
-
ExpireDate: '2024-12-31', // yyyy-MM-dd 格式
|
|
668
|
-
// ...
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
### 問題 8: 定期定額建立失敗
|
|
673
|
-
|
|
674
|
-
**原因:** 週期參數不完整
|
|
675
|
-
|
|
676
|
-
**解決方案:**
|
|
677
|
-
```typescript
|
|
678
|
-
//
|
|
679
|
-
const periodicParams = {
|
|
680
|
-
PeriodAmount: 1000, // 扣款金額
|
|
681
|
-
PeriodType: 'M', // D=日, M=月, Y=年
|
|
682
|
-
Frequency: 1, // 頻率(每 1 個週期)
|
|
683
|
-
ExecTimes: 12, // 執行次數(12 次)
|
|
684
|
-
PeriodReturnURL: 'https://yourdomain.com/api/payment/periodic-callback',
|
|
685
|
-
// ...
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
const periodicParams = {
|
|
690
|
-
PeriodAmt: 1000,
|
|
691
|
-
PeriodType: 'M',
|
|
692
|
-
PeriodPoint: '01', // 每月 1 號扣款
|
|
693
|
-
PeriodTimes: 12,
|
|
694
|
-
// ...
|
|
695
|
-
}
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
### 問題 9: 跨域問題
|
|
699
|
-
|
|
700
|
-
**錯誤:** 前端導向付款頁失敗
|
|
701
|
-
|
|
702
|
-
**原因:** 使用 AJAX 或 Fetch,受 CORS 限制
|
|
703
|
-
|
|
704
|
-
**解決方案:**
|
|
705
|
-
```typescript
|
|
706
|
-
//
|
|
707
|
-
fetch(paymentUrl, { method: 'POST', body: formData }) // 會被 CORS 阻擋
|
|
708
|
-
|
|
709
|
-
//
|
|
710
|
-
function submitPaymentForm(action: string, params: Record<string, string>) {
|
|
711
|
-
const form = document.createElement('form')
|
|
712
|
-
form.method = 'POST'
|
|
713
|
-
form.action = action
|
|
714
|
-
form.target = '_self' // 整頁跳轉
|
|
715
|
-
|
|
716
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
717
|
-
const input = document.createElement('input')
|
|
718
|
-
input.type = 'hidden'
|
|
719
|
-
input.name = key
|
|
720
|
-
input.value = value
|
|
721
|
-
form.appendChild(input)
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
document.body.appendChild(form)
|
|
725
|
-
form.submit() // 直接提交
|
|
726
|
-
}
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
### 問題 10: 商店代號錯誤
|
|
730
|
-
|
|
731
|
-
**錯誤訊息:** 回傳商店不存在
|
|
732
|
-
|
|
733
|
-
**原因:**
|
|
734
|
-
1. MerchantID 錯誤
|
|
735
|
-
2. 測試/正式環境混用
|
|
736
|
-
|
|
737
|
-
**解決方案:**
|
|
738
|
-
```typescript
|
|
739
|
-
//
|
|
740
|
-
const config = {
|
|
741
|
-
merchantID: process.env.NODE_ENV === 'production'
|
|
742
|
-
? process.env.ECPAY_MERCHANT_ID_PROD
|
|
743
|
-
: process.env.ECPAY_MERCHANT_ID_TEST,
|
|
744
|
-
apiUrl: process.env.NODE_ENV === 'production'
|
|
745
|
-
? 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
746
|
-
: 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
747
|
-
}
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
## 測試帳號
|
|
751
|
-
|
|
752
|
-
### 綠界測試環境
|
|
753
|
-
```
|
|
754
|
-
MerchantID: 3002607
|
|
755
|
-
HashKey: pwFHCqoQZGmho4w6
|
|
756
|
-
HashIV: EkRm7iFT261dpevs
|
|
757
|
-
測試 URL: https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5
|
|
758
|
-
測試卡號: 4311-9522-2222-2222
|
|
759
|
-
後台: https://vendor-stage.ecpay.com.tw/
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
### 藍新測試環境
|
|
763
|
-
```
|
|
764
|
-
MerchantID: 請至後台申請
|
|
765
|
-
HashKey: 請至後台申請(32 bytes)
|
|
766
|
-
HashIV: 請至後台申請(16 bytes)
|
|
767
|
-
測試 URL: https://ccore.newebpay.com/MPG/mpg_gateway
|
|
768
|
-
測試卡號: 4000-2211-1111-1111
|
|
769
|
-
後台: https://cwww.newebpay.com/
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
### 統一測試環境
|
|
773
|
-
```
|
|
774
|
-
MerchantID: 請至後台申請
|
|
775
|
-
HashKey: 請至後台申請
|
|
776
|
-
HashIV: 請至後台申請
|
|
777
|
-
測試 URL: https://sandbox-api.payuni.com.tw/api/upp
|
|
778
|
-
後台: https://sandbox.payuni.com.tw/
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
## 開發檢查清單
|
|
782
|
-
|
|
783
|
-
使用此清單確保實作完整:
|
|
784
|
-
|
|
785
|
-
### 基礎設定
|
|
786
|
-
- [ ] 實作 `PaymentService` 介面
|
|
787
|
-
- [ ] 實作各服務商加密機制(SHA256 / AES-CBC / AES-GCM)
|
|
788
|
-
- [ ] 設定環境變數(測試/正式)
|
|
789
|
-
- [ ] 配置 ReturnURL(HTTPS)
|
|
790
|
-
- [ ] 配置 OrderResultURL(付款完成導向頁)
|
|
791
|
-
|
|
792
|
-
### 訂單處理
|
|
793
|
-
- [ ] 儲存 `paymentProvider` 欄位(查詢/退款時需要)
|
|
794
|
-
- [ ] 儲存 `merchantTradeNo`(唯一訂單編號)
|
|
795
|
-
- [ ] 儲存 `tradeNo`(金流商訂單號,退款時需要)
|
|
796
|
-
- [ ] 金額驗證(正整數、最小金額)
|
|
797
|
-
- [ ] 商品金額總和驗證
|
|
798
|
-
|
|
799
|
-
### 付款方式
|
|
800
|
-
- [ ] 支援信用卡一次付清
|
|
801
|
-
- [ ] 支援 ATM 虛擬帳號(可選)
|
|
802
|
-
- [ ] 支援超商代碼/條碼(可選)
|
|
803
|
-
- [ ] 支援電子錢包(可選)
|
|
804
|
-
- [ ] 支援信用卡分期(可選)
|
|
805
|
-
- [ ] 支援定期定額(可選)
|
|
806
|
-
|
|
807
|
-
### 回呼處理
|
|
808
|
-
- [ ] 驗證 CheckMacValue / TradeSha / HashInfo
|
|
809
|
-
- [ ] 更新訂單狀態
|
|
810
|
-
- [ ] 回應 "1|OK" 格式
|
|
811
|
-
- [ ] 防止重複通知處理(冪等性)
|
|
812
|
-
|
|
813
|
-
### 查詢退款
|
|
814
|
-
- [ ] 實作訂單查詢 API
|
|
815
|
-
- [ ] 實作退款 API
|
|
816
|
-
- [ ] 處理部分退款邏輯
|
|
817
|
-
- [ ] 檢查退款期限
|
|
818
|
-
|
|
819
|
-
### 錯誤處理
|
|
820
|
-
- [ ] 實作錯誤處理與 logger
|
|
821
|
-
- [ ] 記錄完整請求/回應(除敏感資訊)
|
|
822
|
-
- [ ] 處理加密錯誤
|
|
823
|
-
- [ ] 處理網路逾時
|
|
824
|
-
|
|
825
|
-
### 測試驗證
|
|
826
|
-
- [ ] 測試環境驗證
|
|
827
|
-
- [ ] 使用官方測試卡測試
|
|
828
|
-
- [ ] 測試付款通知接收
|
|
829
|
-
- [ ] 測試查詢功能
|
|
830
|
-
- [ ] 測試退款功能
|
|
831
|
-
|
|
832
|
-
## 新增服務商步驟
|
|
833
|
-
|
|
834
|
-
1. 在 `lib/services/` 建立 `{provider}-payment-service.ts`
|
|
835
|
-
2. 實作 `PaymentService` 介面的所有方法
|
|
836
|
-
3. 在 `PaymentServiceFactory` 註冊新服務商
|
|
837
|
-
4. 在 `prisma/schema.prisma` 的 `PaymentProvider` enum 新增選項
|
|
838
|
-
5. 執行 `prisma migrate` 或 `prisma db push`
|
|
839
|
-
6. 更新前端設定頁面
|
|
840
|
-
7. 撰寫單元測試
|
|
841
|
-
8. 更新文檔
|
|
842
|
-
|
|
843
|
-
## 參考資料
|
|
844
|
-
|
|
845
|
-
詳細 API 規格請查看 `references/` 目錄:
|
|
846
|
-
- [綠界 ECPay Payment API 規格](./references/ECPAY_PAYMENT_REFERENCE.md)
|
|
847
|
-
- [藍新 NewebPay Payment API 規格](./references/NEWEBPAY_PAYMENT_REFERENCE.md)
|
|
848
|
-
- [統一 PAYUNi Payment API 規格](./references/PAYUNI_PAYMENT_REFERENCE.md)
|
|
849
|
-
|
|
850
|
-
官方文檔:
|
|
851
|
-
- ECPay: https://developers.ecpay.com.tw/
|
|
852
|
-
- NewebPay: https://www.newebpay.com/website/Page/content/download_api
|
|
853
|
-
- PAYUNi: https://www.payuni.com.tw/docs/
|
|
854
|
-
|
|
855
|
-
---
|
|
856
|
-
|
|
857
|
-
最後更新:2026/01/29
|
|
1
|
+
---
|
|
2
|
+
name: taiwan-payment
|
|
3
|
+
description: Taiwan Payment API integration specialist for ECPay, NewebPay, and PAYUNi payment gateways. Use when developing payment systems, implementing credit card, ATM, CVS, or e-wallet payments, or working with Taiwan payment gateway APIs. Handles encryption (SHA256, AES-256-CBC, AES-256-GCM), API requests, and service provider differences.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Taiwan Payment Development Skill
|
|
8
|
+
|
|
9
|
+
> 此技能涵蓋台灣金流 API 整合開發,包含綠界科技 (ECPay)、藍新金流 (NewebPay)、統一金流 (PAYUNi) 三家服務商。
|
|
10
|
+
|
|
11
|
+
## 快速導覽
|
|
12
|
+
|
|
13
|
+
### 相關文件
|
|
14
|
+
使用此技能時,請參考專案中的 API 規格文件:
|
|
15
|
+
- `references/ECPAY_PAYMENT_REFERENCE.md` - 綠界金流 API 規格
|
|
16
|
+
- `references/NEWEBPAY_PAYMENT_REFERENCE.md` - 藍新金流 API 規格
|
|
17
|
+
- `references/PAYUNI_PAYMENT_REFERENCE.md` - 統一金流 API 規格
|
|
18
|
+
- [EXAMPLES.md](EXAMPLES.md) - 程式碼範例集
|
|
19
|
+
|
|
20
|
+
### 智能工具
|
|
21
|
+
- `scripts/search.py` - BM25 搜索引擎(查詢 API、錯誤碼、欄位映射、付款方式)
|
|
22
|
+
- `scripts/recommend.py` - 金流服務商推薦系統
|
|
23
|
+
- `scripts/test_payment.py` - 付款測試工具
|
|
24
|
+
- `data/` - CSV 數據檔(providers, operations, error-codes, field-mappings, payment-methods, troubleshooting, reasoning)
|
|
25
|
+
|
|
26
|
+
### 何時使用此技能
|
|
27
|
+
- 開發線上金流付款功能
|
|
28
|
+
- 整合台灣金流服務商 API
|
|
29
|
+
- 實作信用卡、ATM、超商、電子錢包等付款方式
|
|
30
|
+
- 處理訂單查詢、退款、定期定額扣款
|
|
31
|
+
- 處理加密簽章(SHA256、AES-256-CBC、AES-256-GCM)
|
|
32
|
+
- 解決金流 API 整合問題
|
|
33
|
+
|
|
34
|
+
## 智能搜索與推薦
|
|
35
|
+
|
|
36
|
+
### 搜索引擎 (search.py)
|
|
37
|
+
|
|
38
|
+
使用 BM25 算法在資料庫中搜索相關資訊:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 搜索服務商
|
|
42
|
+
python scripts/search.py "ecpay" --domain provider
|
|
43
|
+
|
|
44
|
+
# 搜索錯誤碼
|
|
45
|
+
python scripts/search.py "10100058" --domain error
|
|
46
|
+
|
|
47
|
+
# 搜索欄位映射
|
|
48
|
+
python scripts/search.py "MerchantTradeNo" --domain field
|
|
49
|
+
|
|
50
|
+
# 搜索付款方式
|
|
51
|
+
python scripts/search.py "信用卡" --domain payment_method
|
|
52
|
+
|
|
53
|
+
# 搜索疑難排解
|
|
54
|
+
python scripts/search.py "CheckMacValue 錯誤" --domain troubleshoot
|
|
55
|
+
|
|
56
|
+
# 全域搜索
|
|
57
|
+
python scripts/search.py "金額計算" --domain all
|
|
58
|
+
|
|
59
|
+
# JSON 輸出
|
|
60
|
+
python scripts/search.py "ATM" --format json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**搜索域:**
|
|
64
|
+
| 域 | 說明 | CSV 檔案 |
|
|
65
|
+
|-----|------|----------|
|
|
66
|
+
| `provider` | 服務商比較 | providers.csv |
|
|
67
|
+
| `operation` | API 操作端點 | operations.csv |
|
|
68
|
+
| `error` | 錯誤碼查詢 | error-codes.csv |
|
|
69
|
+
| `field` | 欄位映射 | field-mappings.csv |
|
|
70
|
+
| `payment_method` | 付款方式 | payment-methods.csv |
|
|
71
|
+
| `troubleshoot` | 疑難排解 | troubleshooting.csv |
|
|
72
|
+
| `reasoning` | 推薦決策規則 | reasoning.csv |
|
|
73
|
+
|
|
74
|
+
**域自動偵測:**
|
|
75
|
+
搜索引擎會自動偵測查詢內容並選擇最適合的域:
|
|
76
|
+
- 錯誤碼格式(如 "10100058")→ error
|
|
77
|
+
- 服務商名稱(如 "ECPay")→ provider
|
|
78
|
+
- 付款方式(如 "信用卡"、"ATM")→ payment_method
|
|
79
|
+
- API 欄位(如 "MerchantID")→ field
|
|
80
|
+
|
|
81
|
+
### 推薦系統 (recommend.py)
|
|
82
|
+
|
|
83
|
+
根據需求自動推薦最適合的金流服務商:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 高交易量電商
|
|
87
|
+
python scripts/recommend.py "高交易量 穩定 電商"
|
|
88
|
+
# → 推薦 ECPay (市佔率高,穩定性佳)
|
|
89
|
+
|
|
90
|
+
# 多元支付需求
|
|
91
|
+
python scripts/recommend.py "多元支付 LINE Pay Apple Pay"
|
|
92
|
+
# → 推薦 NewebPay (支援 13 種付款方式)
|
|
93
|
+
|
|
94
|
+
# API 設計優先
|
|
95
|
+
python scripts/recommend.py "API RESTful JSON"
|
|
96
|
+
# → 推薦 PAYUNi (RESTful JSON API)
|
|
97
|
+
|
|
98
|
+
# JSON 輸出
|
|
99
|
+
python scripts/recommend.py "新創公司 快速整合" --format json
|
|
100
|
+
|
|
101
|
+
# 簡單文字輸出
|
|
102
|
+
python scripts/recommend.py "會員制 定期扣款" --format simple
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**推薦關鍵字:**
|
|
106
|
+
- **ECPay**: 穩定、市佔、高交易量、電商、ATM、超商、定期、訂閱、分期、發票、物流
|
|
107
|
+
- **NewebPay**: 多元、支付方式、電子錢包、LINE、行動、記憶、會員、跨境
|
|
108
|
+
- **PAYUNi**: API、JSON、RESTful、統一、新創
|
|
109
|
+
|
|
110
|
+
**反模式警告:**
|
|
111
|
+
推薦系統會自動提示不建議的場景:
|
|
112
|
+
- ECPay: 無技術資源、極簡需求
|
|
113
|
+
- NewebPay: 簡單 API、單一支付
|
|
114
|
+
- PAYUNi: 大型專案、完整文檔
|
|
115
|
+
|
|
116
|
+
### 付款測試工具 (test_payment.py)
|
|
117
|
+
|
|
118
|
+
快速測試金流服務商連線:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 測試 ECPay 連線
|
|
122
|
+
python scripts/test_payment.py ecpay
|
|
123
|
+
|
|
124
|
+
# 測試 NewebPay 連線
|
|
125
|
+
python scripts/test_payment.py newebpay
|
|
126
|
+
|
|
127
|
+
# 測試 PAYUNi 連線
|
|
128
|
+
python scripts/test_payment.py payuni
|
|
129
|
+
|
|
130
|
+
# 測試所有服務商
|
|
131
|
+
python scripts/test_payment.py all
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 付款方式說明
|
|
137
|
+
|
|
138
|
+
### 信用卡支付
|
|
139
|
+
- **一次付清**: 最常用的付款方式,2-3 天到帳
|
|
140
|
+
- **信用卡分期**: 3/6/12/18/24/30 期,需最低金額 1000 元
|
|
141
|
+
- **信用卡定期**: 週期扣款,適用訂閱制服務
|
|
142
|
+
- **信用卡紅利**: 紅利折抵功能
|
|
143
|
+
- **銀聯卡**: 需另外申請,支援中國銀聯
|
|
144
|
+
- **美國運通卡**: 需另外申請
|
|
145
|
+
|
|
146
|
+
### 電子錢包
|
|
147
|
+
- **Apple Pay**: 需申請,適合 iOS 用戶
|
|
148
|
+
- **Google Pay**: 需申請,適合 Android 用戶
|
|
149
|
+
- **Samsung Pay**: 需申請,三星手機專用
|
|
150
|
+
- **LINE Pay**: 需申請,LINE 生態系整合
|
|
151
|
+
- **玉山 Wallet**: 玉山銀行電子錢包
|
|
152
|
+
- **台灣 Pay**: 官方行動支付,最高 49,999 元
|
|
153
|
+
|
|
154
|
+
### ATM 轉帳
|
|
155
|
+
- **網路 ATM**: 即時轉帳,最高 49,999 元,1 天到帳
|
|
156
|
+
- **ATM 虛擬帳號**: 產生專屬繳費帳號,1-3 天到帳,最高 49,999 元
|
|
157
|
+
|
|
158
|
+
### 超商支付
|
|
159
|
+
- **超商代碼**: 至四大超商繳費,30-20,000 元,1-3 天到帳
|
|
160
|
+
- **超商條碼**: 產生繳費條碼,20-40,000 元,1-3 天到帳
|
|
161
|
+
|
|
162
|
+
### 其他支付方式
|
|
163
|
+
- **TWQR**: 台灣 Pay QR Code 掃碼支付
|
|
164
|
+
- **BNPL 無卡分期**: 先買後付,50-300,000 元
|
|
165
|
+
- **AFTEE**: PAYUNi 專屬先享後付
|
|
166
|
+
- **iCash**: PAYUNi 專屬愛金卡支付
|
|
167
|
+
- **簡單付支付寶/微信**: 跨境支付(中國市場)
|
|
168
|
+
|
|
169
|
+
## 三家服務商特性比較
|
|
170
|
+
|
|
171
|
+
| 特性 | 綠界 ECPay | 藍新 NewebPay | 統一 PAYUNi |
|
|
172
|
+
|------|-----------|--------------|------------|
|
|
173
|
+
| 加密方式 | URL Encode + SHA256 | AES-256-CBC + SHA256 雙層 | AES-256-GCM + SHA256 |
|
|
174
|
+
| API 風格 | Form POST | Form POST + AES | RESTful JSON |
|
|
175
|
+
| 內容格式 | application/x-www-form-urlencoded | application/x-www-form-urlencoded | application/json |
|
|
176
|
+
| 測試/正式 URL | 不同 URL | 不同 URL | 不同 URL |
|
|
177
|
+
| 市佔率 | 最高 | 高 | 中等 |
|
|
178
|
+
| 支付方式 | 11 種(含 BNPL、TWQR) | 13 種(含 LINE Pay、Apple Pay) | 8 種(含 AFTEE、iCash) |
|
|
179
|
+
| 特色功能 | 完整文檔、SDK、同時支援發票物流 | MPG 整合、信用卡記憶、多元電子錢包 | RESTful 設計、統一集團背景 |
|
|
180
|
+
| 適用場景 | 高交易量電商、傳統產業、PHP 開發 | 多元支付、會員制、行動 App | 新創公司、API 優先、Node.js 開發 |
|
|
181
|
+
|
|
182
|
+
### ECPay 特性
|
|
183
|
+
- **優勢**: 市佔率最高、穩定性最佳、文檔完整、社群資源豐富、測試帳號可用
|
|
184
|
+
- **加密**: URL Encode + SHA256(參數排序 + HashKey + HashIV)
|
|
185
|
+
- **傳輸**: Form POST,application/x-www-form-urlencoded
|
|
186
|
+
- **特色**: 同時支援金流、發票、物流三合一服務
|
|
187
|
+
|
|
188
|
+
### NewebPay 特性
|
|
189
|
+
- **優勢**: 支援最多支付方式(13 種)、MPG 整合、信用卡記憶功能、完整電子錢包
|
|
190
|
+
- **加密**: AES-256-CBC 加密 TradeInfo,再計算 SHA256 TradeSha
|
|
191
|
+
- **傳輸**: Form POST,雙層加密(AES + SHA256)
|
|
192
|
+
- **特色**: LINE Pay、Apple Pay、Google Pay 原生支援
|
|
193
|
+
|
|
194
|
+
### PAYUNi 特性
|
|
195
|
+
- **優勢**: RESTful JSON API、統一集團背景、AES-GCM 現代加密
|
|
196
|
+
- **加密**: AES-256-GCM 加密 + SHA256 簽章
|
|
197
|
+
- **傳輸**: RESTful JSON,application/json
|
|
198
|
+
- **特色**: AFTEE 先享後付、iCash 愛金卡(獨家)
|
|
199
|
+
|
|
200
|
+
## 開發實作步驟
|
|
201
|
+
|
|
202
|
+
### 1. 服務實作架構
|
|
203
|
+
|
|
204
|
+
創建服務時遵循以下結構:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// lib/services/payment-provider.ts - 介面定義
|
|
208
|
+
export interface PaymentService {
|
|
209
|
+
createOrder(userId: string, data: PaymentOrderData): Promise<PaymentOrderResponse>
|
|
210
|
+
queryOrder(userId: string, merchantTradeNo: string): Promise<PaymentQueryResponse>
|
|
211
|
+
refundOrder(userId: string, tradeNo: string, amount: number): Promise<PaymentRefundResponse>
|
|
212
|
+
createPeriodic(userId: string, data: PeriodicPaymentData): Promise<PeriodicResponse>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// lib/services/{provider}-payment-service.ts - 各服務商實作
|
|
216
|
+
export class ECPayPaymentService implements PaymentService {
|
|
217
|
+
private generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
|
|
218
|
+
// SHA256 簽章實作
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async createOrder(userId: string, data: PaymentOrderData) {
|
|
222
|
+
// 1. 取得使用者設定
|
|
223
|
+
// 2. 準備 API 資料
|
|
224
|
+
// 3. 計算 CheckMacValue
|
|
225
|
+
// 4. 生成表單 HTML
|
|
226
|
+
// 5. 回傳標準格式
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 2. 加密實作
|
|
232
|
+
|
|
233
|
+
**綠界 (ECPay) - SHA256 簽章:**
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import crypto from 'crypto'
|
|
237
|
+
|
|
238
|
+
function generateECPayCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
|
|
239
|
+
// 1. 移除 CheckMacValue 本身
|
|
240
|
+
const { CheckMacValue, ...cleanParams } = params
|
|
241
|
+
|
|
242
|
+
// 2. 依照 key 排序(字母順序)
|
|
243
|
+
const sortedKeys = Object.keys(cleanParams).sort()
|
|
244
|
+
|
|
245
|
+
// 3. 組合參數字串: key1=value1&key2=value2
|
|
246
|
+
const paramString = sortedKeys
|
|
247
|
+
.map(key => `${key}=${cleanParams[key]}`)
|
|
248
|
+
.join('&')
|
|
249
|
+
|
|
250
|
+
// 4. 前後加上 HashKey 和 HashIV
|
|
251
|
+
const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
|
|
252
|
+
|
|
253
|
+
// 5. URL Encode (lowercase)
|
|
254
|
+
const encoded = encodeURIComponent(rawString).toLowerCase()
|
|
255
|
+
|
|
256
|
+
// 6. SHA256 雜湊
|
|
257
|
+
const hash = crypto.createHash('sha256').update(encoded).digest('hex')
|
|
258
|
+
|
|
259
|
+
// 7. 轉大寫
|
|
260
|
+
return hash.toUpperCase()
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**藍新 (NewebPay) - AES-256-CBC 雙層加密:**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string): {
|
|
268
|
+
TradeInfo: string,
|
|
269
|
+
TradeSha: string
|
|
270
|
+
} {
|
|
271
|
+
// 1. 轉換為查詢字串
|
|
272
|
+
const queryString = new URLSearchParams(data).toString()
|
|
273
|
+
|
|
274
|
+
// 2. AES-256-CBC 加密
|
|
275
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
|
|
276
|
+
cipher.setAutoPadding(true)
|
|
277
|
+
let encrypted = cipher.update(queryString, 'utf8', 'hex')
|
|
278
|
+
encrypted += cipher.final('hex')
|
|
279
|
+
|
|
280
|
+
// 3. 計算 SHA256
|
|
281
|
+
const tradeSha = crypto
|
|
282
|
+
.createHash('sha256')
|
|
283
|
+
.update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
|
|
284
|
+
.digest('hex')
|
|
285
|
+
.toUpperCase()
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
TradeInfo: encrypted,
|
|
289
|
+
TradeSha: tradeSha
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function decryptNewebPay(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
|
|
294
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', hashKey, hashIV)
|
|
295
|
+
decipher.setAutoPadding(true)
|
|
296
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
|
297
|
+
decrypted += decipher.final('utf8')
|
|
298
|
+
|
|
299
|
+
return Object.fromEntries(new URLSearchParams(decrypted))
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**統一 (PAYUNi) - AES-256-GCM 加密:**
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string): {
|
|
307
|
+
EncryptInfo: string,
|
|
308
|
+
HashInfo: string
|
|
309
|
+
} {
|
|
310
|
+
// 1. JSON 字串化
|
|
311
|
+
const jsonString = JSON.stringify(data)
|
|
312
|
+
|
|
313
|
+
// 2. AES-256-GCM 加密
|
|
314
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
|
|
315
|
+
let encrypted = cipher.update(jsonString, 'utf8', 'hex')
|
|
316
|
+
encrypted += cipher.final('hex')
|
|
317
|
+
|
|
318
|
+
// 3. 取得 Auth Tag (16 bytes)
|
|
319
|
+
const authTag = cipher.getAuthTag().toString('hex')
|
|
320
|
+
|
|
321
|
+
// 4. 組合加密資料 (encrypted + tag)
|
|
322
|
+
const encryptInfo = encrypted + authTag
|
|
323
|
+
|
|
324
|
+
// 5. SHA256 簽章
|
|
325
|
+
const hashInfo = crypto
|
|
326
|
+
.createHash('sha256')
|
|
327
|
+
.update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
|
|
328
|
+
.digest('hex')
|
|
329
|
+
.toUpperCase()
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
EncryptInfo: encryptInfo,
|
|
333
|
+
HashInfo: hashInfo
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function decryptPAYUNi(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
|
|
338
|
+
// 1. 分離加密內容和 Auth Tag (最後 32 個字元 = 16 bytes hex)
|
|
339
|
+
const encryptedContent = encryptedData.slice(0, -32)
|
|
340
|
+
const authTag = Buffer.from(encryptedData.slice(-32), 'hex')
|
|
341
|
+
|
|
342
|
+
// 2. AES-256-GCM 解密
|
|
343
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', hashKey, hashIV)
|
|
344
|
+
decipher.setAuthTag(authTag)
|
|
345
|
+
let decrypted = decipher.update(encryptedContent, 'hex', 'utf8')
|
|
346
|
+
decrypted += decipher.final('utf8')
|
|
347
|
+
|
|
348
|
+
return JSON.parse(decrypted)
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 3. 訂單建立流程
|
|
353
|
+
|
|
354
|
+
**關鍵:各服務商都使用 Form POST 導向付款頁**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// 後端產生付款表單
|
|
358
|
+
async function createPaymentOrder(provider: string, orderData: OrderData) {
|
|
359
|
+
const service = PaymentServiceFactory.getService(provider)
|
|
360
|
+
const result = await service.createOrder(userId, orderData)
|
|
361
|
+
|
|
362
|
+
// 儲存訂單資訊
|
|
363
|
+
await prisma.order.update({
|
|
364
|
+
where: { id: orderId },
|
|
365
|
+
data: {
|
|
366
|
+
merchantTradeNo: result.merchantTradeNo,
|
|
367
|
+
paymentProvider: provider, // **重要**:儲存使用的服務商
|
|
368
|
+
paymentMethod: orderData.paymentMethod,
|
|
369
|
+
status: 'PENDING'
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// 回傳表單資料
|
|
374
|
+
return {
|
|
375
|
+
type: 'form',
|
|
376
|
+
action: result.formAction,
|
|
377
|
+
method: result.formMethod,
|
|
378
|
+
params: result.formParams
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 前端提交表單
|
|
383
|
+
function submitPaymentForm(formData: PaymentFormData) {
|
|
384
|
+
const form = document.createElement('form')
|
|
385
|
+
form.method = formData.method
|
|
386
|
+
form.action = formData.action
|
|
387
|
+
form.target = '_self' // 整頁跳轉
|
|
388
|
+
|
|
389
|
+
// 添加隱藏欄位
|
|
390
|
+
Object.entries(formData.params).forEach(([key, value]) => {
|
|
391
|
+
const input = document.createElement('input')
|
|
392
|
+
input.type = 'hidden'
|
|
393
|
+
input.name = key
|
|
394
|
+
input.value = value
|
|
395
|
+
form.appendChild(input)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
document.body.appendChild(form)
|
|
399
|
+
form.submit()
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### 4. 付款通知處理
|
|
404
|
+
|
|
405
|
+
**ReturnURL 處理(付款完成後):**
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// app/api/payment/callback/route.ts
|
|
409
|
+
export async function POST(request: Request) {
|
|
410
|
+
const formData = await request.formData()
|
|
411
|
+
const params = Object.fromEntries(formData)
|
|
412
|
+
|
|
413
|
+
// 1. 偵測服務商(根據欄位判斷)
|
|
414
|
+
const provider = detectProvider(params)
|
|
415
|
+
const service = PaymentServiceFactory.getService(provider)
|
|
416
|
+
|
|
417
|
+
// 2. 驗證簽章
|
|
418
|
+
const isValid = service.verifyCallback(params)
|
|
419
|
+
if (!isValid) {
|
|
420
|
+
return new Response('0|CheckMacValue Error', { status: 400 })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 3. 更新訂單狀態
|
|
424
|
+
const merchantTradeNo = params.MerchantTradeNo || params.MerchantOrderNo
|
|
425
|
+
await prisma.order.update({
|
|
426
|
+
where: { merchantTradeNo },
|
|
427
|
+
data: {
|
|
428
|
+
status: params.RtnCode === '1' ? 'PAID' : 'FAILED',
|
|
429
|
+
paidAt: new Date(),
|
|
430
|
+
tradeNo: params.TradeNo, // **重要**:儲存金流商訂單號(退款時需要)
|
|
431
|
+
paymentDetails: params
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
// 4. 回應固定格式
|
|
436
|
+
return new Response('1|OK') // ECPay/NewebPay 要求
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### 5. 查詢訂單
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
async function queryPaymentOrder(merchantTradeNo: string) {
|
|
444
|
+
// 1. 查詢訂單取得服務商
|
|
445
|
+
const order = await prisma.order.findUnique({
|
|
446
|
+
where: { merchantTradeNo }
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// 2. 使用訂單記錄的服務商查詢
|
|
450
|
+
const service = PaymentServiceFactory.getService(order.paymentProvider)
|
|
451
|
+
const result = await service.queryOrder(order.userId, merchantTradeNo)
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 6. 退款處理
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
async function refundPaymentOrder(merchantTradeNo: string, refundAmount: number) {
|
|
461
|
+
const order = await prisma.order.findUnique({
|
|
462
|
+
where: { merchantTradeNo }
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// **重要**:使用開立時的服務商和 TradeNo
|
|
466
|
+
const service = PaymentServiceFactory.getService(order.paymentProvider)
|
|
467
|
+
const result = await service.refundOrder(
|
|
468
|
+
order.userId,
|
|
469
|
+
order.tradeNo, // 金流商訂單號
|
|
470
|
+
refundAmount
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
// 更新訂單狀態
|
|
474
|
+
await prisma.order.update({
|
|
475
|
+
where: { merchantTradeNo },
|
|
476
|
+
data: {
|
|
477
|
+
status: 'REFUNDED',
|
|
478
|
+
refundAmount,
|
|
479
|
+
refundedAt: new Date()
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
return result
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## 常見問題排除
|
|
488
|
+
|
|
489
|
+
### 問題 1: CheckMacValue 驗證失敗
|
|
490
|
+
|
|
491
|
+
**錯誤訊息:** ECPay 回傳 `10100058`,NewebPay 回傳 `CheckValue Error`
|
|
492
|
+
|
|
493
|
+
**常見原因:**
|
|
494
|
+
1. 參數排序錯誤(必須按照字母順序)
|
|
495
|
+
2. URL Encode 不正確(ECPay 需要 lowercase)
|
|
496
|
+
3. 編碼問題(UTF-8)
|
|
497
|
+
4. 忘記移除 CheckMacValue 本身
|
|
498
|
+
|
|
499
|
+
**解決方案:**
|
|
500
|
+
```typescript
|
|
501
|
+
// * 正確
|
|
502
|
+
function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
|
|
503
|
+
// 1. 移除 CheckMacValue
|
|
504
|
+
const { CheckMacValue, ...cleanParams } = params
|
|
505
|
+
|
|
506
|
+
// 2. 排序
|
|
507
|
+
const sortedKeys = Object.keys(cleanParams).sort()
|
|
508
|
+
|
|
509
|
+
// 3. 組合字串
|
|
510
|
+
const paramString = sortedKeys.map(k => `${k}=${cleanParams[k]}`).join('&')
|
|
511
|
+
|
|
512
|
+
// 4. 加上 HashKey/HashIV
|
|
513
|
+
const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
|
|
514
|
+
|
|
515
|
+
// 5. URL Encode (lowercase for ECPay)
|
|
516
|
+
const encoded = encodeURIComponent(rawString).toLowerCase()
|
|
517
|
+
|
|
518
|
+
// 6. SHA256
|
|
519
|
+
return crypto.createHash('sha256').update(encoded).digest('hex').toUpperCase()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// * 錯誤:未排序
|
|
523
|
+
const paramString = Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&')
|
|
524
|
+
|
|
525
|
+
// * 錯誤:URL Encode 使用 uppercase
|
|
526
|
+
const encoded = encodeURIComponent(rawString) // 應該用 toLowerCase()
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### 問題 2: 訂單編號重複
|
|
530
|
+
|
|
531
|
+
**錯誤訊息:** ECPay `10100003`,NewebPay/PAYUNi 訂單已存在
|
|
532
|
+
|
|
533
|
+
**原因:** 使用相同的 MerchantTradeNo
|
|
534
|
+
|
|
535
|
+
**解決方案:**
|
|
536
|
+
```typescript
|
|
537
|
+
// * 建議:加入時間戳保證唯一性
|
|
538
|
+
function generateMerchantTradeNo(prefix: string = 'ORD') {
|
|
539
|
+
const timestamp = Date.now()
|
|
540
|
+
const random = Math.random().toString(36).substring(2, 8).toUpperCase()
|
|
541
|
+
return `${prefix}${timestamp}${random}`.substring(0, 20) // ECPay 限制 20 字元
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 範例輸出: ORD1706512345A7B2
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 問題 3: 金額計算錯誤
|
|
548
|
+
|
|
549
|
+
**錯誤訊息:** 回傳金額驗算錯誤
|
|
550
|
+
|
|
551
|
+
**常見原因:**
|
|
552
|
+
1. 金額包含小數
|
|
553
|
+
2. 金額為負數或 0
|
|
554
|
+
3. 商品金額總和不等於訂單金額
|
|
555
|
+
|
|
556
|
+
**解決方案:**
|
|
557
|
+
```typescript
|
|
558
|
+
// * 確保金額為正整數
|
|
559
|
+
function validateAmount(amount: number): number {
|
|
560
|
+
if (amount <= 0) {
|
|
561
|
+
throw new Error('金額必須大於 0')
|
|
562
|
+
}
|
|
563
|
+
return Math.round(amount) // 移除小數
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// * 驗證商品金額總和
|
|
567
|
+
function validateItemsAmount(items: Item[], totalAmount: number) {
|
|
568
|
+
const itemsSum = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
|
569
|
+
if (Math.round(itemsSum) !== Math.round(totalAmount)) {
|
|
570
|
+
throw new Error(`商品金額總和 ${itemsSum} 不等於訂單金額 ${totalAmount}`)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 問題 4: 收不到付款通知
|
|
576
|
+
|
|
577
|
+
**原因:**
|
|
578
|
+
1. ReturnURL 不是 HTTPS
|
|
579
|
+
2. 防火牆阻擋
|
|
580
|
+
3. 回應格式錯誤
|
|
581
|
+
|
|
582
|
+
**解決方案:**
|
|
583
|
+
```typescript
|
|
584
|
+
// * 確認 ReturnURL 使用 HTTPS
|
|
585
|
+
const returnURL = 'https://yourdomain.com/api/payment/callback' // 必須 HTTPS
|
|
586
|
+
|
|
587
|
+
// * 正確回應格式
|
|
588
|
+
export async function POST(request: Request) {
|
|
589
|
+
// ... 處理邏輯
|
|
590
|
+
|
|
591
|
+
// ECPay/NewebPay 需要回應 "1|OK"
|
|
592
|
+
return new Response('1|OK', {
|
|
593
|
+
status: 200,
|
|
594
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// * 錯誤:JSON 回應
|
|
599
|
+
return Response.json({ success: true }) // 不正確
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### 問題 5: 測試卡無法付款
|
|
603
|
+
|
|
604
|
+
**原因:** 使用真實卡號或測試卡格式錯誤
|
|
605
|
+
|
|
606
|
+
**解決方案:**
|
|
607
|
+
```
|
|
608
|
+
# ECPay 測試卡
|
|
609
|
+
卡號: 4311-9522-2222-2222
|
|
610
|
+
有效期: 任意未來月年
|
|
611
|
+
CVV: 任意 3 碼
|
|
612
|
+
|
|
613
|
+
# NewebPay 測試卡
|
|
614
|
+
卡號: 4000-2211-1111-1111
|
|
615
|
+
有效期: 任意未來月年
|
|
616
|
+
CVV: 任意 3 碼
|
|
617
|
+
|
|
618
|
+
# PAYUNi 測試卡
|
|
619
|
+
請至後台查詢官方測試卡號
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### 問題 6: AES 加密失敗
|
|
623
|
+
|
|
624
|
+
**NewebPay AES-256-CBC 加密錯誤:**
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// * 確認 Key/IV 長度
|
|
628
|
+
const hashKey = 'your32BytesHashKeyHere123456' // 必須 32 bytes
|
|
629
|
+
const hashIV = 'your16BytesIV123' // 必須 16 bytes
|
|
630
|
+
|
|
631
|
+
// * 使用正確的 padding
|
|
632
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
|
|
633
|
+
cipher.setAutoPadding(true) // PKCS7 padding
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**PAYUNi AES-256-GCM 加密錯誤:**
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// * 記得附加 Auth Tag
|
|
640
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
|
|
641
|
+
let encrypted = cipher.update(jsonString, 'utf8', 'hex')
|
|
642
|
+
encrypted += cipher.final('hex')
|
|
643
|
+
const authTag = cipher.getAuthTag().toString('hex') // **重要**
|
|
644
|
+
const encryptInfo = encrypted + authTag // 總長度 = encrypted + 32 chars (16 bytes hex)
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 問題 7: ATM 虛擬帳號未產生
|
|
648
|
+
|
|
649
|
+
**原因:**
|
|
650
|
+
1. 未設定 ChoosePayment=ATM
|
|
651
|
+
2. ExpireDate 格式錯誤
|
|
652
|
+
3. ExpireDate 超過範圍(3-60 天)
|
|
653
|
+
|
|
654
|
+
**解決方案:**
|
|
655
|
+
```typescript
|
|
656
|
+
// * ECPay ATM 設定
|
|
657
|
+
const params = {
|
|
658
|
+
ChoosePayment: 'ATM',
|
|
659
|
+
ExpireDate: 3, // 3-60 天
|
|
660
|
+
PaymentInfoURL: 'https://yourdomain.com/api/payment/atm-info', // 接收帳號通知
|
|
661
|
+
// ...
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// * NewebPay ATM 設定
|
|
665
|
+
const params = {
|
|
666
|
+
VACC: 1, // 啟用 ATM
|
|
667
|
+
ExpireDate: '2024-12-31', // yyyy-MM-dd 格式
|
|
668
|
+
// ...
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### 問題 8: 定期定額建立失敗
|
|
673
|
+
|
|
674
|
+
**原因:** 週期參數不完整
|
|
675
|
+
|
|
676
|
+
**解決方案:**
|
|
677
|
+
```typescript
|
|
678
|
+
// * ECPay 定期定額
|
|
679
|
+
const periodicParams = {
|
|
680
|
+
PeriodAmount: 1000, // 扣款金額
|
|
681
|
+
PeriodType: 'M', // D=日, M=月, Y=年
|
|
682
|
+
Frequency: 1, // 頻率(每 1 個週期)
|
|
683
|
+
ExecTimes: 12, // 執行次數(12 次)
|
|
684
|
+
PeriodReturnURL: 'https://yourdomain.com/api/payment/periodic-callback',
|
|
685
|
+
// ...
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// * NewebPay 定期定額
|
|
689
|
+
const periodicParams = {
|
|
690
|
+
PeriodAmt: 1000,
|
|
691
|
+
PeriodType: 'M',
|
|
692
|
+
PeriodPoint: '01', // 每月 1 號扣款
|
|
693
|
+
PeriodTimes: 12,
|
|
694
|
+
// ...
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 問題 9: 跨域問題
|
|
699
|
+
|
|
700
|
+
**錯誤:** 前端導向付款頁失敗
|
|
701
|
+
|
|
702
|
+
**原因:** 使用 AJAX 或 Fetch,受 CORS 限制
|
|
703
|
+
|
|
704
|
+
**解決方案:**
|
|
705
|
+
```typescript
|
|
706
|
+
// * 錯誤:使用 AJAX
|
|
707
|
+
fetch(paymentUrl, { method: 'POST', body: formData }) // 會被 CORS 阻擋
|
|
708
|
+
|
|
709
|
+
// * 正確:使用 Form POST 整頁跳轉
|
|
710
|
+
function submitPaymentForm(action: string, params: Record<string, string>) {
|
|
711
|
+
const form = document.createElement('form')
|
|
712
|
+
form.method = 'POST'
|
|
713
|
+
form.action = action
|
|
714
|
+
form.target = '_self' // 整頁跳轉
|
|
715
|
+
|
|
716
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
717
|
+
const input = document.createElement('input')
|
|
718
|
+
input.type = 'hidden'
|
|
719
|
+
input.name = key
|
|
720
|
+
input.value = value
|
|
721
|
+
form.appendChild(input)
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
document.body.appendChild(form)
|
|
725
|
+
form.submit() // 直接提交
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### 問題 10: 商店代號錯誤
|
|
730
|
+
|
|
731
|
+
**錯誤訊息:** 回傳商店不存在
|
|
732
|
+
|
|
733
|
+
**原因:**
|
|
734
|
+
1. MerchantID 錯誤
|
|
735
|
+
2. 測試/正式環境混用
|
|
736
|
+
|
|
737
|
+
**解決方案:**
|
|
738
|
+
```typescript
|
|
739
|
+
// * 使用環境變數區分
|
|
740
|
+
const config = {
|
|
741
|
+
merchantID: process.env.NODE_ENV === 'production'
|
|
742
|
+
? process.env.ECPAY_MERCHANT_ID_PROD
|
|
743
|
+
: process.env.ECPAY_MERCHANT_ID_TEST,
|
|
744
|
+
apiUrl: process.env.NODE_ENV === 'production'
|
|
745
|
+
? 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
746
|
+
: 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## 測試帳號
|
|
751
|
+
|
|
752
|
+
### 綠界測試環境
|
|
753
|
+
```
|
|
754
|
+
MerchantID: 3002607
|
|
755
|
+
HashKey: pwFHCqoQZGmho4w6
|
|
756
|
+
HashIV: EkRm7iFT261dpevs
|
|
757
|
+
測試 URL: https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5
|
|
758
|
+
測試卡號: 4311-9522-2222-2222
|
|
759
|
+
後台: https://vendor-stage.ecpay.com.tw/
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### 藍新測試環境
|
|
763
|
+
```
|
|
764
|
+
MerchantID: 請至後台申請
|
|
765
|
+
HashKey: 請至後台申請(32 bytes)
|
|
766
|
+
HashIV: 請至後台申請(16 bytes)
|
|
767
|
+
測試 URL: https://ccore.newebpay.com/MPG/mpg_gateway
|
|
768
|
+
測試卡號: 4000-2211-1111-1111
|
|
769
|
+
後台: https://cwww.newebpay.com/
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### 統一測試環境
|
|
773
|
+
```
|
|
774
|
+
MerchantID: 請至後台申請
|
|
775
|
+
HashKey: 請至後台申請
|
|
776
|
+
HashIV: 請至後台申請
|
|
777
|
+
測試 URL: https://sandbox-api.payuni.com.tw/api/upp
|
|
778
|
+
後台: https://sandbox.payuni.com.tw/
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
## 開發檢查清單
|
|
782
|
+
|
|
783
|
+
使用此清單確保實作完整:
|
|
784
|
+
|
|
785
|
+
### 基礎設定
|
|
786
|
+
- [ ] 實作 `PaymentService` 介面
|
|
787
|
+
- [ ] 實作各服務商加密機制(SHA256 / AES-CBC / AES-GCM)
|
|
788
|
+
- [ ] 設定環境變數(測試/正式)
|
|
789
|
+
- [ ] 配置 ReturnURL(HTTPS)
|
|
790
|
+
- [ ] 配置 OrderResultURL(付款完成導向頁)
|
|
791
|
+
|
|
792
|
+
### 訂單處理
|
|
793
|
+
- [ ] 儲存 `paymentProvider` 欄位(查詢/退款時需要)
|
|
794
|
+
- [ ] 儲存 `merchantTradeNo`(唯一訂單編號)
|
|
795
|
+
- [ ] 儲存 `tradeNo`(金流商訂單號,退款時需要)
|
|
796
|
+
- [ ] 金額驗證(正整數、最小金額)
|
|
797
|
+
- [ ] 商品金額總和驗證
|
|
798
|
+
|
|
799
|
+
### 付款方式
|
|
800
|
+
- [ ] 支援信用卡一次付清
|
|
801
|
+
- [ ] 支援 ATM 虛擬帳號(可選)
|
|
802
|
+
- [ ] 支援超商代碼/條碼(可選)
|
|
803
|
+
- [ ] 支援電子錢包(可選)
|
|
804
|
+
- [ ] 支援信用卡分期(可選)
|
|
805
|
+
- [ ] 支援定期定額(可選)
|
|
806
|
+
|
|
807
|
+
### 回呼處理
|
|
808
|
+
- [ ] 驗證 CheckMacValue / TradeSha / HashInfo
|
|
809
|
+
- [ ] 更新訂單狀態
|
|
810
|
+
- [ ] 回應 "1|OK" 格式
|
|
811
|
+
- [ ] 防止重複通知處理(冪等性)
|
|
812
|
+
|
|
813
|
+
### 查詢退款
|
|
814
|
+
- [ ] 實作訂單查詢 API
|
|
815
|
+
- [ ] 實作退款 API
|
|
816
|
+
- [ ] 處理部分退款邏輯
|
|
817
|
+
- [ ] 檢查退款期限
|
|
818
|
+
|
|
819
|
+
### 錯誤處理
|
|
820
|
+
- [ ] 實作錯誤處理與 logger
|
|
821
|
+
- [ ] 記錄完整請求/回應(除敏感資訊)
|
|
822
|
+
- [ ] 處理加密錯誤
|
|
823
|
+
- [ ] 處理網路逾時
|
|
824
|
+
|
|
825
|
+
### 測試驗證
|
|
826
|
+
- [ ] 測試環境驗證
|
|
827
|
+
- [ ] 使用官方測試卡測試
|
|
828
|
+
- [ ] 測試付款通知接收
|
|
829
|
+
- [ ] 測試查詢功能
|
|
830
|
+
- [ ] 測試退款功能
|
|
831
|
+
|
|
832
|
+
## 新增服務商步驟
|
|
833
|
+
|
|
834
|
+
1. 在 `lib/services/` 建立 `{provider}-payment-service.ts`
|
|
835
|
+
2. 實作 `PaymentService` 介面的所有方法
|
|
836
|
+
3. 在 `PaymentServiceFactory` 註冊新服務商
|
|
837
|
+
4. 在 `prisma/schema.prisma` 的 `PaymentProvider` enum 新增選項
|
|
838
|
+
5. 執行 `prisma migrate` 或 `prisma db push`
|
|
839
|
+
6. 更新前端設定頁面
|
|
840
|
+
7. 撰寫單元測試
|
|
841
|
+
8. 更新文檔
|
|
842
|
+
|
|
843
|
+
## 參考資料
|
|
844
|
+
|
|
845
|
+
詳細 API 規格請查看 `references/` 目錄:
|
|
846
|
+
- [綠界 ECPay Payment API 規格](./references/ECPAY_PAYMENT_REFERENCE.md)
|
|
847
|
+
- [藍新 NewebPay Payment API 規格](./references/NEWEBPAY_PAYMENT_REFERENCE.md)
|
|
848
|
+
- [統一 PAYUNi Payment API 規格](./references/PAYUNI_PAYMENT_REFERENCE.md)
|
|
849
|
+
|
|
850
|
+
官方文檔:
|
|
851
|
+
- ECPay: https://developers.ecpay.com.tw/
|
|
852
|
+
- NewebPay: https://www.newebpay.com/website/Page/content/download_api
|
|
853
|
+
- PAYUNi: https://www.payuni.com.tw/docs/
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
最後更新:2026/01/29
|