> 以下是我從 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 ```