> 以下是我從 production code 抽取出來的,串接步驟沒有單獨測試過
* [API 文件下載](https://www.newebpay.com/website/Page/content/download_api)
以下說明 [藍新金流](https://www.newebpay.com/) 的串接方式。
> 也可參考 <https://github.com/ihower/rails-exercise-ac7/pull/1> 但這是藍新前身(智付寶)串接範例,主要是加解密的地方不一樣。
> Github 上看到的另一個分享的實作,供參考 https://github.com/cellvinchung/newebpay-rails
## 基本架構流程
* 假設你有自己的 Order model
* 每次付款,都會有單獨的 Payment model 對應金流紀錄 (Order has_many payments, 例如付款失款或客人更換付款方式,就會有多筆 payments 紀錄)
* lib/newebpay.rb 是用來協助加解密的程式
* 用戶選擇付款方式後,先建立一個 Payment 紀錄,然後有個 HTML form 會自動將客人導至金流平台付款
* 付款後,金流平台會同步將客人轉回來,也會非同步在背景通知系統付款成功
* ATM虛擬帳號: 客人去藍新金流可以選偏好銀行,然後導回來時,會帶虛擬帳號參數。客人非同步付款後,藍新金流會再打系統付款成功
## 步驟
1. 上測試專區 https://cwww.newebpay.com/ 註冊企業帳號取得API串接金鑰
* 內容隨便填即可 (不過統編要記得輸入了什麼! 下次登入還要用!)
* 進入商店資料設定 -> 詳細資料 -> 拿到 Merchant ID、HashKey 和 HashIV
2. 新增 `config/newebpay.yml` 將 Merchant ID、HashKey 和 HashIV 填入
```
default: &default
merchant_id: 'xxx'
hash_key: xxx
hash_iv: xxx
url: https://ccore.newebpay.com/MPG/mpg_gateway # 這是藍新的測試端點,正式的是 https://core.newebpay.com/MPG/mpg_gateway
notify_url: 'https://test.com/newebpay/notify' # 非同步通知付款成功的端點。本機開發可用 https://ngrok.com/
development:
<<: *default
staging:
<<: *default
test:
<<: *default
production:
<<: *default
```
3. 新增 `lib/newebpay.rb` 是主要做加解密的程式
```
class Newebpay
mattr_accessor :merchant_id
mattr_accessor :hash_key
mattr_accessor :hash_iv
mattr_accessor :url
mattr_accessor :notify_url
def self.setup
yield(self)
end
def initialize(payment)
@payment = payment
end
def generate_form_data(return_url:, customer_url:, client_back_url:)
newebpay_data = {
MerchantID: self.merchant_id,
RespondType: "JSON",
TimeStamp: @payment.created_at.to_i,
Version: "1.4",
LangType: I18n.locale.downcase, # zh-tw or en
MerchantOrderNo: @payment.external_id,
Amt: @payment.amount.to_i,
ItemDesc: @payment.name,
ClientBackURL: client_back_url,
NotifyURL: self.notify_url,
Email: @payment.email,
LoginType: 0,
CREDIT: 0,
WEBATM: 0,
VACC: 0,
CVS: 0,
BARCODE: 0
}
# 以下是根據 payment 的付款方式,決定過去藍金金流頁面時,要啟用哪些付款方式
#if @payment.order.shipping_type == "cvs"
# newebpay_data.merge!( :CVSCOM => 1 )
#end
case @payment.payment_method
when "Credit"
newebpay_data.merge!( :CREDIT => 1 )
newebpay_data.merge!( :ReturnURL => return_url )
#when "WebATM"
# newebpay_data.merge!( :WEBATM => 1 )
when "ATM"
newebpay_data.merge!( :VACC => 1, :ExpireDate => @payment.deadline.strftime("%Y%m%d") )
newebpay_data.merge!( :CustomerURL => customer_url )
#when "CVS"
# newebpay_data.merge!( :CVS => 1, :ExpireDate => @payment.deadline.strftime("%Y%m%d") )
#when "BARCODE"
# newebpay_data.merge!( :BARCODE => 1, :ExpireDate => @payment.deadline.strftime("%Y%m%d") )
end
Rails.logger.debug(newebpay_data)
trade_info = self.encrypt(newebpay_data)
trade_sha = self.class.generate_aes_sha256(trade_info)
{
MerchantID: self.merchant_id,
TradeInfo: trade_info,
TradeSha: trade_sha,
Version: '1.4'
}
end
def self.decrypt(trade_info, trade_sha)
return nil if self.generate_aes_sha256(trade_info) != trade_sha
decipher = OpenSSL::Cipher::AES256.new(:CBC)
decipher.decrypt
decipher.padding = 0
decipher.key = self.hash_key
decipher.iv = self.hash_iv
binary_encrypted = [trade_info].pack('H*') # hex 轉 binary
plain = decipher.update(binary_encrypted) + decipher.final
# strip last space padding
plain = plain.strip
# strip last \x17 padding
if plain[-1] != '}'
plain = plain[0, plain.index(plain[-1])]
end
return JSON.parse(plain)
end
protected
def encrypt(params_data)
cipher = OpenSSL::Cipher::AES256.new(:CBC)
cipher.encrypt
cipher.key = self.hash_key
cipher.iv = self.hash_iv
encrypted = cipher.update(params_data.to_query) + cipher.final
return encrypted.unpack('H*')[0] # binary 轉 hex
end
def self.generate_aes_sha256(trade_info)
str = "HashKey=#{self.hash_key}&#{trade_info}&HashIV=#{self.hash_iv}"
Digest::SHA256.hexdigest(str).upcase
end
end
```
4. 新增 `config/initializers/newebpay.rb` 在 rails 啟動時載入上述的 yml 設定
```
require 'newebpay'
newebpay_config = Rails.application.config_for(:newebpay)
Newebpay.setup do |config|
config.merchant_id = newebpay_config["merchant_id"]
config.hash_key = newebpay_config["hash_key"]
config.hash_iv = newebpay_config["hash_iv"]
config.url = newebpay_config["url"]
config.notify_url = newebpay_config["notify_url"]
end
```
5. 新增 Payment model,作為每次付款的紀錄
```
# 欄位有
# t.integer "order_id"
# t.integer "user_id"
# t.string "payment_method"
# t.decimal "amount", precision: 13, scale: 2
# t.datetime "paid_at"
# t.text "params"
# t.text "atm_params"
# t.datetime "created_at", null: false
# t.datetime "updated_at", null: false
class Payment < ApplicationRecord
PAYMENT_METHODS = %w[Credit ATM] # WebATM BARCODE CVS
validates_inclusion_of :payment_method, :in => PAYMENT_METHODS
serialize :params, JSON
serialize :atm_params, JSON
belongs_to :order, optional: true
after_update :update_order_status
def name
"你的商店名稱"
end
def email
self.order.email
end
def deadline
Time.now + 3.days
end
def external_id
"#{self.id}#{Rails.env.upcase[0]}"
end
def self.find_and_process_atm(params)
data = Newebpay.decrypt(params['TradeInfo'], params['TradeSha'])
if data
payment = self.find(data['Result']['MerchantOrderNo'].to_i)
payment.atm_params = data
return payment
else
return nil
end
end
def self.find_and_process(params)
data = Newebpay.decrypt(params['TradeInfo'], params['TradeSha'])
if data
payment = self.find(data['Result']['MerchantOrderNo'].to_i)
if params['Status'] == 'SUCCESS'
payment.paid_at = Time.now
end
payment.params = data
return payment
else
return nil
end
end
def update_order_status
return true if self.order.nil?
order = self.order
if self.paid_at
# 這裡寫你怎麼更新 Order 的已付款狀態,例如:
order.payment_status = "paid"
order.paid_at = self.paid_at
order.status = "ready" if order.status == "pending"
end
order.save!
end
end
```
5. 新增 checkout_newebpay 的路由,以及從金流付款後回來時處理的路由
post 'newebpay/return' # 這是客人從 藍金金流 付款成功後,瀏覽器直接回來的端點
post 'newebpay/receive_atm_code' # 這是 客人從 藍金金流 取得ATM虛擬帳號後,瀏覽器直接回來的端點
post 'newebpay/notify' # 這是 藍新金流,系統非同步通知付款結果的端點
resources :orders do
member do
post :checkout_newebpay # 這是放一個 HTML form,將客人瀏覽器導去 藍新金流的 頁面
end
end
6. 在付款畫面(這裡假設是 Orders show action) 加上付款的按鈕,按下後將前往 checkout_newebpay action
```
<%= link_to "前往付款", checkout_newebpay_order_path(@order, :payment_method => 'CREDIT'), :method => :post %>
```
這是 OrdersController 裡面的 action:
```
def checkout_newebpay
@order = current_user.orders.find(params[:id])
if @order.paid?
redirect_to :back, alert: 'already paid!'
else
@payment = Payment.create!( :order => @order,
:payment_method => params[:payment_method] )
render :layout => false
end
end
```
7. 新增 `checkout_newebpay.html.erb` 頁面,這會自動將客人的瀏覽器導向藍新金流:
```
<h1>轉址前往藍新金流付款頁面,請勿關閉或離開頁面...</h1>
<form action="<%= Newebpay.url %>" method="post">
<% Newebpay.new(@payment).generate_form_data(return_url: newebpay_return_url,
customer_url: newebpay_receive_atm_code_url,
client_back_url: order_url(@payment.order) ).each do |key, value| %>
<%= hidden_field_tag key, value %>
<% end %>
<input type="submit" style="visibility:hidden;">
</form>
<script>
document.forms[0].submit();
</script>
```
8. 新增 newebpay_controller 的 return 和 notify,這是從金流回來時的處理
```
class NewebpayController < ActionController::Base
skip_before_action :verify_authenticity_token
def return
result = nil
ActiveRecord::Base.transaction do
@payment = Payment.find_and_process(newebpay_params)
result = @payment.save
end
@order = @payment.order
if @payment.paid_at
redirect_to order_path(@order)
else
flash[:alert] = '付款失敗!請再次付款!'
redirect_to order_path(@order)
end
end
def receive_atm_code
result = nil
ActiveRecord::Base.transaction do
@payment = Payment.find_and_process_atm(newebpay_params)
result = @payment.save
end
@order = @payment.order
if @payment.atm_params['Status'] == 'SUCCESS'
@order.expire_at = @payment.atm_expire_date.end_of_day
@order.save
flash[:notice] = "已建立本訂單專用的 ATM 匯款帳號,請儘速轉帳!"
redirect_to order_path(@order)
else
flash[:alert] = '交易失敗!請重新選擇付款方式!'
redirect_to order_path(@order)
end
end
def notify
result = nil
ActiveRecord::Base.transaction do
@payment = Payment.find_and_process(newebpay_params)
result = @payment.save
end
if @payment.paid_at
# OrderMailer.checkout_complete_order(@payment).deliver_later! # 可以發付款成功通知
end
if result
render :plain => "1|OK"
else
render :plain => "0|ErrorMessage"
end
end
private
def newebpay_params
params.slice("Status", "MerchantID", "Version", "TradeInfo", "TradeSha")
end
end
```