taiwan-payment-skill 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1425 +1,1425 @@
1
- # 台灣金流 Skill - 完整範例集
2
-
3
- 這份文件包含使用 `taiwan-payment` skill 的完整實作範例。
4
-
5
- ## 目錄
6
-
7
- 1. [基礎範例](#基礎範例)
8
- 2. [進階範例](#進階範例)
9
- 3. [實戰場景](#實戰場景)
10
- 4. [常見錯誤與修正](#常見錯誤與修正)
11
-
12
- ---
13
-
14
- ## 基礎範例
15
-
16
- ### 範例 1: ECPay 信用卡一次付清
17
-
18
- **場景:** 客戶購買商品,金額 1050 元,使用綠界信用卡付款
19
-
20
- **Claude 提示詞:**
21
- ```
22
- 使用 ECPay 測試環境建立信用卡付款
23
- - 訂單金額: 1050 元
24
- - 付款方式: 信用卡一次付清
25
- - 商品名稱: 測試商品
26
- ```
27
-
28
- **生成的程式碼:**
29
-
30
- ```typescript
31
- import { ECPayPaymentService } from '@/lib/services/ecpay-payment-service'
32
- import crypto from 'crypto'
33
-
34
- async function createECPayOrder() {
35
- const service = new ECPayPaymentService(false) // 測試環境
36
-
37
- // 產生唯一訂單編號
38
- const merchantTradeNo = `ORD${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`
39
-
40
- const orderData = {
41
- MerchantTradeNo: merchantTradeNo,
42
- MerchantTradeDate: new Date().toLocaleString('zh-TW', {
43
- year: 'numeric',
44
- month: '2-digit',
45
- day: '2-digit',
46
- hour: '2-digit',
47
- minute: '2-digit',
48
- second: '2-digit',
49
- hour12: false
50
- }).replace(/\//g, '/'),
51
- TotalAmount: 1050,
52
- TradeDesc: '測試商品購買',
53
- ItemName: '測試商品 x1',
54
- ReturnURL: 'https://yourdomain.com/api/payment/callback',
55
- ChoosePayment: 'Credit',
56
- EncryptType: 1,
57
- }
58
-
59
- try {
60
- const result = await service.createOrder('user-123', orderData)
61
-
62
- if (result.success) {
63
- console.log('[OK] 訂單建立成功')
64
- console.log('訂單編號:', result.merchantTradeNo)
65
- console.log('請導向付款頁:', result.formAction)
66
-
67
- // 儲存訂單
68
- await prisma.order.create({
69
- data: {
70
- userId: 'user-123',
71
- merchantTradeNo: result.merchantTradeNo,
72
- amount: 1050,
73
- status: 'PENDING',
74
- paymentProvider: 'ECPAY',
75
- paymentMethod: 'Credit',
76
- }
77
- })
78
-
79
- return result
80
- } else {
81
- console.error('[ERROR] 建立失敗:', result.msg)
82
- }
83
- } catch (error) {
84
- console.error('付款建立異常:', error)
85
- throw error
86
- }
87
- }
88
-
89
- // 前端提交表單
90
- function submitToECPay(formData: { action: string, params: Record<string, string> }) {
91
- const form = document.createElement('form')
92
- form.method = 'POST'
93
- form.action = formData.action
94
- form.target = '_self'
95
-
96
- Object.entries(formData.params).forEach(([key, value]) => {
97
- const input = document.createElement('input')
98
- input.type = 'hidden'
99
- input.name = key
100
- input.value = value
101
- form.appendChild(input)
102
- })
103
-
104
- document.body.appendChild(form)
105
- form.submit()
106
- }
107
- ```
108
-
109
- **預期輸出:**
110
- ```
111
- [OK] 訂單建立成功
112
- 訂單編號: ORD1738123456ABC7
113
- 請導向付款頁: https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5
114
- ```
115
-
116
- ---
117
-
118
- ### 範例 2: NewebPay MPG 整合支付
119
-
120
- **場景:** 使用藍新金流整合支付頁,支援信用卡、ATM、超商代碼
121
-
122
- **Claude 提示詞:**
123
- ```
124
- 使用 NewebPay MPG 建立整合付款
125
- - 金額: 2500 元
126
- - 付款方式: 信用卡、ATM、超商代碼
127
- - 使用 AES 加密
128
- ```
129
-
130
- **生成的程式碼:**
131
-
132
- ```typescript
133
- import crypto from 'crypto'
134
-
135
- class NewebPayService {
136
- private merchantID: string
137
- private hashKey: string
138
- private hashIV: string
139
- private apiUrl: string
140
-
141
- constructor(isProd: boolean = false) {
142
- this.merchantID = isProd
143
- ? process.env.NEWEBPAY_MERCHANT_ID_PROD!
144
- : process.env.NEWEBPAY_MERCHANT_ID_TEST!
145
- this.hashKey = isProd
146
- ? process.env.NEWEBPAY_HASH_KEY_PROD!
147
- : process.env.NEWEBPAY_HASH_KEY_TEST!
148
- this.hashIV = isProd
149
- ? process.env.NEWEBPAY_HASH_IV_PROD!
150
- : process.env.NEWEBPAY_HASH_IV_TEST!
151
- this.apiUrl = isProd
152
- ? 'https://core.newebpay.com/MPG/mpg_gateway'
153
- : 'https://ccore.newebpay.com/MPG/mpg_gateway'
154
- }
155
-
156
- private encrypt(data: Record<string, any>): { TradeInfo: string, TradeSha: string } {
157
- // 1. 轉換為查詢字串
158
- const queryString = new URLSearchParams(data).toString()
159
-
160
- // 2. AES-256-CBC 加密
161
- const cipher = crypto.createCipheriv('aes-256-cbc', this.hashKey, this.hashIV)
162
- cipher.setAutoPadding(true)
163
- let encrypted = cipher.update(queryString, 'utf8', 'hex')
164
- encrypted += cipher.final('hex')
165
-
166
- // 3. 計算 SHA256
167
- const tradeSha = crypto
168
- .createHash('sha256')
169
- .update(`HashKey=${this.hashKey}&${encrypted}&HashIV=${this.hashIV}`)
170
- .digest('hex')
171
- .toUpperCase()
172
-
173
- return {
174
- TradeInfo: encrypted,
175
- TradeSha: tradeSha
176
- }
177
- }
178
-
179
- async createMPGOrder(userId: string, orderData: any) {
180
- const merchantOrderNo = `MPG${Date.now()}`
181
-
182
- const tradeInfo = {
183
- MerchantID: this.merchantID,
184
- RespondType: 'JSON',
185
- TimeStamp: Math.floor(Date.now() / 1000).toString(),
186
- Version: '2.0',
187
- MerchantOrderNo: merchantOrderNo,
188
- Amt: orderData.amount,
189
- ItemDesc: orderData.itemDesc || '商品購買',
190
- ReturnURL: orderData.returnURL,
191
- NotifyURL: orderData.notifyURL,
192
- Email: orderData.email,
193
- // 啟用付款方式
194
- CREDIT: 1, // 信用卡
195
- VACC: 1, // ATM
196
- CVS: 1, // 超商代碼
197
- }
198
-
199
- // 加密
200
- const { TradeInfo, TradeSha } = this.encrypt(tradeInfo)
201
-
202
- console.log('[OK] NewebPay MPG 訂單建立')
203
- console.log('訂單編號:', merchantOrderNo)
204
- console.log('加密 TradeInfo 長度:', TradeInfo.length)
205
-
206
- return {
207
- success: true,
208
- formAction: this.apiUrl,
209
- formMethod: 'POST',
210
- formParams: {
211
- MerchantID: this.merchantID,
212
- TradeInfo: TradeInfo,
213
- TradeSha: TradeSha,
214
- Version: '2.0'
215
- }
216
- }
217
- }
218
- }
219
-
220
- // 使用範例
221
- async function createNewebPayMPG() {
222
- const service = new NewebPayService(false) // 測試環境
223
-
224
- const result = await service.createMPGOrder('user-123', {
225
- amount: 2500,
226
- itemDesc: '網站年費方案',
227
- returnURL: 'https://yourdomain.com/payment/return',
228
- notifyURL: 'https://yourdomain.com/api/payment/callback',
229
- email: 'test@example.com'
230
- })
231
-
232
- return result
233
- }
234
- ```
235
-
236
- **重要說明:**
237
- - NewebPay 使用 AES-256-CBC 雙層加密(TradeInfo + TradeSha)
238
- - HashKey 必須 32 bytes,HashIV 必須 16 bytes
239
- - MPG 整合支付頁會根據啟用的付款方式顯示選項
240
-
241
- **預期輸出:**
242
- ```
243
- [OK] NewebPay MPG 訂單建立
244
- 訂單編號: MPG1738123456789
245
- 加密 TradeInfo 長度: 384
246
- ```
247
-
248
- ---
249
-
250
- ### 範例 3: PAYUNi RESTful API
251
-
252
- **場景:** 使用統一金流 RESTful JSON API 建立付款
253
-
254
- **Claude 提示詞:**
255
- ```
256
- 使用 PAYUNi RESTful API 建立信用卡付款
257
- - 金額: 3000 元
258
- - JSON 格式
259
- - AES-256-GCM 加密
260
- ```
261
-
262
- **生成的程式碼:**
263
-
264
- ```typescript
265
- import crypto from 'crypto'
266
- import axios from 'axios'
267
-
268
- class PAYUNiService {
269
- private merchantID: string
270
- private hashKey: string
271
- private hashIV: string
272
- private apiUrl: string
273
-
274
- constructor(isProd: boolean = false) {
275
- this.merchantID = isProd
276
- ? process.env.PAYUNI_MERCHANT_ID_PROD!
277
- : process.env.PAYUNI_MERCHANT_ID_TEST!
278
- this.hashKey = isProd
279
- ? process.env.PAYUNI_HASH_KEY_PROD!
280
- : process.env.PAYUNI_HASH_KEY_TEST!
281
- this.hashIV = isProd
282
- ? process.env.PAYUNI_HASH_IV_PROD!
283
- : process.env.PAYUNI_HASH_IV_TEST!
284
- this.apiUrl = isProd
285
- ? 'https://api.payuni.com.tw/api/upp'
286
- : 'https://sandbox-api.payuni.com.tw/api/upp'
287
- }
288
-
289
- private encrypt(data: Record<string, any>): { EncryptInfo: string, HashInfo: string } {
290
- // 1. JSON 字串化
291
- const jsonString = JSON.stringify(data)
292
-
293
- // 2. AES-256-GCM 加密
294
- const cipher = crypto.createCipheriv('aes-256-gcm', this.hashKey, this.hashIV)
295
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
296
- encrypted += cipher.final('hex')
297
-
298
- // 3. 取得 Auth Tag
299
- const authTag = cipher.getAuthTag().toString('hex')
300
-
301
- // 4. 組合加密資料
302
- const encryptInfo = encrypted + authTag
303
-
304
- // 5. SHA256 簽章
305
- const hashInfo = crypto
306
- .createHash('sha256')
307
- .update(`HashKey=${this.hashKey}&${encryptInfo}&HashIV=${this.hashIV}`)
308
- .digest('hex')
309
- .toUpperCase()
310
-
311
- return {
312
- EncryptInfo: encryptInfo,
313
- HashInfo: hashInfo
314
- }
315
- }
316
-
317
- async createOrder(userId: string, orderData: any) {
318
- const merchantOrderNo = `UNI${Date.now()}`
319
-
320
- const tradeData = {
321
- MerchantID: this.merchantID,
322
- MerchantOrderNo: merchantOrderNo,
323
- Amount: orderData.amount,
324
- ItemDescription: orderData.itemDesc || '商品購買',
325
- ReturnURL: orderData.returnURL,
326
- NotifyURL: orderData.notifyURL,
327
- Email: orderData.email,
328
- PaymentMethod: 'CREDIT', // 信用卡
329
- TimeStamp: Math.floor(Date.now() / 1000)
330
- }
331
-
332
- // 加密
333
- const { EncryptInfo, HashInfo } = this.encrypt(tradeData)
334
-
335
- try {
336
- // RESTful POST 請求
337
- const response = await axios.post(this.apiUrl, {
338
- MerchantID: this.merchantID,
339
- EncryptInfo: EncryptInfo,
340
- HashInfo: HashInfo
341
- }, {
342
- headers: {
343
- 'Content-Type': 'application/json'
344
- }
345
- })
346
-
347
- console.log('[OK] PAYUNi 訂單建立')
348
- console.log('訂單編號:', merchantOrderNo)
349
- console.log('回應狀態:', response.data.Status)
350
-
351
- return {
352
- success: response.data.Status === 'SUCCESS',
353
- merchantTradeNo: merchantOrderNo,
354
- paymentUrl: response.data.Data?.PaymentURL,
355
- message: response.data.Message
356
- }
357
- } catch (error) {
358
- console.error('[ERROR] PAYUNi 請求失敗:', error)
359
- throw error
360
- }
361
- }
362
- }
363
-
364
- // 使用範例
365
- async function createPAYUNiOrder() {
366
- const service = new PAYUNiService(false)
367
-
368
- const result = await service.createOrder('user-123', {
369
- amount: 3000,
370
- itemDesc: '會員升級方案',
371
- returnURL: 'https://yourdomain.com/payment/return',
372
- notifyURL: 'https://yourdomain.com/api/payment/callback',
373
- email: 'test@example.com'
374
- })
375
-
376
- if (result.success) {
377
- console.log('請導向:', result.paymentUrl)
378
- }
379
-
380
- return result
381
- }
382
- ```
383
-
384
- **重要說明:**
385
- - PAYUNi 使用 RESTful JSON API(與 ECPay/NewebPay 的 Form POST 不同)
386
- - AES-256-GCM 加密需要附加 Auth Tag(最後 32 字元)
387
- - 回應為 JSON 格式,包含 PaymentURL 導向連結
388
-
389
- **預期輸出:**
390
- ```
391
- [OK] PAYUNi 訂單建立
392
- 訂單編號: UNI1738123456789
393
- 回應狀態: SUCCESS
394
- 請導向: https://sandbox-api.payuni.com.tw/trade/payment?token=xxx
395
- ```
396
-
397
- ---
398
-
399
- ## 進階範例
400
-
401
- ### 範例 4: 服務工廠模式
402
-
403
- **場景:** 建立金流服務工廠,動態切換三家服務商
404
-
405
- **Claude 提示詞:**
406
- ```
407
- 建立金流服務工廠,支援 ECPay、NewebPay、PAYUNi 動態切換
408
- ```
409
-
410
- **生成的程式碼:**
411
-
412
- ```typescript
413
- // lib/services/payment-provider.ts - 介面定義
414
-
415
- export interface PaymentService {
416
- createOrder(userId: string, data: PaymentOrderData): Promise<PaymentOrderResponse>
417
- queryOrder(userId: string, merchantTradeNo: string): Promise<PaymentQueryResponse>
418
- refundOrder(userId: string, tradeNo: string, amount: number): Promise<PaymentRefundResponse>
419
- verifyCallback(params: Record<string, any>): boolean
420
- }
421
-
422
- export interface PaymentOrderData {
423
- merchantTradeNo: string
424
- totalAmount: number
425
- itemName: string
426
- returnURL: string
427
- notifyURL?: string
428
- email?: string
429
- paymentMethod?: string
430
- }
431
-
432
- export interface PaymentOrderResponse {
433
- success: boolean
434
- merchantTradeNo: string
435
- formAction: string
436
- formMethod: string
437
- formParams: Record<string, string>
438
- msg?: string
439
- }
440
-
441
- // lib/services/payment-service-factory.ts - 工廠類別
442
-
443
- import { PaymentService } from './payment-provider'
444
- import { ECPayPaymentService } from './ecpay-payment-service'
445
- import { NewebPayPaymentService } from './newebpay-payment-service'
446
- import { PAYUNiPaymentService } from './payuni-payment-service'
447
- import { prisma } from '@/lib/prisma'
448
-
449
- type PaymentProvider = 'ECPAY' | 'NEWEBPAY' | 'PAYUNI'
450
-
451
- export class PaymentServiceFactory {
452
- /**
453
- * 根據服務商名稱取得服務實例
454
- */
455
- static getService(
456
- provider: PaymentProvider,
457
- isProd: boolean = false
458
- ): PaymentService {
459
- switch (provider) {
460
- case 'ECPAY':
461
- return new ECPayPaymentService(isProd)
462
- case 'NEWEBPAY':
463
- return new NewebPayPaymentService(isProd)
464
- case 'PAYUNI':
465
- return new PAYUNiPaymentService(isProd)
466
- default:
467
- throw new Error(`不支援的金流服務商: ${provider}`)
468
- }
469
- }
470
-
471
- /**
472
- * 根據使用者設定取得服務實例
473
- */
474
- static async getServiceForUser(userId: string): Promise<PaymentService> {
475
- const settings = await prisma.paymentSettings.findUnique({
476
- where: { userId },
477
- })
478
-
479
- if (!settings || !settings.defaultProvider) {
480
- throw new Error('未設定預設金流服務商')
481
- }
482
-
483
- return this.getService(
484
- settings.defaultProvider as PaymentProvider,
485
- settings.isProduction
486
- )
487
- }
488
-
489
- /**
490
- * 根據訂單取得服務實例(用於查詢/退款)
491
- */
492
- static async getServiceForOrder(merchantTradeNo: string): Promise<PaymentService> {
493
- const order = await prisma.order.findUnique({
494
- where: { merchantTradeNo },
495
- })
496
-
497
- if (!order || !order.paymentProvider) {
498
- throw new Error('訂單不存在或未記錄金流服務商')
499
- }
500
-
501
- return this.getService(
502
- order.paymentProvider as PaymentProvider,
503
- order.isProduction
504
- )
505
- }
506
-
507
- /**
508
- * 取得所有可用的服務商
509
- */
510
- static getAvailableProviders(): PaymentProvider[] {
511
- return ['ECPAY', 'NEWEBPAY', 'PAYUNI']
512
- }
513
-
514
- /**
515
- * 自動偵測服務商(根據回呼參數)
516
- */
517
- static detectProvider(params: Record<string, any>): PaymentProvider {
518
- if (params.CheckMacValue && params.MerchantTradeNo) {
519
- return 'ECPAY'
520
- }
521
- if (params.TradeInfo && params.TradeSha) {
522
- return 'NEWEBPAY'
523
- }
524
- if (params.EncryptInfo && params.HashInfo) {
525
- return 'PAYUNI'
526
- }
527
- throw new Error('無法識別金流服務商')
528
- }
529
- }
530
- ```
531
-
532
- **使用範例:**
533
-
534
- ```typescript
535
- // API Route: app/api/payment/create/route.ts
536
-
537
- import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
538
-
539
- export async function POST(request: Request) {
540
- const { userId, amount, itemName, provider } = await request.json()
541
-
542
- // 方式 1: 使用指定服務商
543
- const service = PaymentServiceFactory.getService(provider || 'ECPAY')
544
-
545
- // 方式 2: 使用使用者預設服務商
546
- // const service = await PaymentServiceFactory.getServiceForUser(userId)
547
-
548
- const result = await service.createOrder(userId, {
549
- merchantTradeNo: `ORD${Date.now()}`,
550
- totalAmount: amount,
551
- itemName: itemName,
552
- returnURL: 'https://yourdomain.com/payment/return',
553
- notifyURL: 'https://yourdomain.com/api/payment/callback',
554
- })
555
-
556
- return Response.json(result)
557
- }
558
- ```
559
-
560
- ---
561
-
562
- ### 範例 5: 加密輔助函數
563
-
564
- **場景:** 建立三種加密方式的 helper functions
565
-
566
- **Claude 提示詞:**
567
- ```
568
- 建立加密工具,包含 ECPay SHA256、NewebPay AES-CBC、PAYUNi AES-GCM
569
- ```
570
-
571
- **生成的程式碼:**
572
-
573
- ```typescript
574
- // lib/utils/payment-encryption.ts
575
-
576
- import crypto from 'crypto'
577
-
578
- /**
579
- * ECPay CheckMacValue 計算(SHA256)
580
- */
581
- export function generateECPayCheckMacValue(
582
- params: Record<string, any>,
583
- hashKey: string,
584
- hashIV: string
585
- ): string {
586
- // 1. 移除 CheckMacValue 本身
587
- const { CheckMacValue, ...cleanParams } = params
588
-
589
- // 2. 依照 key 排序(字母順序)
590
- const sortedKeys = Object.keys(cleanParams).sort()
591
-
592
- // 3. 組合參數字串
593
- const paramString = sortedKeys
594
- .map(key => `${key}=${cleanParams[key]}`)
595
- .join('&')
596
-
597
- // 4. 前後加上 HashKey 和 HashIV
598
- const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
599
-
600
- // 5. URL Encode (lowercase)
601
- const encoded = encodeURIComponent(rawString).toLowerCase()
602
-
603
- // 6. SHA256 雜湊
604
- const hash = crypto.createHash('sha256').update(encoded).digest('hex')
605
-
606
- // 7. 轉大寫
607
- return hash.toUpperCase()
608
- }
609
-
610
- /**
611
- * NewebPay AES-256-CBC 加密
612
- */
613
- export function encryptNewebPay(
614
- data: Record<string, any>,
615
- hashKey: string,
616
- hashIV: string
617
- ): { TradeInfo: string; TradeSha: string } {
618
- // 1. 轉換為查詢字串
619
- const queryString = new URLSearchParams(data).toString()
620
-
621
- // 2. AES-256-CBC 加密
622
- const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
623
- cipher.setAutoPadding(true)
624
- let encrypted = cipher.update(queryString, 'utf8', 'hex')
625
- encrypted += cipher.final('hex')
626
-
627
- // 3. 計算 SHA256
628
- const tradeSha = crypto
629
- .createHash('sha256')
630
- .update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
631
- .digest('hex')
632
- .toUpperCase()
633
-
634
- return {
635
- TradeInfo: encrypted,
636
- TradeSha: tradeSha,
637
- }
638
- }
639
-
640
- /**
641
- * NewebPay AES-256-CBC 解密
642
- */
643
- export function decryptNewebPay(
644
- encryptedData: string,
645
- hashKey: string,
646
- hashIV: string
647
- ): Record<string, any> {
648
- const decipher = crypto.createDecipheriv('aes-256-cbc', hashKey, hashIV)
649
- decipher.setAutoPadding(true)
650
- let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
651
- decrypted += decipher.final('utf8')
652
-
653
- return Object.fromEntries(new URLSearchParams(decrypted))
654
- }
655
-
656
- /**
657
- * PAYUNi AES-256-GCM 加密
658
- */
659
- export function encryptPAYUNi(
660
- data: Record<string, any>,
661
- hashKey: string,
662
- hashIV: string
663
- ): { EncryptInfo: string; HashInfo: string } {
664
- // 1. JSON 字串化
665
- const jsonString = JSON.stringify(data)
666
-
667
- // 2. AES-256-GCM 加密
668
- const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
669
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
670
- encrypted += cipher.final('hex')
671
-
672
- // 3. 取得 Auth Tag (16 bytes)
673
- const authTag = cipher.getAuthTag().toString('hex')
674
-
675
- // 4. 組合加密資料
676
- const encryptInfo = encrypted + authTag
677
-
678
- // 5. SHA256 簽章
679
- const hashInfo = crypto
680
- .createHash('sha256')
681
- .update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
682
- .digest('hex')
683
- .toUpperCase()
684
-
685
- return {
686
- EncryptInfo: encryptInfo,
687
- HashInfo: hashInfo,
688
- }
689
- }
690
-
691
- /**
692
- * PAYUNi AES-256-GCM 解密
693
- */
694
- export function decryptPAYUNi(
695
- encryptedData: string,
696
- hashKey: string,
697
- hashIV: string
698
- ): Record<string, any> {
699
- // 1. 分離加密內容和 Auth Tag(最後 32 個字元)
700
- const encryptedContent = encryptedData.slice(0, -32)
701
- const authTag = Buffer.from(encryptedData.slice(-32), 'hex')
702
-
703
- // 2. AES-256-GCM 解密
704
- const decipher = crypto.createDecipheriv('aes-256-gcm', hashKey, hashIV)
705
- decipher.setAuthTag(authTag)
706
- let decrypted = decipher.update(encryptedContent, 'hex', 'utf8')
707
- decrypted += decipher.final('utf8')
708
-
709
- return JSON.parse(decrypted)
710
- }
711
-
712
- /**
713
- * 驗證簽章
714
- */
715
- export function verifySignature(
716
- params: Record<string, any>,
717
- signature: string,
718
- hashKey: string,
719
- hashIV: string,
720
- provider: 'ECPAY' | 'NEWEBPAY' | 'PAYUNI'
721
- ): boolean {
722
- switch (provider) {
723
- case 'ECPAY':
724
- const calculatedECPay = generateECPayCheckMacValue(params, hashKey, hashIV)
725
- return calculatedECPay === signature
726
- case 'NEWEBPAY':
727
- const { TradeSha } = encryptNewebPay(params, hashKey, hashIV)
728
- return TradeSha === signature
729
- case 'PAYUNI':
730
- const { HashInfo } = encryptPAYUNi(params, hashKey, hashIV)
731
- return HashInfo === signature
732
- default:
733
- return false
734
- }
735
- }
736
- ```
737
-
738
- **使用範例:**
739
-
740
- ```typescript
741
- import { generateECPayCheckMacValue, encryptNewebPay, encryptPAYUNi } from '@/lib/utils/payment-encryption'
742
-
743
- // ECPay 簽章
744
- const ecpayParams = {
745
- MerchantID: '3002607',
746
- MerchantTradeNo: 'ORD123456',
747
- TotalAmount: 1050,
748
- }
749
- const checkMacValue = generateECPayCheckMacValue(
750
- ecpayParams,
751
- 'pwFHCqoQZGmho4w6',
752
- 'EkRm7iFT261dpevs'
753
- )
754
- console.log('ECPay CheckMacValue:', checkMacValue)
755
-
756
- // NewebPay 加密
757
- const newebpayData = {
758
- MerchantID: 'MS12345678',
759
- MerchantOrderNo: 'MPG123456',
760
- Amt: 2500,
761
- }
762
- const { TradeInfo, TradeSha } = encryptNewebPay(
763
- newebpayData,
764
- 'your32BytesHashKeyHere123456',
765
- 'your16BytesIV123'
766
- )
767
- console.log('NewebPay TradeInfo:', TradeInfo.substring(0, 50) + '...')
768
- console.log('NewebPay TradeSha:', TradeSha)
769
-
770
- // PAYUNi 加密
771
- const payuniData = {
772
- MerchantID: 'UNI12345',
773
- MerchantOrderNo: 'UNI123456',
774
- Amount: 3000,
775
- }
776
- const { EncryptInfo, HashInfo } = encryptPAYUNi(
777
- payuniData,
778
- 'your32BytesHashKey',
779
- 'your16BytesIV'
780
- )
781
- console.log('PAYUNi EncryptInfo:', EncryptInfo.substring(0, 50) + '...')
782
- console.log('PAYUNi HashInfo:', HashInfo)
783
- ```
784
-
785
- ---
786
-
787
- ## 實戰場景
788
-
789
- ### 場景 1: 電商結帳流程整合
790
-
791
- **需求:** 完整的訂單建立 → 金流付款 → 付款通知 → 訂單查詢流程
792
-
793
- **步驟 1: 建立訂單並導向付款**
794
-
795
- ```typescript
796
- // app/api/checkout/route.ts
797
-
798
- import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
799
- import { prisma } from '@/lib/prisma'
800
-
801
- export async function POST(request: Request) {
802
- const { userId, cartItems, shippingInfo } = await request.json()
803
-
804
- // 1. 計算訂單金額
805
- const totalAmount = cartItems.reduce((sum, item) => {
806
- return sum + (item.price * item.quantity)
807
- }, 0)
808
-
809
- // 2. 產生訂單編號
810
- const merchantTradeNo = `ORD${Date.now()}${Math.random().toString(36).substring(2, 6).toUpperCase()}`
811
-
812
- // 3. 建立資料庫訂單
813
- const order = await prisma.order.create({
814
- data: {
815
- userId: userId,
816
- merchantTradeNo: merchantTradeNo,
817
- totalAmount: totalAmount,
818
- status: 'PENDING',
819
- shippingName: shippingInfo.name,
820
- shippingAddress: shippingInfo.address,
821
- shippingPhone: shippingInfo.phone,
822
- items: {
823
- create: cartItems.map(item => ({
824
- productId: item.productId,
825
- productName: item.name,
826
- quantity: item.quantity,
827
- price: item.price,
828
- }))
829
- }
830
- },
831
- include: { items: true }
832
- })
833
-
834
- // 4. 取得金流服務(使用者預設或指定)
835
- const service = await PaymentServiceFactory.getServiceForUser(userId)
836
-
837
- // 5. 建立付款訂單
838
- const itemNames = order.items.map(item => `${item.productName} x${item.quantity}`).join('|')
839
-
840
- const paymentResult = await service.createOrder(userId, {
841
- merchantTradeNo: merchantTradeNo,
842
- totalAmount: totalAmount,
843
- itemName: itemNames.substring(0, 200), // 限制長度
844
- returnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/return`,
845
- notifyURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/payment/callback`,
846
- email: shippingInfo.email,
847
- })
848
-
849
- // 6. 更新訂單記錄金流服務商
850
- await prisma.order.update({
851
- where: { id: order.id },
852
- data: {
853
- paymentProvider: paymentResult.provider || 'ECPAY',
854
- }
855
- })
856
-
857
- // 7. 回傳付款表單資料
858
- return Response.json({
859
- success: true,
860
- orderId: order.id,
861
- merchantTradeNo: merchantTradeNo,
862
- paymentForm: {
863
- action: paymentResult.formAction,
864
- method: paymentResult.formMethod,
865
- params: paymentResult.formParams,
866
- }
867
- })
868
- }
869
- ```
870
-
871
- **步驟 2: 處理付款通知回呼**
872
-
873
- ```typescript
874
- // app/api/payment/callback/route.ts
875
-
876
- import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
877
- import { prisma } from '@/lib/prisma'
878
-
879
- export async function POST(request: Request) {
880
- const formData = await request.formData()
881
- const params = Object.fromEntries(formData)
882
-
883
- console.log('[INFO] 收到付款通知:', params)
884
-
885
- try {
886
- // 1. 自動偵測服務商
887
- const provider = PaymentServiceFactory.detectProvider(params)
888
- const service = PaymentServiceFactory.getService(provider)
889
-
890
- // 2. 驗證簽章
891
- const isValid = service.verifyCallback(params)
892
- if (!isValid) {
893
- console.error('[ERROR] 簽章驗證失敗')
894
- return new Response('0|CheckMacValue Error', { status: 400 })
895
- }
896
-
897
- // 3. 取得訂單編號(各服務商欄位不同)
898
- const merchantTradeNo = params.MerchantTradeNo || params.MerchantOrderNo
899
-
900
- // 4. 更新訂單狀態
901
- const isPaid = params.RtnCode === '1' || params.Status === 'SUCCESS'
902
-
903
- await prisma.order.update({
904
- where: { merchantTradeNo },
905
- data: {
906
- status: isPaid ? 'PAID' : 'FAILED',
907
- paidAt: isPaid ? new Date() : null,
908
- tradeNo: params.TradeNo || params.TradeID, // 金流商訂單號
909
- paymentMethod: params.PaymentType || params.PaymentMethod,
910
- paymentDetails: JSON.stringify(params),
911
- failureReason: isPaid ? null : params.RtnMsg || params.Message,
912
- }
913
- })
914
-
915
- console.log(`[OK] 訂單 ${merchantTradeNo} 狀態更新為 ${isPaid ? 'PAID' : 'FAILED'}`)
916
-
917
- // 5. 付款成功後續處理
918
- if (isPaid) {
919
- // 發送通知郵件、扣減庫存、開立發票等
920
- // await sendOrderConfirmationEmail(merchantTradeNo)
921
- // await reduceInventory(merchantTradeNo)
922
- }
923
-
924
- // 6. 回應固定格式
925
- return new Response('1|OK', {
926
- status: 200,
927
- headers: { 'Content-Type': 'text/plain' }
928
- })
929
-
930
- } catch (error) {
931
- console.error('[ERROR] 付款通知處理失敗:', error)
932
- return new Response('0|Error', { status: 500 })
933
- }
934
- }
935
- ```
936
-
937
- **步驟 3: 查詢訂單狀態**
938
-
939
- ```typescript
940
- // app/api/payment/query/route.ts
941
-
942
- import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
943
- import { prisma } from '@/lib/prisma'
944
-
945
- export async function GET(request: Request) {
946
- const { searchParams } = new URL(request.url)
947
- const merchantTradeNo = searchParams.get('merchantTradeNo')
948
-
949
- if (!merchantTradeNo) {
950
- return Response.json({ error: '缺少訂單編號' }, { status: 400 })
951
- }
952
-
953
- try {
954
- // 1. 從資料庫取得訂單
955
- const order = await prisma.order.findUnique({
956
- where: { merchantTradeNo }
957
- })
958
-
959
- if (!order) {
960
- return Response.json({ error: '訂單不存在' }, { status: 404 })
961
- }
962
-
963
- // 2. 使用訂單記錄的服務商查詢
964
- const service = await PaymentServiceFactory.getServiceForOrder(merchantTradeNo)
965
- const queryResult = await service.queryOrder(order.userId, merchantTradeNo)
966
-
967
- // 3. 同步訂單狀態
968
- if (queryResult.success && queryResult.status !== order.status) {
969
- await prisma.order.update({
970
- where: { merchantTradeNo },
971
- data: {
972
- status: queryResult.status,
973
- paidAt: queryResult.paidAt,
974
- }
975
- })
976
- }
977
-
978
- return Response.json({
979
- success: true,
980
- order: {
981
- merchantTradeNo: order.merchantTradeNo,
982
- amount: order.totalAmount,
983
- status: queryResult.status || order.status,
984
- paidAt: queryResult.paidAt || order.paidAt,
985
- provider: order.paymentProvider,
986
- }
987
- })
988
-
989
- } catch (error) {
990
- console.error('[ERROR] 查詢訂單失敗:', error)
991
- return Response.json({ error: '查詢失敗' }, { status: 500 })
992
- }
993
- }
994
- ```
995
-
996
- **步驟 4: 前端整合**
997
-
998
- ```typescript
999
- // components/CheckoutButton.tsx
1000
-
1001
- 'use client'
1002
-
1003
- import { useState } from 'react'
1004
- import { Button } from '@/components/ui/button'
1005
-
1006
- export function CheckoutButton({ cartItems, shippingInfo }) {
1007
- const [loading, setLoading] = useState(false)
1008
-
1009
- const handleCheckout = async () => {
1010
- setLoading(true)
1011
- try {
1012
- // 1. 建立訂單
1013
- const response = await fetch('/api/checkout', {
1014
- method: 'POST',
1015
- headers: { 'Content-Type': 'application/json' },
1016
- body: JSON.stringify({
1017
- userId: 'user-123',
1018
- cartItems,
1019
- shippingInfo,
1020
- })
1021
- })
1022
-
1023
- const result = await response.json()
1024
-
1025
- if (!result.success) {
1026
- alert('結帳失敗')
1027
- return
1028
- }
1029
-
1030
- // 2. 提交付款表單
1031
- const form = document.createElement('form')
1032
- form.method = result.paymentForm.method
1033
- form.action = result.paymentForm.action
1034
- form.target = '_self'
1035
-
1036
- Object.entries(result.paymentForm.params).forEach(([key, value]) => {
1037
- const input = document.createElement('input')
1038
- input.type = 'hidden'
1039
- input.name = key
1040
- input.value = value as string
1041
- form.appendChild(input)
1042
- })
1043
-
1044
- document.body.appendChild(form)
1045
- form.submit()
1046
-
1047
- } catch (error) {
1048
- console.error('結帳失敗:', error)
1049
- alert('結帳失敗')
1050
- } finally {
1051
- setLoading(false)
1052
- }
1053
- }
1054
-
1055
- return (
1056
- <Button onClick={handleCheckout} disabled={loading} size="lg">
1057
- {loading ? '處理中...' : '前往付款'}
1058
- </Button>
1059
- )
1060
- }
1061
- ```
1062
-
1063
- ---
1064
-
1065
- ### 場景 2: 定期定額訂閱
1066
-
1067
- **需求:** 實作週期扣款功能(會員訂閱制)
1068
-
1069
- **步驟 1: 建立定期定額訂單**
1070
-
1071
- ```typescript
1072
- // app/api/subscription/create/route.ts
1073
-
1074
- import { ECPayPaymentService } from '@/lib/services/ecpay-payment-service'
1075
- import { prisma } from '@/lib/prisma'
1076
-
1077
- export async function POST(request: Request) {
1078
- const { userId, planId, email } = await request.json()
1079
-
1080
- // 1. 取得訂閱方案
1081
- const plan = await prisma.subscriptionPlan.findUnique({
1082
- where: { id: planId }
1083
- })
1084
-
1085
- if (!plan) {
1086
- return Response.json({ error: '方案不存在' }, { status: 404 })
1087
- }
1088
-
1089
- // 2. 產生訂閱編號
1090
- const merchantTradeNo = `SUB${Date.now()}`
1091
-
1092
- // 3. 建立訂閱記錄
1093
- const subscription = await prisma.subscription.create({
1094
- data: {
1095
- userId: userId,
1096
- planId: planId,
1097
- merchantTradeNo: merchantTradeNo,
1098
- status: 'PENDING',
1099
- amount: plan.price,
1100
- frequency: plan.frequency, // 'M' = 月, 'Y' = 年
1101
- totalTimes: plan.totalTimes || 999, // 999 = 無限次
1102
- }
1103
- })
1104
-
1105
- // 4. 建立 ECPay 定期定額訂單
1106
- const service = new ECPayPaymentService(false) // 測試環境
1107
-
1108
- const periodicData = {
1109
- MerchantTradeNo: merchantTradeNo,
1110
- MerchantTradeDate: new Date().toLocaleString('zh-TW', {
1111
- year: 'numeric',
1112
- month: '2-digit',
1113
- day: '2-digit',
1114
- hour: '2-digit',
1115
- minute: '2-digit',
1116
- second: '2-digit',
1117
- hour12: false
1118
- }).replace(/\//g, '/'),
1119
- TotalAmount: plan.price,
1120
- TradeDesc: `訂閱方案:${plan.name}`,
1121
- ItemName: plan.name,
1122
- ReturnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/subscription/callback`,
1123
- PeriodAmount: plan.price, // 每期金額
1124
- PeriodType: plan.frequency, // 'M' = 月, 'Y' = 年
1125
- Frequency: 1, // 每 1 個週期
1126
- ExecTimes: plan.totalTimes, // 執行次數
1127
- PeriodReturnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/subscription/periodic-callback`,
1128
- }
1129
-
1130
- const result = await service.createPeriodicOrder(userId, periodicData)
1131
-
1132
- return Response.json({
1133
- success: result.success,
1134
- subscriptionId: subscription.id,
1135
- paymentForm: {
1136
- action: result.formAction,
1137
- method: result.formMethod,
1138
- params: result.formParams,
1139
- }
1140
- })
1141
- }
1142
- ```
1143
-
1144
- **步驟 2: 處理首次授權回呼**
1145
-
1146
- ```typescript
1147
- // app/api/subscription/callback/route.ts
1148
-
1149
- import { prisma } from '@/lib/prisma'
1150
-
1151
- export async function POST(request: Request) {
1152
- const formData = await request.formData()
1153
- const params = Object.fromEntries(formData)
1154
-
1155
- console.log('[INFO] 收到訂閱授權通知:', params)
1156
-
1157
- const merchantTradeNo = params.MerchantTradeNo
1158
- const isPaid = params.RtnCode === '1'
1159
-
1160
- // 更新訂閱狀態
1161
- await prisma.subscription.update({
1162
- where: { merchantTradeNo },
1163
- data: {
1164
- status: isPaid ? 'ACTIVE' : 'FAILED',
1165
- gwsr: params.gwsr, // 綠界週期編號(重要:後續扣款需要)
1166
- firstPaidAt: isPaid ? new Date() : null,
1167
- }
1168
- })
1169
-
1170
- console.log(`[OK] 訂閱 ${merchantTradeNo} 授權${isPaid ? '成功' : '失敗'}`)
1171
-
1172
- return new Response('1|OK')
1173
- }
1174
- ```
1175
-
1176
- **步驟 3: 處理週期扣款通知**
1177
-
1178
- ```typescript
1179
- // app/api/subscription/periodic-callback/route.ts
1180
-
1181
- import { prisma } from '@/lib/prisma'
1182
-
1183
- export async function POST(request: Request) {
1184
- const formData = await request.formData()
1185
- const params = Object.fromEntries(formData)
1186
-
1187
- console.log('[INFO] 收到週期扣款通知:', params)
1188
-
1189
- const gwsr = params.gwsr // 綠界週期編號
1190
- const isPaid = params.RtnCode === '1'
1191
- const execTimes = parseInt(params.ExecTimes) // 當前第幾次扣款
1192
-
1193
- // 1. 更新訂閱記錄
1194
- const subscription = await prisma.subscription.findFirst({
1195
- where: { gwsr }
1196
- })
1197
-
1198
- if (subscription) {
1199
- // 2. 建立扣款記錄
1200
- await prisma.subscriptionPayment.create({
1201
- data: {
1202
- subscriptionId: subscription.id,
1203
- merchantTradeNo: params.MerchantTradeNo,
1204
- tradeNo: params.TradeNo,
1205
- amount: parseInt(params.amount),
1206
- execTimes: execTimes,
1207
- status: isPaid ? 'PAID' : 'FAILED',
1208
- paidAt: isPaid ? new Date() : null,
1209
- failureReason: isPaid ? null : params.RtnMsg,
1210
- }
1211
- })
1212
-
1213
- // 3. 更新訂閱狀態
1214
- await prisma.subscription.update({
1215
- where: { id: subscription.id },
1216
- data: {
1217
- currentExecTimes: execTimes,
1218
- lastPaidAt: isPaid ? new Date() : subscription.lastPaidAt,
1219
- }
1220
- })
1221
-
1222
- console.log(`[OK] 訂閱 ${subscription.merchantTradeNo} 第 ${execTimes} 次扣款${isPaid ? '成功' : '失敗'}`)
1223
-
1224
- // 4. 扣款成功後續處理
1225
- if (isPaid) {
1226
- // 延長會員期限、發送通知等
1227
- // await extendMembershipPeriod(subscription.userId)
1228
- }
1229
- }
1230
-
1231
- return new Response('1|OK')
1232
- }
1233
- ```
1234
-
1235
- ---
1236
-
1237
- ## 常見錯誤與修正
1238
-
1239
- ### 錯誤 1: CheckMacValue 計算錯誤
1240
-
1241
- **錯誤訊息:** ECPay 回傳 `10100058: 請確認檢查碼是否正確`
1242
-
1243
- **原因:** 參數排序錯誤或 URL Encode 不正確
1244
-
1245
- **修正前:**
1246
- ```typescript
1247
- // 錯誤:未排序參數
1248
- function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
1249
- const paramString = Object.entries(params)
1250
- .map(([k, v]) => `${k}=${v}`)
1251
- .join('&')
1252
-
1253
- const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
1254
- const hash = crypto.createHash('sha256').update(rawString).digest('hex')
1255
- return hash.toUpperCase()
1256
- }
1257
- ```
1258
-
1259
- **修正後:**
1260
- ```typescript
1261
- // 正確:排序 + URL Encode (lowercase)
1262
- function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
1263
- // 1. 移除 CheckMacValue 本身
1264
- const { CheckMacValue, ...cleanParams } = params
1265
-
1266
- // 2. 排序 keys
1267
- const sortedKeys = Object.keys(cleanParams).sort()
1268
-
1269
- // 3. 組合參數字串
1270
- const paramString = sortedKeys
1271
- .map(key => `${key}=${cleanParams[key]}`)
1272
- .join('&')
1273
-
1274
- // 4. 前後加上 HashKey/HashIV
1275
- const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
1276
-
1277
- // 5. URL Encode (lowercase)
1278
- const encoded = encodeURIComponent(rawString).toLowerCase()
1279
-
1280
- // 6. SHA256 + 大寫
1281
- const hash = crypto.createHash('sha256').update(encoded).digest('hex')
1282
- return hash.toUpperCase()
1283
- }
1284
- ```
1285
-
1286
- **驗證方法:**
1287
- ```typescript
1288
- // 測試範例
1289
- const params = {
1290
- MerchantID: '3002607',
1291
- MerchantTradeNo: 'ORD123456',
1292
- MerchantTradeDate: '2024/01/29 12:00:00',
1293
- TotalAmount: 1050,
1294
- }
1295
-
1296
- const checkMacValue = generateCheckMacValue(
1297
- params,
1298
- 'pwFHCqoQZGmho4w6',
1299
- 'EkRm7iFT261dpevs'
1300
- )
1301
-
1302
- console.log('計算結果:', checkMacValue)
1303
- // 應該與 ECPay 要求的值一致
1304
- ```
1305
-
1306
- ---
1307
-
1308
- ### 錯誤 2: AES 加密錯誤
1309
-
1310
- **錯誤訊息:** NewebPay 回傳 `TradeSha 錯誤` 或 PAYUNi 回傳 `HashInfo 錯誤`
1311
-
1312
- **原因:** Key/IV 長度錯誤或未附加 Auth Tag
1313
-
1314
- **修正前 (NewebPay):**
1315
- ```typescript
1316
- // 錯誤:Key/IV 長度不正確
1317
- function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string) {
1318
- const queryString = new URLSearchParams(data).toString()
1319
-
1320
- // 錯誤:未檢查 Key/IV 長度
1321
- const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
1322
- let encrypted = cipher.update(queryString, 'utf8', 'hex')
1323
- encrypted += cipher.final('hex')
1324
-
1325
- return encrypted
1326
- }
1327
- ```
1328
-
1329
- **修正後 (NewebPay):**
1330
- ```typescript
1331
- // 正確:確認 Key/IV 長度 + 計算 TradeSha
1332
- function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string) {
1333
- // 確認長度
1334
- if (hashKey.length !== 32) throw new Error('HashKey 必須 32 bytes')
1335
- if (hashIV.length !== 16) throw new Error('HashIV 必須 16 bytes')
1336
-
1337
- const queryString = new URLSearchParams(data).toString()
1338
-
1339
- // AES-256-CBC 加密
1340
- const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
1341
- cipher.setAutoPadding(true)
1342
- let encrypted = cipher.update(queryString, 'utf8', 'hex')
1343
- encrypted += cipher.final('hex')
1344
-
1345
- // 計算 TradeSha
1346
- const tradeSha = crypto
1347
- .createHash('sha256')
1348
- .update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
1349
- .digest('hex')
1350
- .toUpperCase()
1351
-
1352
- return {
1353
- TradeInfo: encrypted,
1354
- TradeSha: tradeSha
1355
- }
1356
- }
1357
- ```
1358
-
1359
- **修正前 (PAYUNi):**
1360
- ```typescript
1361
- // 錯誤:忘記附加 Auth Tag
1362
- function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string) {
1363
- const jsonString = JSON.stringify(data)
1364
-
1365
- const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
1366
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
1367
- encrypted += cipher.final('hex')
1368
-
1369
- // 錯誤:忘記取得 Auth Tag
1370
- return encrypted
1371
- }
1372
- ```
1373
-
1374
- **修正後 (PAYUNi):**
1375
- ```typescript
1376
- // 正確:附加 Auth Tag
1377
- function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string) {
1378
- const jsonString = JSON.stringify(data)
1379
-
1380
- // AES-256-GCM 加密
1381
- const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
1382
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
1383
- encrypted += cipher.final('hex')
1384
-
1385
- // 取得 Auth Tag(16 bytes = 32 hex chars)
1386
- const authTag = cipher.getAuthTag().toString('hex')
1387
-
1388
- // 組合:encrypted + authTag
1389
- const encryptInfo = encrypted + authTag
1390
-
1391
- // 計算 HashInfo
1392
- const hashInfo = crypto
1393
- .createHash('sha256')
1394
- .update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
1395
- .digest('hex')
1396
- .toUpperCase()
1397
-
1398
- return {
1399
- EncryptInfo: encryptInfo,
1400
- HashInfo: hashInfo
1401
- }
1402
- }
1403
- ```
1404
-
1405
- **測試工具:**
1406
- ```typescript
1407
- // 測試 NewebPay 加密/解密
1408
- const testData = { test: 'hello', amount: 1000 }
1409
- const { TradeInfo, TradeSha } = encryptNewebPay(testData, hashKey, hashIV)
1410
- const decrypted = decryptNewebPay(TradeInfo, hashKey, hashIV)
1411
- console.log('原始:', testData)
1412
- console.log('解密:', decrypted)
1413
- console.log('一致:', JSON.stringify(testData) === JSON.stringify(decrypted))
1414
-
1415
- // 測試 PAYUNi 加密/解密
1416
- const { EncryptInfo, HashInfo } = encryptPAYUNi(testData, hashKey, hashIV)
1417
- const decryptedPAYUNi = decryptPAYUNi(EncryptInfo, hashKey, hashIV)
1418
- console.log('原始:', testData)
1419
- console.log('解密:', decryptedPAYUNi)
1420
- console.log('一致:', JSON.stringify(testData) === JSON.stringify(decryptedPAYUNi))
1421
- ```
1422
-
1423
- ---
1424
-
1425
- **更多範例持續更新中...**
1
+ # 台灣金流 Skill - 完整範例集
2
+
3
+ 這份文件包含使用 `taiwan-payment` skill 的完整實作範例。
4
+
5
+ ## 目錄
6
+
7
+ 1. [基礎範例](#基礎範例)
8
+ 2. [進階範例](#進階範例)
9
+ 3. [實戰場景](#實戰場景)
10
+ 4. [常見錯誤與修正](#常見錯誤與修正)
11
+
12
+ ---
13
+
14
+ ## 基礎範例
15
+
16
+ ### 範例 1: ECPay 信用卡一次付清
17
+
18
+ **場景:** 客戶購買商品,金額 1050 元,使用綠界信用卡付款
19
+
20
+ **Claude 提示詞:**
21
+ ```
22
+ 使用 ECPay 測試環境建立信用卡付款
23
+ - 訂單金額: 1050 元
24
+ - 付款方式: 信用卡一次付清
25
+ - 商品名稱: 測試商品
26
+ ```
27
+
28
+ **生成的程式碼:**
29
+
30
+ ```typescript
31
+ import { ECPayPaymentService } from '@/lib/services/ecpay-payment-service'
32
+ import crypto from 'crypto'
33
+
34
+ async function createECPayOrder() {
35
+ const service = new ECPayPaymentService(false) // 測試環境
36
+
37
+ // 產生唯一訂單編號
38
+ const merchantTradeNo = `ORD${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`
39
+
40
+ const orderData = {
41
+ MerchantTradeNo: merchantTradeNo,
42
+ MerchantTradeDate: new Date().toLocaleString('zh-TW', {
43
+ year: 'numeric',
44
+ month: '2-digit',
45
+ day: '2-digit',
46
+ hour: '2-digit',
47
+ minute: '2-digit',
48
+ second: '2-digit',
49
+ hour12: false
50
+ }).replace(/\//g, '/'),
51
+ TotalAmount: 1050,
52
+ TradeDesc: '測試商品購買',
53
+ ItemName: '測試商品 x1',
54
+ ReturnURL: 'https://yourdomain.com/api/payment/callback',
55
+ ChoosePayment: 'Credit',
56
+ EncryptType: 1,
57
+ }
58
+
59
+ try {
60
+ const result = await service.createOrder('user-123', orderData)
61
+
62
+ if (result.success) {
63
+ console.log('[OK] 訂單建立成功')
64
+ console.log('訂單編號:', result.merchantTradeNo)
65
+ console.log('請導向付款頁:', result.formAction)
66
+
67
+ // 儲存訂單
68
+ await prisma.order.create({
69
+ data: {
70
+ userId: 'user-123',
71
+ merchantTradeNo: result.merchantTradeNo,
72
+ amount: 1050,
73
+ status: 'PENDING',
74
+ paymentProvider: 'ECPAY',
75
+ paymentMethod: 'Credit',
76
+ }
77
+ })
78
+
79
+ return result
80
+ } else {
81
+ console.error('[ERROR] 建立失敗:', result.msg)
82
+ }
83
+ } catch (error) {
84
+ console.error('付款建立異常:', error)
85
+ throw error
86
+ }
87
+ }
88
+
89
+ // 前端提交表單
90
+ function submitToECPay(formData: { action: string, params: Record<string, string> }) {
91
+ const form = document.createElement('form')
92
+ form.method = 'POST'
93
+ form.action = formData.action
94
+ form.target = '_self'
95
+
96
+ Object.entries(formData.params).forEach(([key, value]) => {
97
+ const input = document.createElement('input')
98
+ input.type = 'hidden'
99
+ input.name = key
100
+ input.value = value
101
+ form.appendChild(input)
102
+ })
103
+
104
+ document.body.appendChild(form)
105
+ form.submit()
106
+ }
107
+ ```
108
+
109
+ **預期輸出:**
110
+ ```
111
+ [OK] 訂單建立成功
112
+ 訂單編號: ORD1738123456ABC7
113
+ 請導向付款頁: https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5
114
+ ```
115
+
116
+ ---
117
+
118
+ ### 範例 2: NewebPay MPG 整合支付
119
+
120
+ **場景:** 使用藍新金流整合支付頁,支援信用卡、ATM、超商代碼
121
+
122
+ **Claude 提示詞:**
123
+ ```
124
+ 使用 NewebPay MPG 建立整合付款
125
+ - 金額: 2500 元
126
+ - 付款方式: 信用卡、ATM、超商代碼
127
+ - 使用 AES 加密
128
+ ```
129
+
130
+ **生成的程式碼:**
131
+
132
+ ```typescript
133
+ import crypto from 'crypto'
134
+
135
+ class NewebPayService {
136
+ private merchantID: string
137
+ private hashKey: string
138
+ private hashIV: string
139
+ private apiUrl: string
140
+
141
+ constructor(isProd: boolean = false) {
142
+ this.merchantID = isProd
143
+ ? process.env.NEWEBPAY_MERCHANT_ID_PROD!
144
+ : process.env.NEWEBPAY_MERCHANT_ID_TEST!
145
+ this.hashKey = isProd
146
+ ? process.env.NEWEBPAY_HASH_KEY_PROD!
147
+ : process.env.NEWEBPAY_HASH_KEY_TEST!
148
+ this.hashIV = isProd
149
+ ? process.env.NEWEBPAY_HASH_IV_PROD!
150
+ : process.env.NEWEBPAY_HASH_IV_TEST!
151
+ this.apiUrl = isProd
152
+ ? 'https://core.newebpay.com/MPG/mpg_gateway'
153
+ : 'https://ccore.newebpay.com/MPG/mpg_gateway'
154
+ }
155
+
156
+ private encrypt(data: Record<string, any>): { TradeInfo: string, TradeSha: string } {
157
+ // 1. 轉換為查詢字串
158
+ const queryString = new URLSearchParams(data).toString()
159
+
160
+ // 2. AES-256-CBC 加密
161
+ const cipher = crypto.createCipheriv('aes-256-cbc', this.hashKey, this.hashIV)
162
+ cipher.setAutoPadding(true)
163
+ let encrypted = cipher.update(queryString, 'utf8', 'hex')
164
+ encrypted += cipher.final('hex')
165
+
166
+ // 3. 計算 SHA256
167
+ const tradeSha = crypto
168
+ .createHash('sha256')
169
+ .update(`HashKey=${this.hashKey}&${encrypted}&HashIV=${this.hashIV}`)
170
+ .digest('hex')
171
+ .toUpperCase()
172
+
173
+ return {
174
+ TradeInfo: encrypted,
175
+ TradeSha: tradeSha
176
+ }
177
+ }
178
+
179
+ async createMPGOrder(userId: string, orderData: any) {
180
+ const merchantOrderNo = `MPG${Date.now()}`
181
+
182
+ const tradeInfo = {
183
+ MerchantID: this.merchantID,
184
+ RespondType: 'JSON',
185
+ TimeStamp: Math.floor(Date.now() / 1000).toString(),
186
+ Version: '2.0',
187
+ MerchantOrderNo: merchantOrderNo,
188
+ Amt: orderData.amount,
189
+ ItemDesc: orderData.itemDesc || '商品購買',
190
+ ReturnURL: orderData.returnURL,
191
+ NotifyURL: orderData.notifyURL,
192
+ Email: orderData.email,
193
+ // 啟用付款方式
194
+ CREDIT: 1, // 信用卡
195
+ VACC: 1, // ATM
196
+ CVS: 1, // 超商代碼
197
+ }
198
+
199
+ // 加密
200
+ const { TradeInfo, TradeSha } = this.encrypt(tradeInfo)
201
+
202
+ console.log('[OK] NewebPay MPG 訂單建立')
203
+ console.log('訂單編號:', merchantOrderNo)
204
+ console.log('加密 TradeInfo 長度:', TradeInfo.length)
205
+
206
+ return {
207
+ success: true,
208
+ formAction: this.apiUrl,
209
+ formMethod: 'POST',
210
+ formParams: {
211
+ MerchantID: this.merchantID,
212
+ TradeInfo: TradeInfo,
213
+ TradeSha: TradeSha,
214
+ Version: '2.0'
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // 使用範例
221
+ async function createNewebPayMPG() {
222
+ const service = new NewebPayService(false) // 測試環境
223
+
224
+ const result = await service.createMPGOrder('user-123', {
225
+ amount: 2500,
226
+ itemDesc: '網站年費方案',
227
+ returnURL: 'https://yourdomain.com/payment/return',
228
+ notifyURL: 'https://yourdomain.com/api/payment/callback',
229
+ email: 'test@example.com'
230
+ })
231
+
232
+ return result
233
+ }
234
+ ```
235
+
236
+ **重要說明:**
237
+ - NewebPay 使用 AES-256-CBC 雙層加密(TradeInfo + TradeSha)
238
+ - HashKey 必須 32 bytes,HashIV 必須 16 bytes
239
+ - MPG 整合支付頁會根據啟用的付款方式顯示選項
240
+
241
+ **預期輸出:**
242
+ ```
243
+ [OK] NewebPay MPG 訂單建立
244
+ 訂單編號: MPG1738123456789
245
+ 加密 TradeInfo 長度: 384
246
+ ```
247
+
248
+ ---
249
+
250
+ ### 範例 3: PAYUNi RESTful API
251
+
252
+ **場景:** 使用統一金流 RESTful JSON API 建立付款
253
+
254
+ **Claude 提示詞:**
255
+ ```
256
+ 使用 PAYUNi RESTful API 建立信用卡付款
257
+ - 金額: 3000 元
258
+ - JSON 格式
259
+ - AES-256-GCM 加密
260
+ ```
261
+
262
+ **生成的程式碼:**
263
+
264
+ ```typescript
265
+ import crypto from 'crypto'
266
+ import axios from 'axios'
267
+
268
+ class PAYUNiService {
269
+ private merchantID: string
270
+ private hashKey: string
271
+ private hashIV: string
272
+ private apiUrl: string
273
+
274
+ constructor(isProd: boolean = false) {
275
+ this.merchantID = isProd
276
+ ? process.env.PAYUNI_MERCHANT_ID_PROD!
277
+ : process.env.PAYUNI_MERCHANT_ID_TEST!
278
+ this.hashKey = isProd
279
+ ? process.env.PAYUNI_HASH_KEY_PROD!
280
+ : process.env.PAYUNI_HASH_KEY_TEST!
281
+ this.hashIV = isProd
282
+ ? process.env.PAYUNI_HASH_IV_PROD!
283
+ : process.env.PAYUNI_HASH_IV_TEST!
284
+ this.apiUrl = isProd
285
+ ? 'https://api.payuni.com.tw/api/upp'
286
+ : 'https://sandbox-api.payuni.com.tw/api/upp'
287
+ }
288
+
289
+ private encrypt(data: Record<string, any>): { EncryptInfo: string, HashInfo: string } {
290
+ // 1. JSON 字串化
291
+ const jsonString = JSON.stringify(data)
292
+
293
+ // 2. AES-256-GCM 加密
294
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.hashKey, this.hashIV)
295
+ let encrypted = cipher.update(jsonString, 'utf8', 'hex')
296
+ encrypted += cipher.final('hex')
297
+
298
+ // 3. 取得 Auth Tag
299
+ const authTag = cipher.getAuthTag().toString('hex')
300
+
301
+ // 4. 組合加密資料
302
+ const encryptInfo = encrypted + authTag
303
+
304
+ // 5. SHA256 簽章
305
+ const hashInfo = crypto
306
+ .createHash('sha256')
307
+ .update(`HashKey=${this.hashKey}&${encryptInfo}&HashIV=${this.hashIV}`)
308
+ .digest('hex')
309
+ .toUpperCase()
310
+
311
+ return {
312
+ EncryptInfo: encryptInfo,
313
+ HashInfo: hashInfo
314
+ }
315
+ }
316
+
317
+ async createOrder(userId: string, orderData: any) {
318
+ const merchantOrderNo = `UNI${Date.now()}`
319
+
320
+ const tradeData = {
321
+ MerchantID: this.merchantID,
322
+ MerchantOrderNo: merchantOrderNo,
323
+ Amount: orderData.amount,
324
+ ItemDescription: orderData.itemDesc || '商品購買',
325
+ ReturnURL: orderData.returnURL,
326
+ NotifyURL: orderData.notifyURL,
327
+ Email: orderData.email,
328
+ PaymentMethod: 'CREDIT', // 信用卡
329
+ TimeStamp: Math.floor(Date.now() / 1000)
330
+ }
331
+
332
+ // 加密
333
+ const { EncryptInfo, HashInfo } = this.encrypt(tradeData)
334
+
335
+ try {
336
+ // RESTful POST 請求
337
+ const response = await axios.post(this.apiUrl, {
338
+ MerchantID: this.merchantID,
339
+ EncryptInfo: EncryptInfo,
340
+ HashInfo: HashInfo
341
+ }, {
342
+ headers: {
343
+ 'Content-Type': 'application/json'
344
+ }
345
+ })
346
+
347
+ console.log('[OK] PAYUNi 訂單建立')
348
+ console.log('訂單編號:', merchantOrderNo)
349
+ console.log('回應狀態:', response.data.Status)
350
+
351
+ return {
352
+ success: response.data.Status === 'SUCCESS',
353
+ merchantTradeNo: merchantOrderNo,
354
+ paymentUrl: response.data.Data?.PaymentURL,
355
+ message: response.data.Message
356
+ }
357
+ } catch (error) {
358
+ console.error('[ERROR] PAYUNi 請求失敗:', error)
359
+ throw error
360
+ }
361
+ }
362
+ }
363
+
364
+ // 使用範例
365
+ async function createPAYUNiOrder() {
366
+ const service = new PAYUNiService(false)
367
+
368
+ const result = await service.createOrder('user-123', {
369
+ amount: 3000,
370
+ itemDesc: '會員升級方案',
371
+ returnURL: 'https://yourdomain.com/payment/return',
372
+ notifyURL: 'https://yourdomain.com/api/payment/callback',
373
+ email: 'test@example.com'
374
+ })
375
+
376
+ if (result.success) {
377
+ console.log('請導向:', result.paymentUrl)
378
+ }
379
+
380
+ return result
381
+ }
382
+ ```
383
+
384
+ **重要說明:**
385
+ - PAYUNi 使用 RESTful JSON API(與 ECPay/NewebPay 的 Form POST 不同)
386
+ - AES-256-GCM 加密需要附加 Auth Tag(最後 32 字元)
387
+ - 回應為 JSON 格式,包含 PaymentURL 導向連結
388
+
389
+ **預期輸出:**
390
+ ```
391
+ [OK] PAYUNi 訂單建立
392
+ 訂單編號: UNI1738123456789
393
+ 回應狀態: SUCCESS
394
+ 請導向: https://sandbox-api.payuni.com.tw/trade/payment?token=xxx
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 進階範例
400
+
401
+ ### 範例 4: 服務工廠模式
402
+
403
+ **場景:** 建立金流服務工廠,動態切換三家服務商
404
+
405
+ **Claude 提示詞:**
406
+ ```
407
+ 建立金流服務工廠,支援 ECPay、NewebPay、PAYUNi 動態切換
408
+ ```
409
+
410
+ **生成的程式碼:**
411
+
412
+ ```typescript
413
+ // lib/services/payment-provider.ts - 介面定義
414
+
415
+ export interface PaymentService {
416
+ createOrder(userId: string, data: PaymentOrderData): Promise<PaymentOrderResponse>
417
+ queryOrder(userId: string, merchantTradeNo: string): Promise<PaymentQueryResponse>
418
+ refundOrder(userId: string, tradeNo: string, amount: number): Promise<PaymentRefundResponse>
419
+ verifyCallback(params: Record<string, any>): boolean
420
+ }
421
+
422
+ export interface PaymentOrderData {
423
+ merchantTradeNo: string
424
+ totalAmount: number
425
+ itemName: string
426
+ returnURL: string
427
+ notifyURL?: string
428
+ email?: string
429
+ paymentMethod?: string
430
+ }
431
+
432
+ export interface PaymentOrderResponse {
433
+ success: boolean
434
+ merchantTradeNo: string
435
+ formAction: string
436
+ formMethod: string
437
+ formParams: Record<string, string>
438
+ msg?: string
439
+ }
440
+
441
+ // lib/services/payment-service-factory.ts - 工廠類別
442
+
443
+ import { PaymentService } from './payment-provider'
444
+ import { ECPayPaymentService } from './ecpay-payment-service'
445
+ import { NewebPayPaymentService } from './newebpay-payment-service'
446
+ import { PAYUNiPaymentService } from './payuni-payment-service'
447
+ import { prisma } from '@/lib/prisma'
448
+
449
+ type PaymentProvider = 'ECPAY' | 'NEWEBPAY' | 'PAYUNI'
450
+
451
+ export class PaymentServiceFactory {
452
+ /**
453
+ * 根據服務商名稱取得服務實例
454
+ */
455
+ static getService(
456
+ provider: PaymentProvider,
457
+ isProd: boolean = false
458
+ ): PaymentService {
459
+ switch (provider) {
460
+ case 'ECPAY':
461
+ return new ECPayPaymentService(isProd)
462
+ case 'NEWEBPAY':
463
+ return new NewebPayPaymentService(isProd)
464
+ case 'PAYUNI':
465
+ return new PAYUNiPaymentService(isProd)
466
+ default:
467
+ throw new Error(`不支援的金流服務商: ${provider}`)
468
+ }
469
+ }
470
+
471
+ /**
472
+ * 根據使用者設定取得服務實例
473
+ */
474
+ static async getServiceForUser(userId: string): Promise<PaymentService> {
475
+ const settings = await prisma.paymentSettings.findUnique({
476
+ where: { userId },
477
+ })
478
+
479
+ if (!settings || !settings.defaultProvider) {
480
+ throw new Error('未設定預設金流服務商')
481
+ }
482
+
483
+ return this.getService(
484
+ settings.defaultProvider as PaymentProvider,
485
+ settings.isProduction
486
+ )
487
+ }
488
+
489
+ /**
490
+ * 根據訂單取得服務實例(用於查詢/退款)
491
+ */
492
+ static async getServiceForOrder(merchantTradeNo: string): Promise<PaymentService> {
493
+ const order = await prisma.order.findUnique({
494
+ where: { merchantTradeNo },
495
+ })
496
+
497
+ if (!order || !order.paymentProvider) {
498
+ throw new Error('訂單不存在或未記錄金流服務商')
499
+ }
500
+
501
+ return this.getService(
502
+ order.paymentProvider as PaymentProvider,
503
+ order.isProduction
504
+ )
505
+ }
506
+
507
+ /**
508
+ * 取得所有可用的服務商
509
+ */
510
+ static getAvailableProviders(): PaymentProvider[] {
511
+ return ['ECPAY', 'NEWEBPAY', 'PAYUNI']
512
+ }
513
+
514
+ /**
515
+ * 自動偵測服務商(根據回呼參數)
516
+ */
517
+ static detectProvider(params: Record<string, any>): PaymentProvider {
518
+ if (params.CheckMacValue && params.MerchantTradeNo) {
519
+ return 'ECPAY'
520
+ }
521
+ if (params.TradeInfo && params.TradeSha) {
522
+ return 'NEWEBPAY'
523
+ }
524
+ if (params.EncryptInfo && params.HashInfo) {
525
+ return 'PAYUNI'
526
+ }
527
+ throw new Error('無法識別金流服務商')
528
+ }
529
+ }
530
+ ```
531
+
532
+ **使用範例:**
533
+
534
+ ```typescript
535
+ // API Route: app/api/payment/create/route.ts
536
+
537
+ import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
538
+
539
+ export async function POST(request: Request) {
540
+ const { userId, amount, itemName, provider } = await request.json()
541
+
542
+ // 方式 1: 使用指定服務商
543
+ const service = PaymentServiceFactory.getService(provider || 'ECPAY')
544
+
545
+ // 方式 2: 使用使用者預設服務商
546
+ // const service = await PaymentServiceFactory.getServiceForUser(userId)
547
+
548
+ const result = await service.createOrder(userId, {
549
+ merchantTradeNo: `ORD${Date.now()}`,
550
+ totalAmount: amount,
551
+ itemName: itemName,
552
+ returnURL: 'https://yourdomain.com/payment/return',
553
+ notifyURL: 'https://yourdomain.com/api/payment/callback',
554
+ })
555
+
556
+ return Response.json(result)
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ### 範例 5: 加密輔助函數
563
+
564
+ **場景:** 建立三種加密方式的 helper functions
565
+
566
+ **Claude 提示詞:**
567
+ ```
568
+ 建立加密工具,包含 ECPay SHA256、NewebPay AES-CBC、PAYUNi AES-GCM
569
+ ```
570
+
571
+ **生成的程式碼:**
572
+
573
+ ```typescript
574
+ // lib/utils/payment-encryption.ts
575
+
576
+ import crypto from 'crypto'
577
+
578
+ /**
579
+ * ECPay CheckMacValue 計算(SHA256)
580
+ */
581
+ export function generateECPayCheckMacValue(
582
+ params: Record<string, any>,
583
+ hashKey: string,
584
+ hashIV: string
585
+ ): string {
586
+ // 1. 移除 CheckMacValue 本身
587
+ const { CheckMacValue, ...cleanParams } = params
588
+
589
+ // 2. 依照 key 排序(字母順序)
590
+ const sortedKeys = Object.keys(cleanParams).sort()
591
+
592
+ // 3. 組合參數字串
593
+ const paramString = sortedKeys
594
+ .map(key => `${key}=${cleanParams[key]}`)
595
+ .join('&')
596
+
597
+ // 4. 前後加上 HashKey 和 HashIV
598
+ const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
599
+
600
+ // 5. URL Encode (lowercase)
601
+ const encoded = encodeURIComponent(rawString).toLowerCase()
602
+
603
+ // 6. SHA256 雜湊
604
+ const hash = crypto.createHash('sha256').update(encoded).digest('hex')
605
+
606
+ // 7. 轉大寫
607
+ return hash.toUpperCase()
608
+ }
609
+
610
+ /**
611
+ * NewebPay AES-256-CBC 加密
612
+ */
613
+ export function encryptNewebPay(
614
+ data: Record<string, any>,
615
+ hashKey: string,
616
+ hashIV: string
617
+ ): { TradeInfo: string; TradeSha: string } {
618
+ // 1. 轉換為查詢字串
619
+ const queryString = new URLSearchParams(data).toString()
620
+
621
+ // 2. AES-256-CBC 加密
622
+ const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
623
+ cipher.setAutoPadding(true)
624
+ let encrypted = cipher.update(queryString, 'utf8', 'hex')
625
+ encrypted += cipher.final('hex')
626
+
627
+ // 3. 計算 SHA256
628
+ const tradeSha = crypto
629
+ .createHash('sha256')
630
+ .update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
631
+ .digest('hex')
632
+ .toUpperCase()
633
+
634
+ return {
635
+ TradeInfo: encrypted,
636
+ TradeSha: tradeSha,
637
+ }
638
+ }
639
+
640
+ /**
641
+ * NewebPay AES-256-CBC 解密
642
+ */
643
+ export function decryptNewebPay(
644
+ encryptedData: string,
645
+ hashKey: string,
646
+ hashIV: string
647
+ ): Record<string, any> {
648
+ const decipher = crypto.createDecipheriv('aes-256-cbc', hashKey, hashIV)
649
+ decipher.setAutoPadding(true)
650
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
651
+ decrypted += decipher.final('utf8')
652
+
653
+ return Object.fromEntries(new URLSearchParams(decrypted))
654
+ }
655
+
656
+ /**
657
+ * PAYUNi AES-256-GCM 加密
658
+ */
659
+ export function encryptPAYUNi(
660
+ data: Record<string, any>,
661
+ hashKey: string,
662
+ hashIV: string
663
+ ): { EncryptInfo: string; HashInfo: string } {
664
+ // 1. JSON 字串化
665
+ const jsonString = JSON.stringify(data)
666
+
667
+ // 2. AES-256-GCM 加密
668
+ const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
669
+ let encrypted = cipher.update(jsonString, 'utf8', 'hex')
670
+ encrypted += cipher.final('hex')
671
+
672
+ // 3. 取得 Auth Tag (16 bytes)
673
+ const authTag = cipher.getAuthTag().toString('hex')
674
+
675
+ // 4. 組合加密資料
676
+ const encryptInfo = encrypted + authTag
677
+
678
+ // 5. SHA256 簽章
679
+ const hashInfo = crypto
680
+ .createHash('sha256')
681
+ .update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
682
+ .digest('hex')
683
+ .toUpperCase()
684
+
685
+ return {
686
+ EncryptInfo: encryptInfo,
687
+ HashInfo: hashInfo,
688
+ }
689
+ }
690
+
691
+ /**
692
+ * PAYUNi AES-256-GCM 解密
693
+ */
694
+ export function decryptPAYUNi(
695
+ encryptedData: string,
696
+ hashKey: string,
697
+ hashIV: string
698
+ ): Record<string, any> {
699
+ // 1. 分離加密內容和 Auth Tag(最後 32 個字元)
700
+ const encryptedContent = encryptedData.slice(0, -32)
701
+ const authTag = Buffer.from(encryptedData.slice(-32), 'hex')
702
+
703
+ // 2. AES-256-GCM 解密
704
+ const decipher = crypto.createDecipheriv('aes-256-gcm', hashKey, hashIV)
705
+ decipher.setAuthTag(authTag)
706
+ let decrypted = decipher.update(encryptedContent, 'hex', 'utf8')
707
+ decrypted += decipher.final('utf8')
708
+
709
+ return JSON.parse(decrypted)
710
+ }
711
+
712
+ /**
713
+ * 驗證簽章
714
+ */
715
+ export function verifySignature(
716
+ params: Record<string, any>,
717
+ signature: string,
718
+ hashKey: string,
719
+ hashIV: string,
720
+ provider: 'ECPAY' | 'NEWEBPAY' | 'PAYUNI'
721
+ ): boolean {
722
+ switch (provider) {
723
+ case 'ECPAY':
724
+ const calculatedECPay = generateECPayCheckMacValue(params, hashKey, hashIV)
725
+ return calculatedECPay === signature
726
+ case 'NEWEBPAY':
727
+ const { TradeSha } = encryptNewebPay(params, hashKey, hashIV)
728
+ return TradeSha === signature
729
+ case 'PAYUNI':
730
+ const { HashInfo } = encryptPAYUNi(params, hashKey, hashIV)
731
+ return HashInfo === signature
732
+ default:
733
+ return false
734
+ }
735
+ }
736
+ ```
737
+
738
+ **使用範例:**
739
+
740
+ ```typescript
741
+ import { generateECPayCheckMacValue, encryptNewebPay, encryptPAYUNi } from '@/lib/utils/payment-encryption'
742
+
743
+ // ECPay 簽章
744
+ const ecpayParams = {
745
+ MerchantID: '3002607',
746
+ MerchantTradeNo: 'ORD123456',
747
+ TotalAmount: 1050,
748
+ }
749
+ const checkMacValue = generateECPayCheckMacValue(
750
+ ecpayParams,
751
+ 'pwFHCqoQZGmho4w6',
752
+ 'EkRm7iFT261dpevs'
753
+ )
754
+ console.log('ECPay CheckMacValue:', checkMacValue)
755
+
756
+ // NewebPay 加密
757
+ const newebpayData = {
758
+ MerchantID: 'MS12345678',
759
+ MerchantOrderNo: 'MPG123456',
760
+ Amt: 2500,
761
+ }
762
+ const { TradeInfo, TradeSha } = encryptNewebPay(
763
+ newebpayData,
764
+ 'your32BytesHashKeyHere123456',
765
+ 'your16BytesIV123'
766
+ )
767
+ console.log('NewebPay TradeInfo:', TradeInfo.substring(0, 50) + '...')
768
+ console.log('NewebPay TradeSha:', TradeSha)
769
+
770
+ // PAYUNi 加密
771
+ const payuniData = {
772
+ MerchantID: 'UNI12345',
773
+ MerchantOrderNo: 'UNI123456',
774
+ Amount: 3000,
775
+ }
776
+ const { EncryptInfo, HashInfo } = encryptPAYUNi(
777
+ payuniData,
778
+ 'your32BytesHashKey',
779
+ 'your16BytesIV'
780
+ )
781
+ console.log('PAYUNi EncryptInfo:', EncryptInfo.substring(0, 50) + '...')
782
+ console.log('PAYUNi HashInfo:', HashInfo)
783
+ ```
784
+
785
+ ---
786
+
787
+ ## 實戰場景
788
+
789
+ ### 場景 1: 電商結帳流程整合
790
+
791
+ **需求:** 完整的訂單建立 → 金流付款 → 付款通知 → 訂單查詢流程
792
+
793
+ **步驟 1: 建立訂單並導向付款**
794
+
795
+ ```typescript
796
+ // app/api/checkout/route.ts
797
+
798
+ import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
799
+ import { prisma } from '@/lib/prisma'
800
+
801
+ export async function POST(request: Request) {
802
+ const { userId, cartItems, shippingInfo } = await request.json()
803
+
804
+ // 1. 計算訂單金額
805
+ const totalAmount = cartItems.reduce((sum, item) => {
806
+ return sum + (item.price * item.quantity)
807
+ }, 0)
808
+
809
+ // 2. 產生訂單編號
810
+ const merchantTradeNo = `ORD${Date.now()}${Math.random().toString(36).substring(2, 6).toUpperCase()}`
811
+
812
+ // 3. 建立資料庫訂單
813
+ const order = await prisma.order.create({
814
+ data: {
815
+ userId: userId,
816
+ merchantTradeNo: merchantTradeNo,
817
+ totalAmount: totalAmount,
818
+ status: 'PENDING',
819
+ shippingName: shippingInfo.name,
820
+ shippingAddress: shippingInfo.address,
821
+ shippingPhone: shippingInfo.phone,
822
+ items: {
823
+ create: cartItems.map(item => ({
824
+ productId: item.productId,
825
+ productName: item.name,
826
+ quantity: item.quantity,
827
+ price: item.price,
828
+ }))
829
+ }
830
+ },
831
+ include: { items: true }
832
+ })
833
+
834
+ // 4. 取得金流服務(使用者預設或指定)
835
+ const service = await PaymentServiceFactory.getServiceForUser(userId)
836
+
837
+ // 5. 建立付款訂單
838
+ const itemNames = order.items.map(item => `${item.productName} x${item.quantity}`).join('|')
839
+
840
+ const paymentResult = await service.createOrder(userId, {
841
+ merchantTradeNo: merchantTradeNo,
842
+ totalAmount: totalAmount,
843
+ itemName: itemNames.substring(0, 200), // 限制長度
844
+ returnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/return`,
845
+ notifyURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/payment/callback`,
846
+ email: shippingInfo.email,
847
+ })
848
+
849
+ // 6. 更新訂單記錄金流服務商
850
+ await prisma.order.update({
851
+ where: { id: order.id },
852
+ data: {
853
+ paymentProvider: paymentResult.provider || 'ECPAY',
854
+ }
855
+ })
856
+
857
+ // 7. 回傳付款表單資料
858
+ return Response.json({
859
+ success: true,
860
+ orderId: order.id,
861
+ merchantTradeNo: merchantTradeNo,
862
+ paymentForm: {
863
+ action: paymentResult.formAction,
864
+ method: paymentResult.formMethod,
865
+ params: paymentResult.formParams,
866
+ }
867
+ })
868
+ }
869
+ ```
870
+
871
+ **步驟 2: 處理付款通知回呼**
872
+
873
+ ```typescript
874
+ // app/api/payment/callback/route.ts
875
+
876
+ import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
877
+ import { prisma } from '@/lib/prisma'
878
+
879
+ export async function POST(request: Request) {
880
+ const formData = await request.formData()
881
+ const params = Object.fromEntries(formData)
882
+
883
+ console.log('[INFO] 收到付款通知:', params)
884
+
885
+ try {
886
+ // 1. 自動偵測服務商
887
+ const provider = PaymentServiceFactory.detectProvider(params)
888
+ const service = PaymentServiceFactory.getService(provider)
889
+
890
+ // 2. 驗證簽章
891
+ const isValid = service.verifyCallback(params)
892
+ if (!isValid) {
893
+ console.error('[ERROR] 簽章驗證失敗')
894
+ return new Response('0|CheckMacValue Error', { status: 400 })
895
+ }
896
+
897
+ // 3. 取得訂單編號(各服務商欄位不同)
898
+ const merchantTradeNo = params.MerchantTradeNo || params.MerchantOrderNo
899
+
900
+ // 4. 更新訂單狀態
901
+ const isPaid = params.RtnCode === '1' || params.Status === 'SUCCESS'
902
+
903
+ await prisma.order.update({
904
+ where: { merchantTradeNo },
905
+ data: {
906
+ status: isPaid ? 'PAID' : 'FAILED',
907
+ paidAt: isPaid ? new Date() : null,
908
+ tradeNo: params.TradeNo || params.TradeID, // 金流商訂單號
909
+ paymentMethod: params.PaymentType || params.PaymentMethod,
910
+ paymentDetails: JSON.stringify(params),
911
+ failureReason: isPaid ? null : params.RtnMsg || params.Message,
912
+ }
913
+ })
914
+
915
+ console.log(`[OK] 訂單 ${merchantTradeNo} 狀態更新為 ${isPaid ? 'PAID' : 'FAILED'}`)
916
+
917
+ // 5. 付款成功後續處理
918
+ if (isPaid) {
919
+ // 發送通知郵件、扣減庫存、開立發票等
920
+ // await sendOrderConfirmationEmail(merchantTradeNo)
921
+ // await reduceInventory(merchantTradeNo)
922
+ }
923
+
924
+ // 6. 回應固定格式
925
+ return new Response('1|OK', {
926
+ status: 200,
927
+ headers: { 'Content-Type': 'text/plain' }
928
+ })
929
+
930
+ } catch (error) {
931
+ console.error('[ERROR] 付款通知處理失敗:', error)
932
+ return new Response('0|Error', { status: 500 })
933
+ }
934
+ }
935
+ ```
936
+
937
+ **步驟 3: 查詢訂單狀態**
938
+
939
+ ```typescript
940
+ // app/api/payment/query/route.ts
941
+
942
+ import { PaymentServiceFactory } from '@/lib/services/payment-service-factory'
943
+ import { prisma } from '@/lib/prisma'
944
+
945
+ export async function GET(request: Request) {
946
+ const { searchParams } = new URL(request.url)
947
+ const merchantTradeNo = searchParams.get('merchantTradeNo')
948
+
949
+ if (!merchantTradeNo) {
950
+ return Response.json({ error: '缺少訂單編號' }, { status: 400 })
951
+ }
952
+
953
+ try {
954
+ // 1. 從資料庫取得訂單
955
+ const order = await prisma.order.findUnique({
956
+ where: { merchantTradeNo }
957
+ })
958
+
959
+ if (!order) {
960
+ return Response.json({ error: '訂單不存在' }, { status: 404 })
961
+ }
962
+
963
+ // 2. 使用訂單記錄的服務商查詢
964
+ const service = await PaymentServiceFactory.getServiceForOrder(merchantTradeNo)
965
+ const queryResult = await service.queryOrder(order.userId, merchantTradeNo)
966
+
967
+ // 3. 同步訂單狀態
968
+ if (queryResult.success && queryResult.status !== order.status) {
969
+ await prisma.order.update({
970
+ where: { merchantTradeNo },
971
+ data: {
972
+ status: queryResult.status,
973
+ paidAt: queryResult.paidAt,
974
+ }
975
+ })
976
+ }
977
+
978
+ return Response.json({
979
+ success: true,
980
+ order: {
981
+ merchantTradeNo: order.merchantTradeNo,
982
+ amount: order.totalAmount,
983
+ status: queryResult.status || order.status,
984
+ paidAt: queryResult.paidAt || order.paidAt,
985
+ provider: order.paymentProvider,
986
+ }
987
+ })
988
+
989
+ } catch (error) {
990
+ console.error('[ERROR] 查詢訂單失敗:', error)
991
+ return Response.json({ error: '查詢失敗' }, { status: 500 })
992
+ }
993
+ }
994
+ ```
995
+
996
+ **步驟 4: 前端整合**
997
+
998
+ ```typescript
999
+ // components/CheckoutButton.tsx
1000
+
1001
+ 'use client'
1002
+
1003
+ import { useState } from 'react'
1004
+ import { Button } from '@/components/ui/button'
1005
+
1006
+ export function CheckoutButton({ cartItems, shippingInfo }) {
1007
+ const [loading, setLoading] = useState(false)
1008
+
1009
+ const handleCheckout = async () => {
1010
+ setLoading(true)
1011
+ try {
1012
+ // 1. 建立訂單
1013
+ const response = await fetch('/api/checkout', {
1014
+ method: 'POST',
1015
+ headers: { 'Content-Type': 'application/json' },
1016
+ body: JSON.stringify({
1017
+ userId: 'user-123',
1018
+ cartItems,
1019
+ shippingInfo,
1020
+ })
1021
+ })
1022
+
1023
+ const result = await response.json()
1024
+
1025
+ if (!result.success) {
1026
+ alert('結帳失敗')
1027
+ return
1028
+ }
1029
+
1030
+ // 2. 提交付款表單
1031
+ const form = document.createElement('form')
1032
+ form.method = result.paymentForm.method
1033
+ form.action = result.paymentForm.action
1034
+ form.target = '_self'
1035
+
1036
+ Object.entries(result.paymentForm.params).forEach(([key, value]) => {
1037
+ const input = document.createElement('input')
1038
+ input.type = 'hidden'
1039
+ input.name = key
1040
+ input.value = value as string
1041
+ form.appendChild(input)
1042
+ })
1043
+
1044
+ document.body.appendChild(form)
1045
+ form.submit()
1046
+
1047
+ } catch (error) {
1048
+ console.error('結帳失敗:', error)
1049
+ alert('結帳失敗')
1050
+ } finally {
1051
+ setLoading(false)
1052
+ }
1053
+ }
1054
+
1055
+ return (
1056
+ <Button onClick={handleCheckout} disabled={loading} size="lg">
1057
+ {loading ? '處理中...' : '前往付款'}
1058
+ </Button>
1059
+ )
1060
+ }
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ### 場景 2: 定期定額訂閱
1066
+
1067
+ **需求:** 實作週期扣款功能(會員訂閱制)
1068
+
1069
+ **步驟 1: 建立定期定額訂單**
1070
+
1071
+ ```typescript
1072
+ // app/api/subscription/create/route.ts
1073
+
1074
+ import { ECPayPaymentService } from '@/lib/services/ecpay-payment-service'
1075
+ import { prisma } from '@/lib/prisma'
1076
+
1077
+ export async function POST(request: Request) {
1078
+ const { userId, planId, email } = await request.json()
1079
+
1080
+ // 1. 取得訂閱方案
1081
+ const plan = await prisma.subscriptionPlan.findUnique({
1082
+ where: { id: planId }
1083
+ })
1084
+
1085
+ if (!plan) {
1086
+ return Response.json({ error: '方案不存在' }, { status: 404 })
1087
+ }
1088
+
1089
+ // 2. 產生訂閱編號
1090
+ const merchantTradeNo = `SUB${Date.now()}`
1091
+
1092
+ // 3. 建立訂閱記錄
1093
+ const subscription = await prisma.subscription.create({
1094
+ data: {
1095
+ userId: userId,
1096
+ planId: planId,
1097
+ merchantTradeNo: merchantTradeNo,
1098
+ status: 'PENDING',
1099
+ amount: plan.price,
1100
+ frequency: plan.frequency, // 'M' = 月, 'Y' = 年
1101
+ totalTimes: plan.totalTimes || 999, // 999 = 無限次
1102
+ }
1103
+ })
1104
+
1105
+ // 4. 建立 ECPay 定期定額訂單
1106
+ const service = new ECPayPaymentService(false) // 測試環境
1107
+
1108
+ const periodicData = {
1109
+ MerchantTradeNo: merchantTradeNo,
1110
+ MerchantTradeDate: new Date().toLocaleString('zh-TW', {
1111
+ year: 'numeric',
1112
+ month: '2-digit',
1113
+ day: '2-digit',
1114
+ hour: '2-digit',
1115
+ minute: '2-digit',
1116
+ second: '2-digit',
1117
+ hour12: false
1118
+ }).replace(/\//g, '/'),
1119
+ TotalAmount: plan.price,
1120
+ TradeDesc: `訂閱方案:${plan.name}`,
1121
+ ItemName: plan.name,
1122
+ ReturnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/subscription/callback`,
1123
+ PeriodAmount: plan.price, // 每期金額
1124
+ PeriodType: plan.frequency, // 'M' = 月, 'Y' = 年
1125
+ Frequency: 1, // 每 1 個週期
1126
+ ExecTimes: plan.totalTimes, // 執行次數
1127
+ PeriodReturnURL: `${process.env.NEXT_PUBLIC_BASE_URL}/api/subscription/periodic-callback`,
1128
+ }
1129
+
1130
+ const result = await service.createPeriodicOrder(userId, periodicData)
1131
+
1132
+ return Response.json({
1133
+ success: result.success,
1134
+ subscriptionId: subscription.id,
1135
+ paymentForm: {
1136
+ action: result.formAction,
1137
+ method: result.formMethod,
1138
+ params: result.formParams,
1139
+ }
1140
+ })
1141
+ }
1142
+ ```
1143
+
1144
+ **步驟 2: 處理首次授權回呼**
1145
+
1146
+ ```typescript
1147
+ // app/api/subscription/callback/route.ts
1148
+
1149
+ import { prisma } from '@/lib/prisma'
1150
+
1151
+ export async function POST(request: Request) {
1152
+ const formData = await request.formData()
1153
+ const params = Object.fromEntries(formData)
1154
+
1155
+ console.log('[INFO] 收到訂閱授權通知:', params)
1156
+
1157
+ const merchantTradeNo = params.MerchantTradeNo
1158
+ const isPaid = params.RtnCode === '1'
1159
+
1160
+ // 更新訂閱狀態
1161
+ await prisma.subscription.update({
1162
+ where: { merchantTradeNo },
1163
+ data: {
1164
+ status: isPaid ? 'ACTIVE' : 'FAILED',
1165
+ gwsr: params.gwsr, // 綠界週期編號(重要:後續扣款需要)
1166
+ firstPaidAt: isPaid ? new Date() : null,
1167
+ }
1168
+ })
1169
+
1170
+ console.log(`[OK] 訂閱 ${merchantTradeNo} 授權${isPaid ? '成功' : '失敗'}`)
1171
+
1172
+ return new Response('1|OK')
1173
+ }
1174
+ ```
1175
+
1176
+ **步驟 3: 處理週期扣款通知**
1177
+
1178
+ ```typescript
1179
+ // app/api/subscription/periodic-callback/route.ts
1180
+
1181
+ import { prisma } from '@/lib/prisma'
1182
+
1183
+ export async function POST(request: Request) {
1184
+ const formData = await request.formData()
1185
+ const params = Object.fromEntries(formData)
1186
+
1187
+ console.log('[INFO] 收到週期扣款通知:', params)
1188
+
1189
+ const gwsr = params.gwsr // 綠界週期編號
1190
+ const isPaid = params.RtnCode === '1'
1191
+ const execTimes = parseInt(params.ExecTimes) // 當前第幾次扣款
1192
+
1193
+ // 1. 更新訂閱記錄
1194
+ const subscription = await prisma.subscription.findFirst({
1195
+ where: { gwsr }
1196
+ })
1197
+
1198
+ if (subscription) {
1199
+ // 2. 建立扣款記錄
1200
+ await prisma.subscriptionPayment.create({
1201
+ data: {
1202
+ subscriptionId: subscription.id,
1203
+ merchantTradeNo: params.MerchantTradeNo,
1204
+ tradeNo: params.TradeNo,
1205
+ amount: parseInt(params.amount),
1206
+ execTimes: execTimes,
1207
+ status: isPaid ? 'PAID' : 'FAILED',
1208
+ paidAt: isPaid ? new Date() : null,
1209
+ failureReason: isPaid ? null : params.RtnMsg,
1210
+ }
1211
+ })
1212
+
1213
+ // 3. 更新訂閱狀態
1214
+ await prisma.subscription.update({
1215
+ where: { id: subscription.id },
1216
+ data: {
1217
+ currentExecTimes: execTimes,
1218
+ lastPaidAt: isPaid ? new Date() : subscription.lastPaidAt,
1219
+ }
1220
+ })
1221
+
1222
+ console.log(`[OK] 訂閱 ${subscription.merchantTradeNo} 第 ${execTimes} 次扣款${isPaid ? '成功' : '失敗'}`)
1223
+
1224
+ // 4. 扣款成功後續處理
1225
+ if (isPaid) {
1226
+ // 延長會員期限、發送通知等
1227
+ // await extendMembershipPeriod(subscription.userId)
1228
+ }
1229
+ }
1230
+
1231
+ return new Response('1|OK')
1232
+ }
1233
+ ```
1234
+
1235
+ ---
1236
+
1237
+ ## 常見錯誤與修正
1238
+
1239
+ ### 錯誤 1: CheckMacValue 計算錯誤
1240
+
1241
+ **錯誤訊息:** ECPay 回傳 `10100058: 請確認檢查碼是否正確`
1242
+
1243
+ **原因:** 參數排序錯誤或 URL Encode 不正確
1244
+
1245
+ **修正前:**
1246
+ ```typescript
1247
+ // * 錯誤:未排序參數
1248
+ function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
1249
+ const paramString = Object.entries(params)
1250
+ .map(([k, v]) => `${k}=${v}`)
1251
+ .join('&')
1252
+
1253
+ const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
1254
+ const hash = crypto.createHash('sha256').update(rawString).digest('hex')
1255
+ return hash.toUpperCase()
1256
+ }
1257
+ ```
1258
+
1259
+ **修正後:**
1260
+ ```typescript
1261
+ // *! 正確:排序 + URL Encode (lowercase)
1262
+ function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
1263
+ // 1. 移除 CheckMacValue 本身
1264
+ const { CheckMacValue, ...cleanParams } = params
1265
+
1266
+ // 2. 排序 keys
1267
+ const sortedKeys = Object.keys(cleanParams).sort()
1268
+
1269
+ // 3. 組合參數字串
1270
+ const paramString = sortedKeys
1271
+ .map(key => `${key}=${cleanParams[key]}`)
1272
+ .join('&')
1273
+
1274
+ // 4. 前後加上 HashKey/HashIV
1275
+ const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
1276
+
1277
+ // 5. URL Encode (lowercase)
1278
+ const encoded = encodeURIComponent(rawString).toLowerCase()
1279
+
1280
+ // 6. SHA256 + 大寫
1281
+ const hash = crypto.createHash('sha256').update(encoded).digest('hex')
1282
+ return hash.toUpperCase()
1283
+ }
1284
+ ```
1285
+
1286
+ **驗證方法:**
1287
+ ```typescript
1288
+ // 測試範例
1289
+ const params = {
1290
+ MerchantID: '3002607',
1291
+ MerchantTradeNo: 'ORD123456',
1292
+ MerchantTradeDate: '2024/01/29 12:00:00',
1293
+ TotalAmount: 1050,
1294
+ }
1295
+
1296
+ const checkMacValue = generateCheckMacValue(
1297
+ params,
1298
+ 'pwFHCqoQZGmho4w6',
1299
+ 'EkRm7iFT261dpevs'
1300
+ )
1301
+
1302
+ console.log('計算結果:', checkMacValue)
1303
+ // 應該與 ECPay 要求的值一致
1304
+ ```
1305
+
1306
+ ---
1307
+
1308
+ ### 錯誤 2: AES 加密錯誤
1309
+
1310
+ **錯誤訊息:** NewebPay 回傳 `TradeSha 錯誤` 或 PAYUNi 回傳 `HashInfo 錯誤`
1311
+
1312
+ **原因:** Key/IV 長度錯誤或未附加 Auth Tag
1313
+
1314
+ **修正前 (NewebPay):**
1315
+ ```typescript
1316
+ // * 錯誤:Key/IV 長度不正確
1317
+ function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string) {
1318
+ const queryString = new URLSearchParams(data).toString()
1319
+
1320
+ // 錯誤:未檢查 Key/IV 長度
1321
+ const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
1322
+ let encrypted = cipher.update(queryString, 'utf8', 'hex')
1323
+ encrypted += cipher.final('hex')
1324
+
1325
+ return encrypted
1326
+ }
1327
+ ```
1328
+
1329
+ **修正後 (NewebPay):**
1330
+ ```typescript
1331
+ // *! 正確:確認 Key/IV 長度 + 計算 TradeSha
1332
+ function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string) {
1333
+ // 確認長度
1334
+ if (hashKey.length !== 32) throw new Error('HashKey 必須 32 bytes')
1335
+ if (hashIV.length !== 16) throw new Error('HashIV 必須 16 bytes')
1336
+
1337
+ const queryString = new URLSearchParams(data).toString()
1338
+
1339
+ // AES-256-CBC 加密
1340
+ const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
1341
+ cipher.setAutoPadding(true)
1342
+ let encrypted = cipher.update(queryString, 'utf8', 'hex')
1343
+ encrypted += cipher.final('hex')
1344
+
1345
+ // 計算 TradeSha
1346
+ const tradeSha = crypto
1347
+ .createHash('sha256')
1348
+ .update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
1349
+ .digest('hex')
1350
+ .toUpperCase()
1351
+
1352
+ return {
1353
+ TradeInfo: encrypted,
1354
+ TradeSha: tradeSha
1355
+ }
1356
+ }
1357
+ ```
1358
+
1359
+ **修正前 (PAYUNi):**
1360
+ ```typescript
1361
+ // * 錯誤:忘記附加 Auth Tag
1362
+ function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string) {
1363
+ const jsonString = JSON.stringify(data)
1364
+
1365
+ const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
1366
+ let encrypted = cipher.update(jsonString, 'utf8', 'hex')
1367
+ encrypted += cipher.final('hex')
1368
+
1369
+ // 錯誤:忘記取得 Auth Tag
1370
+ return encrypted
1371
+ }
1372
+ ```
1373
+
1374
+ **修正後 (PAYUNi):**
1375
+ ```typescript
1376
+ // *! 正確:附加 Auth Tag
1377
+ function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string) {
1378
+ const jsonString = JSON.stringify(data)
1379
+
1380
+ // AES-256-GCM 加密
1381
+ const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
1382
+ let encrypted = cipher.update(jsonString, 'utf8', 'hex')
1383
+ encrypted += cipher.final('hex')
1384
+
1385
+ // 取得 Auth Tag(16 bytes = 32 hex chars)
1386
+ const authTag = cipher.getAuthTag().toString('hex')
1387
+
1388
+ // 組合:encrypted + authTag
1389
+ const encryptInfo = encrypted + authTag
1390
+
1391
+ // 計算 HashInfo
1392
+ const hashInfo = crypto
1393
+ .createHash('sha256')
1394
+ .update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
1395
+ .digest('hex')
1396
+ .toUpperCase()
1397
+
1398
+ return {
1399
+ EncryptInfo: encryptInfo,
1400
+ HashInfo: hashInfo
1401
+ }
1402
+ }
1403
+ ```
1404
+
1405
+ **測試工具:**
1406
+ ```typescript
1407
+ // 測試 NewebPay 加密/解密
1408
+ const testData = { test: 'hello', amount: 1000 }
1409
+ const { TradeInfo, TradeSha } = encryptNewebPay(testData, hashKey, hashIV)
1410
+ const decrypted = decryptNewebPay(TradeInfo, hashKey, hashIV)
1411
+ console.log('原始:', testData)
1412
+ console.log('解密:', decrypted)
1413
+ console.log('一致:', JSON.stringify(testData) === JSON.stringify(decrypted))
1414
+
1415
+ // 測試 PAYUNi 加密/解密
1416
+ const { EncryptInfo, HashInfo } = encryptPAYUNi(testData, hashKey, hashIV)
1417
+ const decryptedPAYUNi = decryptPAYUNi(EncryptInfo, hashKey, hashIV)
1418
+ console.log('原始:', testData)
1419
+ console.log('解密:', decryptedPAYUNi)
1420
+ console.log('一致:', JSON.stringify(testData) === JSON.stringify(decryptedPAYUNi))
1421
+ ```
1422
+
1423
+ ---
1424
+
1425
+ **更多範例持續更新中...**