QRcode và ứng dụng thanh toán điện tử trong Ruby On Rails

QRcode và ứng dụng thanh toán điện tử trong Ruby On Rails

bài viết trước mình đã giới thiệu cho các bạn về QRCode và chữ kí số, hôm nay mình sẽ sử dụng QRcode để demo quá trình thanh toán online

Ý tưởng thực hiện

  • Sử dụng thuật toán mã hóa và hàm băm để tạo ra một TOKEN, trong TOKEN có chứa thông tin của người dùng và thông tin của sản phẩm
  • Dùng TOKEN này để tạo ra một QRCode tương ứng
  • Người dùng Scan mã QRCode ở trên sẽ được dẫn đến link thanh toán, nếu TOKEN hợp lệ thì thanh toán thành công và trừ tiền trong tài khoản

Thực hiện

Phần khởi tạo project của rails thì mình không nhắc lại nữa mà mình di thẳng vào chi tiết qúa trình luôn


Ở trong project này mình sử dụng các gem sau:

gem "bootstrap-sass"
gem "jquery-rails"
gem "devise"
gem 'devise-bootstrap-views'

Những gem này dùng để tạo ra form đăng nhập, đăng kí, .... điều đó là tất nhiên rồi, phải có đăng nhập thì mới thanh toán được.

gem "rqrcode"

Gem này dùng để tạ ra mã QRCode tương ứng

Xong bundle install là chúng ta đã hoàn thành việc cài đặt các gem cần thiết cho project


Để tạo ra form đăng nhập và đăng kí một các tự động sử dụng gem devise các bạn có thể tham khảo ở đây, ngoài ra để form trông đẹp hơn thì có thể sử dụng thêm gem 'devise-bootstrap-views' xem hướng dẫn sử dụng tại đây, để tránh dài dòng thì việc này mình không nhắc lại nữa.


Tiếp theo, để thanh toán được trước hết phải có tiền đã 😃. Trong khuôn khổ bài viết này ví tiền mà mình tạo ra sẽ không được liên kết với ngân hàng hay gì cả, mà chỉ đơn giản là tạo ra một bảng trong database và input số tiền vào trong đó. Mình thực hiện như sau:

rails g model wallet user:references cash:integer card_name:string card_number:string

Một model sẽ được tao ra như sau:

class Wallet < ApplicationRecord
  belongs_to :user
end

Tạo ra một controller tương ứng dùng để thực hiện việc nhập số tiền:

class WalletsController < ApplicationController
  before_action :authenticate_user!
  def index
    @my_wallet = Wallet.find_by(user_id: params[:user_id])
  end

  def new
    @wallet = Wallet.new
  end

  def create
    @wallet = Wallet.new wallet_params
    @wallet.user_id = current_user.id
    @wallet.save
    redirect_to user_wallets_path(current_user.id)
  end

  private
  def wallet_params
    params.require(:wallet).permit :cash, :card_name, :card_number
  end
end

Tiếp theo chúng ta tạo ra các item để làm dữ liệu thanh toán

rails g model item name:string amount:integer infor:string unit_price:integer image:string

Một model Item sẽ được tạo ra Trong controller tương ứng, sử dụng hàm index để đổ dữ liệu ra view

class ItemsController < ApplicationController
  before_action :authenticate_user!

  def index
    @items = Item.all
  end
end

Sử dụng file seeds.rb để tạo ra dữ liệu

10.times do |n|
  name  = "Sản phẩm #{n+1}"
  amount = rand(1..20)
  infor = "Sản phẩm #{n+1}"
  unit_price = 1000 * n
  image = "item_#{n+1}.jpg"
  Item.create!(name:  name, amount: amount, infor: infor,
    unit_price: unit_price, image: image)
end

Tiếp theo chúng ta sẽ tạo ra bảng payments, đây chính là bảng sẽ lưu lại thông tin giao dịch của user tương ứng

rails g model payment item:references user:references amount:integer status:string type_payment:string

Trường status sẽ nhận một trong 3 giá trị sau:

  • request là trạng thái vừa mới tạo
  • success là trạng thái đã thanh toán thành công
  • reject là trang thái ngừng thanh toán Ở đây chúng ta có thể sử dụng kiểu dữ liệu enum các bạn có thể tham khảo tại đây

Mối quan hệ giữa các bảng user, payment, item, wallet sẽ như sau:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :wallets
end
class Payment < ApplicationRecord
  belongs_to :item
end
class Item < ApplicationRecord
  has_many :payments
end
class Wallet < ApplicationRecord
  belongs_to :user
end

Vậy là xong phần tạo bảng payment(bảng quan trọng nhất trong project này), các phần xử lý trong controller của model này sẽ được nhắc đến ở dưới


Tiếp theo chúng ta sẽ tạo ra các routes tương ứng cho mỗi hành động

Tại file routes.rb

Rails.application.routes.draw do
  devise_for :users
  resources :items do
    resources :payments do
      member do
        get "payment_qrcode"
        get "confirm_payment"
        get "reject_payment"
      end
    end
  end

  resources :users do
    resources :wallets
  end

  root "items#index"
end

Trong controller app/controllers/payments_controller.rb Viết một method create, method này được gọi đến khi user nhập số lượng hàng và tiến hành thanh toán, bản ghi payment được tạo ra với status = 'request'

  def create
    payment = Payment.new
    payment.item_id = params[:item_id]
    payment.type_payment = params[:commit]
    payment.user_id = current_user.id
    payment.amount = params[:amount]
    payment.status = "request"
    payment.save
    redirect_to item_payment_path(item_id: payment.item_id, id: payment.id)
  end

Khi tạo xong thì chuyển đến màn hình QRCode tương ứng

  def show
    payment = Payment.find(params[:id])
    timestamp = ServerTime.now
    token = Digest::MD5.hexdigest(generate_info(payment, timestamp.to_i.to_s))
    payment.update_attributes(token_created_at: timestamp)
    infor = "#{request.original_url}/payment_qrcode?token=#{token}"
    puts infor
    @qr = RQRCode::QRCode.new(infor)
      .as_png.resize(500, 500)
      .to_data_url
  end
  
  private
  def generate_info payment, timestamp
    infor = current_user.email + payment.item.name + payment.amount.to_s
      + payment.item.unit_price.to_s + timestamp
    infor
  end

Ở đây, mình sử dụng TOKEN là một chuỗi gồm thông tin của user là email, thông tin của item và cuối cùng à một chuỗi timestamp để tránh việc trùng lặp, sau đó tất cả được băm ra và tạo ra TOKEN, đồng thời update timestamp này vào trong database.

Ngoài ra giá trị timestamp = ServerTime.now được định nghĩa trong file config/initializers/server_time.rb

ServerTime = Time.zone

đây chính là thời gian hiện tại của server rails

Mã QRCode sẽ được tạo ra tương ứng với mã token cùng với đường dẫn như sau

infor = "#{request.original_url}/payment_qrcode?token=#{token}"

Vậy là xong việc tạo ra QRCode cho record payment tương ứng


Tiếp theo là kiểm tra tính hợp lệ của QRCode

  def payment_qrcode
    payment = Payment.where(id: params[:id], user_id: current_user.id)
    payment = payment.where.not(status: ["success", "reject"]).first
    return render "token_invalid" if !payment.present?
    if !token_expired(payment.token_created_at) &&
      check_valid_token(payment, params[:token])
      @target_payment = payment
      render "payment_qrcode"
    else
      render "token_invalid"
    end
  end
  
  private 
  def check_valid_token payment, token
    infor = current_user.email + payment.item.name + payment.amount.to_s
      + payment.item.unit_price.to_s + payment.token_created_at.to_i.to_s
    Digest::MD5.hexdigest(infor) == token
  end

  def token_expired token_created_at
    (ServerTime.now - token_created_at) > 5.minutes
  end

Nếu TOKEN trong QRCode là hợp lệ thì hiển thị hóa đơn thanh toán, nếu không hợp lệ thì báo lỗi. Ở đây mình đặt thời gian hết hạn của TOKEN là 5 phút, và băm lại thông tin tương ứng của user và record của payment để kiểm tra tính hợp lệ của TOKEN.


Cuối cùng là đến bước thanh toán sau khi thỏa mãn các điều kện trên, người dùng bấm vào nút thanh toán và hệ thống sẽ tự động trừ tiền trong ví

def confirm_payment
   payment = Payment.find(params[:id])
   item = payment.item
   if token_expired(payment.token_created_at)
     @error = "Token expired"
   else
     my_wallet = current_user.wallets.first
     if !my_wallet.present?
       @error = "My wallet is blank. Please input money"
       return render "payment_complete"
     end
     new_cash = my_wallet.cash - payment.amount * payment.item.unit_price
     if new_cash < 0
       @error = "Your wallet does not have enough money"
       return render "payment_complete"
     end
     new_amount_item = item.amount - payment.amount
     ActiveRecord::Base.transaction do
       my_wallet.update_attributes(cash: new_cash)
       payment.update_attributes(status: "success")
       item.update_attributes(amount: new_amount_item)
       @success = "Payment Complete!!"
     end
   end
   render "payment_complete"
 rescue ActiveRecord::RecordInvalid
   @error = "Payment Not Complete"
   render "payment_complete"
 end

Nếu user không muốn thanh toán thì method sau sẽ được thực hiện

def reject_payment
  payment = Payment.find(params[:id])
  payment.update_column(:status, "reject")
  redirect_to root_path
end

Về phần views thì các bạn có thể tùy biến theo ý thích, và đây là source code của mình các bạn có thể tham khảo ở đây

Demo

Để cho trực quan thì mình sẽ kết nối điện thoại với host của rails ở trên máy tính, rất may là rails hỗ trợ điều này, chỉ cần máy tính và điện thoại bắt cùng 1 dải địa chỉ wifi, chúng ta chỉ cần kiểm tra địa chị của máy và chị server rails trên địa chỉ đó Và đây là thành quả Thử mua một sản phẩm QRCode được tạo ra tương ứng

Bây giờ dùng điện thoại scan ta được Truy cập vào đường dẫn thì yêu cầu đăng nhập, đăng nhập xong nếu mã QR hợp lệ thì hiển thị hóa đơn Không hợp lệ thì thông báo lỗi Nhấn vào Confirm check lại điều kiện một lần nữa nếu hợp lệ thì trừ tiền, không hợp lệ thì báo lỗi


Cuối cùng thì cũng xong 😂😂, bài viết này chỉ mang tính chất tìm hiểu cách thức hoạt động của việc thanh toán bằng QRCode, trên thực tế TOKEN được tạo ra để thanh toán phải do bên thứ 3 tin cậy giữa ngân hàng và ứng dụng được kết nối tạo ra, đồng thời còn vô số những chính sách bảo mật để tránh lộ lọt thông tin của khách hàng, vì vậy mà bài viết này chỉ để tham khảo, để ứng dụng vào thực tế thì còn rất nhiều điều cần cải thiện, hy vọng sẽ giúp ích được nhưng ai quan tâm đến mặt này của QRCode


Bài viết đến đây là hết cảm ơn các bạn đã đọc