<%= image_tag(product.image_url) %>
<%= product.title %>
<%= sanitize(product.description) %>
<%= number_to_currency(product.price) %>
柚子快報激活碼778899分享:博客 Ruby設(shè)計-開發(fā)日志
Log
1 產(chǎn)品 Product
1.1 創(chuàng)建 Product
創(chuàng)建名為 project 的 rails 應(yīng)用
rails new project
創(chuàng)建 Product 模型
rails generate scaffold Product title:string description:text image_url:string price:decimal
這會生成一個 migration ,我們需要進(jìn)一步修改這個遷移,保證價格擁有 8 位有效數(shù)字,同時小數(shù)點(diǎn)后保留兩位。修改遷移文件
class CreateProducts < ActiveRecord::Migration[7.0]
def change
create_table :products do |t|
t.string :title
t.text :description
t.string :image_url
t.decimal :price, precision: 8, scale: 2
t.timestamps
end
end
end
然后就可以進(jìn)行 migrate
rake db:migrate
這里的 rake 可以被理解為一個腳本的管理器,db:migrate 是其中的一個腳本。還有一種說法是 rake 類似與 C 中的 make
最終我們對于數(shù)據(jù)庫的修改,都會被記錄在 db/schema.rb 中,比如說現(xiàn)在
ActiveRecord::Schema[7.0].define(version: 2022_12_31_030243) do
create_table "products", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image_url"
t.decimal "price", precision: 8, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
可以看到基本上與 migration 是一致的,原來的 t.timestamps 時間戳變成了 created_at 和 updated_at 兩個屬性。此外主鍵被叫做 product_id ,并沒有在這里顯示,這應(yīng)該是一種默認(rèn)配置。
1.2 本地服務(wù)器
我們輸入如下命令,就可以在本地啟動服務(wù)器
rails s
會看到如下字樣
=> Booting Puma
=> Rails 7.0.4 application starting in development
其中的 Puma 似乎是一個線程管理器,每個線程都用于處理來自客戶端的一個 request 。
1.3 表單
app/views/products/_form.html.erb 是一個局部渲染文件,用于當(dāng)做 product 信息的表單,這個表單會在 new.html.erb, edit.html.erb 這兩個文件中用到,如下所示
<%= render "form", product: @product %>
<%= link_to "Back to products", products_path %>
關(guān)于局部渲染,有如下知識:https://blog.csdn.net/weixin_30621711/article/details/96260112
表單的具體內(nèi)容如下
<%= form_with(model: product) do |form| %>
<% if product.errors.any? %>
<% product.errors.each do |error| %>
<% end %>
<% end %>
<%= form.label :title, style: "display: block" %>
<%= form.text_field :title %>
<%= form.label :description, style: "display: block" %>
<%= form.text_area :description, rows: 10, cols: 60 %>
<%= form.label :image_url, style: "display: block" %>
<%= form.text_field :image_url %>
<%= form.label :price, style: "display: block" %>
<%= form.text_field :price %>
<%= form.submit %>
<% end %>
需要注意我們將表單的產(chǎn)品描述部分的輸入框變大了
<%= form.text_area :description, rows: 10, cols: 60 %>
1.4 seeds
如果數(shù)據(jù)庫不是同一個(一般本地開發(fā)多個,云端一個),那么測試數(shù)據(jù)就成了“個人私有”的,顯然是低效的,我們可以給數(shù)據(jù)庫一組“初始值“(也就是種子,seeds),這組初始值我們可以在 db/seeds.rb 中給出,如下所示
Product.delete_all
Product.create(title: 'Programming Ruby 1.9',
description:
%{
Ruby is the fastest growing and most exciting dynamic language out there.
},
image_url: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.kfzimg.com%2Fsw%2Fkfzimg%2F1575%2F012f2857fe9a2966a5_b.jpg&refer=http%3A%2F%2Fwww.kfzimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1668567051&t=61ed25128b60ad15cbe2c21729511f99',
price: 49.50
)
然后運(yùn)行
rake db:seed
就可以添加這個數(shù)據(jù)。
1.5 SCSS
? 當(dāng)前的 products 頁面過于丑陋,可以考慮給所有的產(chǎn)品界面一組樣式,這里我們用 scss 實(shí)現(xiàn),在 app/assets/stylesheets/ 中創(chuàng)建 products.scss 并寫入下面的內(nèi)容
.products {
table {
border-collapse: collapse;
}
table tr td{
padding: 5px;
vertical-align: top;
}
.list_image {
width: 60px;
height: 70px;
}
.list_description {
width: 60%;
dl {
margin: 0;
}
dt {
color: #244;
font-weight: bold;
font-size: larger;
}
dd {
margin: 0;
}
}
.list_actions {
font-size: x-small;
text-align: right;
padding-left: 1em;
}
.list_line_even {
background: #e0f8f8;
}
.list_line_odd {
background: #e2c3e2;
}
}
? 然后就會發(fā)現(xiàn)運(yùn)行不了,這是因?yàn)?rails 默認(rèn)不能處理 scss,需要在 gemfile 中加入如下依賴
# Use Sass to process CSS
gem "sassc-rails"
gem 'bootstrap-sass'
然后命令行運(yùn)行
bundle install
這是因?yàn)?/p>
rake是Ruby語言的構(gòu)建工具,它的配置文件是Rakefile。
gem是Ruby語言的包管理工具,它的配置文件后綴是.gemspec。
bundler是Ruby語言的外部依賴管理工具,它有一個別名叫”bundle”,它的配置文件是Gemfile。
然后在 views/layouts/application.html.erb 中需要進(jìn)行修改,加上每個類對應(yīng)不同的 scss 。
<%= yield %>
views/layouts/application.html.erb 是一個布局頁面,會對每一個頁面都適用。
最后寫一下 index.html.erb 的具體信息
<%= image_tag(product.image_url, class: 'list_image') %> |
| <%= link_to 'Show', product %> <%= link_to 'Edit', edit_product_path(product) %> <%= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete %> |
<%= link_to 'New product', new_product_path %>
1.6 驗(yàn)證
可以在模型層對于模型的屬性添加驗(yàn)證,對于 Product 來說有如下驗(yàn)證
class Product < ApplicationRecord
validates :title, :description, :image_url, presence: true
validates :price, numericality: {:greater_than_or_equal_to => 0.01}
validates :title, uniqueness: true
validates :image_url, format: {
:with => %r{\.(gif|jpg|png)}i,
:message => 'must be a URL for GIF, JPG or PNG image.'
}
end
驗(yàn)證的格式如下
validate [屬性名], [驗(yàn)證內(nèi)容]
具體的驗(yàn)證內(nèi)容有
presencenumericalityuniquenessuniqueness
1.7 路由設(shè)置
為了更好的展示產(chǎn)品(而不是需要通過 get 路由訪問產(chǎn)品列表),我們可以另外再從用戶的角度完善一個頁面,這需要借助一個一個新的控制器(在后面的開發(fā)中,它被定義為“付費(fèi)購買用戶所使用的控制器”),在終端輸入
rails generate controller Store index
它的意思是生成一個叫做 Store 的控制器,同時只有一個 index 動作。
然后我們希望當(dāng)訪問根目錄的時候,可以訪問到 Store#index 對應(yīng)的界面,所以我們在 router.rb 中加上這句話
root 'store#index', as: 'store_index'
至于這個是怎么來的,可以這樣理解,在路由中,標(biāo)準(zhǔn)寫法是這樣的
get '/test/:id', to: 'test#test', as 'test_test'
這個意思是,用戶用 get 的方式訪問 test/:id 這個 ulr 的時候,實(shí)際訪問的是 Test 控制器對應(yīng)的 test 動作對應(yīng)的 view 。當(dāng)我們有了 as 之后,我們可以通過 test_test_path 來指代 xxxx/test/:id ,用 test_test_url 指代 http:/xxxx/test/:id。也就是說path 類方法是對應(yīng)的路徑,不帶協(xié)議部分。url 生成的帶 http。兩者差別在此。
這樣看上面的 root ,只是某種意義的簡寫。
我們常見的
resoureces: products
其實(shí)就是一堆標(biāo)準(zhǔn)格式的聲明,如下所示
HTTP VerbPathActionUsed forGET/productsindexdisplay a list of all productsGET/products/newnewreturn an HTML form for creating a new productPOST/productscreatecreate a new productGET/products/:idshowdisplay a specific productGET/products/:id/editeditreturn an HTML form for editing a productPATCH/PUT/products/:idupdateupdate a specific productDELETE/products/:iddestroydelete a specific product
其中 PATCH, PUT, POST 都會被轉(zhuǎn)換成 POST
PATCH: 實(shí)體中包含一個表,表中說明與該URI所表示的原內(nèi)容的區(qū)別PUT:上傳資源DELETE:刪除資源
1.8 美化商品目錄
在 store#index 中補(bǔ)充如下代碼,表示按字典序展示所有的 Product
class StoreController < ApplicationController
def index
@products = Product.order(:title)
end
end
同時調(diào)整相應(yīng)的視圖
<%= notice %>
<% @products.each do |product| %>
<%= image_tag(product.image_url) %>
<%= sanitize(product.description) %>
<%= number_to_currency(product.price) %>
<% end %>
相應(yīng)的 scss 表
.store {
h1 {
margin: 0;
padding-bottom: 0.5em;
font: 150% Sans-Serif;
color: #226;
border-bottom: 3px dotted #77d;
}
.entry {
overflow: auto;
margin-top: 1em;
border-bottom: 1px dotted #77d;
height: 100px;
}
img {
width: 80px;
margin-right: 5px;
margin-bottom: 5px;
height: 100px;
position: absolute;
}
h3 {
font-size: 120%;
font-family: sans-serif;
margin-left: 100px;
margin-top: 0;
margin-bottom: 2px;
color: #277;
}
p, div.price_line {
margin-left: 100px;
margin-top: 0.5em;
margin-bottom: 0.8em;
}
.price {
color: #44a;
font-weight: bold;
margin-right: 3em;
}
}
1.9 頁面布局
修改 layouts/application.html.erb 加入側(cè)邊欄和頂欄
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<%= @page_title %>
同時修改 scss 文件
body, body > p, body > ol, body > ul, body > td {
margin: 8px !important;
}
#banner {
position: relative;
min-height: 40px;
background: #9c9;
padding: 10px;
border-bottom: 2px solid;
font: small-caps 40px/40px "Times New Roman", serif;
color: #282;
text-align: center;
img {
position: absolute;
top: 5px;
left: 5px;
width: 60px;
height: 60px;
}
}
#notice {
color: #000 !important;
border: 2px solid red;
padding: 1em;
margin-bottom: 2em;
background-color: #f0f0f0;
font: bold smaller sans-serif;
}
#notice:empty {
display: none;
}
#columns {
background: #141;
display: flex;
#main {
padding: 1em;
background: white;
flex: 1;
}
#side {
padding: 1em 2em;
background: #141;
ul {
padding: 0;
li {
list-style: none;
a {
color: #bfb;
font-size: small;
}
}
}
}
}
@media all and (max-width: 800px) {
#columns {
flex-direction: column-reverse;
}
}
@media all and (max-width: 500px) {
#banner {
height: 1em;
}
#banner .title {
display: none;
}
}
2 購物車 Cart
2.1 Cart 模型
創(chuàng)建購物車
rails generate scaffold Cart
rake db:migrate
可以看到 Cart 基本上沒有任何屬性,這是因?yàn)楫?dāng)前開發(fā)的時候我們還不需要它們。
2.2 LineItem 商品模型
我們稱在購物車中東西為“商品”,與之對應(yīng)的還有“產(chǎn)品 Product”,兩者的區(qū)別是 Product 具有某種靜態(tài)的屬性,沒有辦法說“兩種香皂”,但是很容易形容“兩個香皂”。LineItem 依附 Product 存在,同時也依附 Cart。
所以我們這樣定義它
rails generate scaffold LineItem product:references cart:belongs_to
rake db:migrate
這種定義方式會在模型層自動生成如下代碼
class LineItem < ApplicationRecord
belongs_to :product
belongs_to :cart
end
同時我們還需要在 Cart 和 Product 處進(jìn)一步完善這種關(guān)系
class Cart < ApplicationRecord
has_many :line_items, dependent: :destroy
end
其中的 dependent: :destroy 表示當(dāng) Cart 銷毀的時候,其中的 LineItem 都會被銷毀。
# 與 line_item 關(guān)系
has_many :line_items
before_destroy :ensure_not_referenced_by_any_line_item
private
def ensure_not_referenced_by_any_line_item
unless line_items.empty?
errors.add(:base, 'Line Items present')
throw :abort
end
end
這里說的是,在 Product 被銷毀前,必須執(zhí)行 ensure_not_referenced_by_any_line_item 這個方法,這個方法檢測如果沒有商品關(guān)聯(lián),才可以刪除,否則報錯。
我們可以對這個功能進(jìn)行測試,有
test "can't delete product in cart" do
assert_difference('Product.count', 0) do
delete product_url(products(:two))
end
end
2.3 會話
出于一些原因,我們需要在會話中保存 cart_id,用戶每添加一個商品,我們需要從會話中把 cart_id 取出來,然后通過標(biāo)識符在數(shù)據(jù)庫中查找購物車。
我們在 app/controllers/concerns/current_cart.rb 中寫入如下代碼
module CurrentCart
private
# 用 session 中的 cart_id 去查找 cart,如果沒有找到,就創(chuàng)建一個新的 cart,并在 session 中存儲 cart_id
def set_cart
@cart = Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
@cart = Cart.create
session[:cart_id] = @cart.id
end
end
app/controllers/concerns/ 這個文件夾中一般來說是一些獨(dú)立的邏輯模塊或者是重復(fù)使用的功能模塊,這樣可以提升代碼的可讀性以及維護(hù)性。
2.4 “加入購物車”
我們可以將某個 Product 加入某個 Cart ,其本質(zhì)是利用 Product 產(chǎn)生一個 LineItem。
所以需要現(xiàn)在商品目錄加上這個按鈕,最終達(dá)到調(diào)用 LineItem#create 的目的。
<%= number_to_currency(product.price) %>
<%= button_to 'Add to Cart', line_items_path(product_id: product) %>
其中
line_items_path(product_id: product)
就是調(diào)用 LineItem 創(chuàng)建方法 create 的意思,同時給它傳參 product_id
然后我們來完善 LineItem 的 create 方法。
首先在 LintItemController.rb 中引入 CurrentCart 模塊,并且在每次的 create 方法前都調(diào)用 :set_cart 方法
class LineItemsController < ApplicationController
include CurrentCart
before_action :set_cart, only: [:create]
因?yàn)樵?:set_cart 中會對 @cart 賦值,讓其為當(dāng)前對話 session 獨(dú)有的 cart,所以最終的效果就是 LineItem 創(chuàng)建前就有一個 @cart 屬性了。
然后修改 create 方法
# POST /line_items or /line_items.json
def create
# 根據(jù)傳入的 product_id 查找 product
product = Product.find(params[:product_id])
# build 方法與 new 方法類似,會創(chuàng)建一個與 @cart 和 product 都相關(guān)的 @line_item
@line_item = @cart.line_items.build(product: product)
respond_to do |format|
if @line_item.save
# 跳轉(zhuǎn)的對象不再是 @line_item,而是它的購物車
format.html { redirect_to @line_item.cart, notice: "Line item was successfully created." }
format.json { render :show, status: :created, location: @line_item }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @line_item.errors, status: :unprocessable_entity }
end
end
end
最后修改 cart 的 show 頁面,讓其可以顯示里面的 LineItem。
<%= notice %>
<% @cart.line_items.each do |item| %>
<% end %>
2.5 加入數(shù)量
對于一個商品來說,之前的設(shè)計是有問題的,比如說我們買了兩個香皂,那么不應(yīng)該是“香皂,香皂”的顯示兩遍,而是應(yīng)該“2 x 香皂”這樣的顯示,所以對于 LineItem 來說,數(shù)量是極其必要的。
所以創(chuàng)建遷移
rails generate migration add_quantity_to_line_items quantity:integer
但是還需要接著修改這個遷移,因?yàn)橐O(shè)置其默認(rèn)值為 1
class AddQuantityToLineItems < ActiveRecord::Migration[7.0]
def change
# 默認(rèn)值是 1
add_column :line_items, :quantity, :integer, default: 1
end
end
然后需要修改 add_cart 的行為,并不是每次“加入購物車”,都是會產(chǎn)生一個新的 LineItem 的。首先在 app/models/cart.rb 中加入方法
def add_product(product)
# 根據(jù) product_id 查找 current_item
current_item = line_items.find_by(product: product.id)
if current_item
# 查找到了,就數(shù)量增加 1
current_item.quantity += 1
else
# 沒查找到,就創(chuàng)建 line_item
current_item = line_items.build(product_id: product.id)
end
# 返回 current_item
current_item
end
然后修改 line_item#create
@line_item = @cart.add_product(product)
同時為了讓已有的 LineItem 數(shù)據(jù)依然顯示正確,需要創(chuàng)建一個遷移進(jìn)行修改
rails generate migration combine_items_in_cart
這個遷移沒法按照“約定”自動產(chǎn)生 change,所以需要自己手寫 up, down(這兩個方法似乎也是某種約定)
class CombineItemsInCart < ActiveRecord::Migration[7.0]
def up
Cart.all.each do |cart|
# 把購物車中同一個產(chǎn)品的多個商品替換為單個商品
sums = cart.line_items.group(:product_id).sum(:quantity)
sums.each do |product_id, quantity|
if quantity > 1
# 刪除同一個產(chǎn)品的多個商品
cart.line_items.where(product_id: product_id).delete_all
# 替換為單個商品
item = cart.line_items.build(product_id: product_id)
item.quantity = quantity
item.save!
end
end
end
end
def down
LineItem.where("quantity>1").each do |line_item|
line_item.quantity.times do
LineItem.create(
cart_id: line_item.cart_id,
product_id: line_item.product_id,
quantity: 1
)
end
line_item.destroy
end
end
end
2.6 清空購物車
清空購物車的本質(zhì)是將當(dāng)前的購物車刪除,所以先加入“清空按鈕”在 show.html 中
<%= notice %>
<%= item.quantity %> × | <%= item.product.title %> | <%= number_to_currency(item.total_price) %> |
Total | <%= number_to_currency(@cart.total_price) %> |
<%= button_to 'Empty Cart', @cart, method: :delete, data: {confirm: 'Are you sure?'} %>
并且補(bǔ)充相應(yīng)的方法即可。
2.7 局部渲染
我們希望在側(cè)邊欄也有購物車信息,所以我們考慮利用局部渲染。具體的知識在前面有,所以按照遞歸的思路,我們需要在 application.html.erb 中加入購物車
<%= render @cart %>
這個東西會去渲染 _cart.html.erb 文件,所以需要將它的內(nèi)容改得和 carts/show.html.erb 一樣
Total | <%= number_to_currency(cart.total_price) %> |
<%= button_to 'Empty Cart', @cart, method: :delete, data: {confirm: 'Are you sure?'} %>
這里面有一個
<%= render(cart.line_items) %>
這是因?yàn)閷τ?line_items 的渲染,在 cart_html.erb 中也出現(xiàn)了。這種局部渲染是一種集合渲染,所以渲染的模板在 _line_item.html.erb 中,修改如下
最終意識到其實(shí)沒有必要對 carts/show.html.erb 重復(fù)內(nèi)容,所以將其改為
<%= notice %>
<%= render @cart %>
因?yàn)樵趥?cè)邊欄中現(xiàn)在也有了 cart ,所以同樣需要 set_cart ,所以對于 StoreController 中加入如下代碼
class StoreController < ApplicationController
include CurrentCart
before_action :set_cart
def index
@products = Product.order(:title)
end
end
2.8 Ajax 購物車
現(xiàn)在每次進(jìn)行 Add Cart 操作,本質(zhì)都是在渲染整個 application.html.erb 頁面,這無疑是低效的,所以考慮只渲染側(cè)邊欄的購物車部分。
首先需要在 Add Cart 按鈕上添加 remote 參數(shù)
<%= button_to 'Add to Cart', line_items_path(product_id: product), remote:true %>
然后在 create 方法上加入神秘的 format.js ,我暫時還理解不了為啥,可以理解為啟用了 js 腳本
# POST /line_items or /line_items.json
def create
# 根據(jù)傳入的 product_id 查找 product
product = Product.find(params[:product_id])
# build 方法與 new 方法類似,會創(chuàng)建一個與 @cart 和 product 都相關(guān)的 @line_item
@line_item = @cart.add_product(product)
# respond_to 是一個方法,其參數(shù)為一個 block
# 我現(xiàn)在的理解是 respond_to 描述的是服務(wù)器對于客戶端的反應(yīng),或者說,這是瀏覽器上將執(zhí)行的步驟
# 這里就是先對于 html 進(jìn)行一個重定向操作,然后調(diào)用 js,最后 json
respond_to do |format|
if @line_item.save
# 跳轉(zhuǎn)的對象不再是 @line_item,而是 store 頁面
format.html { redirect_to store_index_url }
# 調(diào)用 js 腳本
format.js
format.json { render :show, status: :created, location: @line_item }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @line_item.errors, status: :unprocessable_entity }
end
end
end
然后就會調(diào)用 views/line_items/create.js.erb 這個文件,將這個文件寫入以下內(nèi)容
$('#cart').html("<%= j render(@cart) %>")
這個腳本描述了將 id = cart 的節(jié)點(diǎn)替換成 render(@cart) 的操作。
2.9 突出顯示
為了增強(qiáng)美工性,考慮引入 jQuery-ui。
jQuery 是 JavaScript 的一個好用的庫,里面有常見的 html 操作和一組簡單的 UI,我們需要更改 Gemfile 來安裝它
# Use jquery as the JavaScript library
gem 'jquery-rails'
gem 'jquery-ui-rails'
然后執(zhí)行
bundle install
然后新建 app/assets/javascripts/application.js 內(nèi)容為
//= require jquery
//= require jquery_ujs
//= require jquery-ui/effect.all
//= require_tree .
至于為啥是這個,并不知道為啥。這樣之后我們就可以使用 jquery-ui 了。
然后考慮如何對“剛剛點(diǎn)擊過”的 LineItem 做一個突出顯示,可以考慮在 create 中維護(hù)一個 @current_item
format.js { @current_item = @line_item }
然后在 _line_item.html.erb 中,將 current 標(biāo)出來
<% if line_item == @current_item %>
<% else %>
<% end %>
然后在 create.js.erb 中進(jìn)行渲染
$('#current_item').css({'background-color': '#88ff88'}).
animate({'background-color': '#114411'}, 1000)
2.10 輔助方法
我們希望可以在購物車內(nèi)商品數(shù)量為 0 的時候,不顯示購物車。
可以利用購物車長度作為判斷,這里我們用到了輔助方法(主要是為了讓代碼更加整潔)
<% if @cart %>
<%= hidden_div_if(@cart.line_items.empty?, id: 'cart') do %>
<%= render @cart %>
<% end %>
<% end %>
輔助方法為在 app/helpers 下的方法,腳手架會自動幫我們建好這些文件。
module ApplicationHelper
def hidden_div_if(condition, attributes={}, &block)
# 將 html 中具有 attributes 并滿足 condition 條件的 div 設(shè)置為 display: none
if condition
attributes["style"] = "display: none"
end
content_tag("div", attributes, &block)
end
end
同時我們需要修改 js 文件,使得購物車顯示的時候比較平滑
if ($('#cart tr').length === 1) { $('cart').show('blind', 1000) }
3 訂單 Order
3.1 Order 模型
訂單模型本質(zhì)上信息全都是收貨的信息,訂單具體有什么商品,其實(shí)并不是由 Order 決定的,而是由 LineItem 決定的。因此建立如下如下模型
rails generate scaffold Order name address:text email phone pay_type:integer
同時給 LineItem 添加外鍵
rails generate migration add_order_to_line_item order:references
最后融合遷移
rails db:migrate
然后就會發(fā)現(xiàn)融合不了,這是因?yàn)閷τ?LineItem,他有三個外鍵,分別是 Product, Cart, Order ,在遷移中自動生成的外鍵,都是不允許為空的,而現(xiàn)在,對于一個 LineItem ,它要么在 Cart 中,要么在 Order 中,所以總會有一個外鍵為空,所以需要修改多處地方。
首先要修改 LineItem 的兩個 migration
# 20230101081559_create_line_items.rb
class CreateLineItems < ActiveRecord::Migration[7.0]
def change
create_table :line_items do |t|
t.references :product, null: false, foreign_key: true
t.belongs_to :cart, null: true, foreign_key: true
t.timestamps
end
end
end
# 20230102121647_add_order_to_line_item.rb
class AddOrderToLineItem < ActiveRecord::Migration[7.0]
def change
add_reference :line_items, :order, null: true , foreign_key: true
end
end
然后還要在 model 中進(jìn)行數(shù)據(jù)關(guān)系的定義
class LineItem < ApplicationRecord
belongs_to :product
# optional 表示外鍵可以為空
belongs_to :cart, optional: true
belongs_to :order, optional: true
def total_price
product.price * quantity
end
end
class Order < ApplicationRecord
enum pay_type: {
"Check" => 0,
"Credit card" => 1,
"Purchase order" => 2
}
has_many :line_items, dependent: :destroy
end
3.2 生成訂單
生成訂單的過程是一個將購物車中所有的 LineItem 都放到 Order 中的一個過程,我們可以用一個方法描述這個過程,定義在 model 中。
class Order < ApplicationRecord
enum pay_type: {
"Check" => 0,
"Credit card" => 1,
"Purchase order" => 2
}
has_many :line_items, dependent: :destroy
validates :name, :address, :email, :phone, presence: true
validates :pay_type, inclusion: pay_types.keys
def add_line_items_from_cart(cart)
cart.line_items.each do |item|
item.cart_id = nil
line_items << item
end
end
end
可以看到還新增了一些字段的驗(yàn)證約束,然后考慮生成表格,首先在 _cart.html.erb 加上生成 Order 的按鈕
<%= button_to 'Checkout', new_order_path, method: :get %>
這個方法會調(diào)用 new 方法,所以我們需要寫一下 new.html.erb 這個模板
還有與之相關(guān)的 _form.html.erb
<%= form_with(model: order) do |form| %>
<% if order.errors.any? %>
<% order.errors.each do |error| %>
<% end %>
<% end %>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name, size: 40 %>
<%= form.label :address, style: "display: block" %>
<%= form.text_area :address, rows: 3, cols: 37 %>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email, size: 40 %>
<%= form.label :phone, style: "display: block" %>
<%= form.text_field :phone, size: 40 %>
<%= form.label :pay_type, style: "display: block" %>
<%= form.select :pay_type, Order.pay_types.keys, prompt: 'Select a payment method' %>
<%= form.submit 'Place Order'%>
<% end %>
并且美化樣式
.project_form {
fieldset {
background: #efe;
h2 {
color: #dfd;
background: #141;
font-family: sans-serif;
padding: 0.2em 1em;
}
div {
margin-bottom: 0.3em;
}
}
form {
label {
width: 5em;
float: left;
text-align: right;
padding-top: 0.2em;
margin-right: 0.1em;
display: block;
}
select, textarea, input {
margin-left: 0.5em;
}
.submit {
margin-left: 4em;
}
br {
display: none;
}
}
}
在 Order#create 的過程中,需要清空購物車,所以需要修改一下 create 方法
# POST /orders or /orders.json
def create
@order = Order.new(order_params)
@order.add_line_items_from_cart(@cart)
respond_to do |format|
if @order.save
# 清空購物車
Cart.destroy(session[:cart_id])
session[:cart_id] = nil
format.html { redirect_to store_index_url(@order), notice: "Thank you for your order." }
format.json { render :show, status: :created, location: @order }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @order.errors, status: :unprocessable_entity }
end
end
end
3.3 訂單展示
作為管理端,需要看到所有的訂單,所以考慮修改 index.html.erb 這個模板,將其改成表格形式會更漂亮一些,同時加上一些操作和跳轉(zhuǎn)。
<%= form_with(model: order) do |form| %>
<% if order.errors.any? %>
<% order.errors.each do |error| %>
<% end %>
<% end %>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name, size: 40 %>
<%= form.label :address, style: "display: block" %>
<%= form.text_area :address, rows: 3, cols: 37 %>
<%= form.label :email, style: "display: block" %>
<%= form.text_field :email, size: 40 %>
<%= form.label :phone, style: "display: block" %>
<%= form.text_field :phone, size: 40 %>
<%= form.label :pay_type, style: "display: block" %>
<%= form.select :pay_type, Order.pay_types.keys, prompt: 'Select a payment method' %>
<%= form.submit 'Place Order'%>
<% end %>
對于訂單的詳情展示,可以仿照 _cart.html.erb 書寫
Total | <%= number_to_currency(order.total_price) %> |
4 用戶 User
4.1 User 模型
考慮用戶具有用戶名,密碼,角色三個屬性,模型如下
rails generate scaffold User name:string password:digest role:integer
rails db:migrate
對于密碼部分,可以借助插件完成“確認(rèn)密碼的功能”
class User < ApplicationRecord
has_secure_password
end
同時在 gemfile 中填入這個插件
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"
然后使用
bundle install
同時還有關(guān)于 role 的設(shè)計,目前有兩個角色,一個是 Buyer,一個是 Admin,其中 Admin 的權(quán)限更高。
class User < ApplicationRecord
enum role: {
"Buyer" => 0,
"Admin" => 1
}
validates :name, presence: true, uniqueness: true
validates :role, presence: true
validates :role, inclusion: roles.keys
has_secure_password
end
注意如果希望 role 作為一個枚舉變量,那么這里一定要定義 enum 的名字為 role,不能叫 role_type 或者其他任何的名字,都不會讓其具有枚舉的效果,這大概就是神秘的 rails 吧。
4.2 控制器與頁面
身份驗(yàn)證就是登錄相關(guān)的功能,這里需要新建兩個控制器,sessions 用于為登錄和登出提供支持,admin 用于為管理員提供歡迎界面。
其中 sessions 只有兩個動作,new, create 對應(yīng)登入,destroy 對應(yīng)登出
rails generate controller Sessions new create destroy
Admin 只有一個動作 index,代表歡迎界面。
rails generate controller Admin index
這也啟發(fā)我,其實(shí)寫一個頁面就是寫一個 controller 和一個 view 而已,這是因?yàn)?view 似乎沒有辦法單獨(dú)成為一個路由資源。
4.3 登錄登出
登入功能就是填一個表單,所以在 new.html.erb 中寫入
<% if flash[:alert] %>
<%= flash[:alert] %>
<% end %>
<%= form_tag do %>
<% end %> %>
可以看到,因?yàn)?Sessions 并沒有與模型關(guān)聯(lián),所以我們并沒有用 form_for ,只是一個普通的 form_tag。收集的信息進(jìn)入了 params。
然后我們在 create 中利用 params 中的信息保存到 session 中
def create
# 這里更加明顯,create 會有一個 params,params 的來歷就是 new 填的表單
user = User.find_by(name: params[:name])
# try 對于為 nil 的 user,會直接進(jìn)入 else
if user.try(:authenticate, params[:password])
session[:user_id] = user.id
session[:user_role] = user.role
# 如果是 Buyer ,就定向到商店,否則定向到 admin 的歡迎界面
if user.role == "Buyer"
redirect_to store_index_url
else
redirect_to admin_url
end
else
redirect_to login_url, alert: "Invalid user/password combination"
end
end
同時完成相應(yīng)界面的跳轉(zhuǎn),我們在 session 中保存了用戶的 id 和權(quán)限信息。
退出登錄的狀態(tài)就很簡單,就是將 session 中的信息注銷掉即可
def destroy
session[:user_id] = nil
session[:user_role] = nil
redirect_to store_index_url, notice: "Logged out"
end
對于管理界面,可以自己設(shè)計一個
It's <%= Time.now %>
We hava <%= pluralize(@total_orders, 'order') %>
最后需要修改路由
get 'admin' => 'admin#index'
controller :sessions do
get 'login' => :new
post 'login' => :create
delete 'logout' => :destroy
end
4.4 訪問限制
訪問限制可以在 application_controller.rb 中利用 before_action 實(shí)現(xiàn)
class ApplicationController < ActionController::Base
before_action :authorize
protected
def authorize
unless User.find_by(id: session[:user_id])
redirect_to login_url, notice: "Pleas log in."
end
end
end
然后在各個控制器,選擇是否跳過 authorize 即可
class SessionsController < ApplicationController
skip_before_action :authorize
這時會發(fā)現(xiàn)所有的 test 基本上都癱瘓了,這是因?yàn)?test 不會自動登錄,所有在 test/test_helper.rb 中寫入如下代碼即可
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
class ActionDispatch::IntegrationTest
def login_as(user)
post login_url, params: {name: user.name, password: 'secret'}
end
def logout
delete logout_url
end
def setup
login_as(users(:one))
end
end
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
4.5 權(quán)限顯示
我們希望對于管理者,可以看到更多的界面,而對于非管理者,則不需要看到這些界面
<% if current_user and current_user.role == 'Admin' %>
<% end %>
<% if current_user %>
<%= current_user.name %> Logged in.
<%= link_to 'Logout', logout_path, method: :delete %>
<% else %>
Please <%= link_to 'Login', login_url %>
<% end %>
這些需要借助 current_user 這個方法,這個方法定義在 application_controller.rb 中
helper_method :current_user
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
5 收藏夾 Favourite
5.1 Favourite 模型
每個用戶都有一個收藏夾,二者是一對一關(guān)系,所以沒有必要單獨(dú)做一個實(shí)體,可以直接使用 User 模型。但是在實(shí)際思考的時候,卻應(yīng)當(dāng)有收藏夾這個模型,比較方便思考。
5.2 FavorItem 收藏品模型
收藏品模型描述的是 Product 和 Favourite 之間的“多對多關(guān)系”,所以需要這樣建立模型
rails generate scaffold FavorItem product:references user:belongs_to
rake db:migrate
對于 User 模型,補(bǔ)充如下代碼,也就是當(dāng) User 持有一個 favor_items 集合,同時當(dāng) User 銷毀的時候,會銷毀所有的 favor_items 。
has_many :favor_items, dependent: :destroy
對于 Product 模型,補(bǔ)充如下代碼,同樣 Product 持有一個 favor_item 集合,同時在銷毀前要檢驗(yàn)是否可以銷毀。
has_many :favor_items
before_destroy :ensure_not_referenced_by_any_favor_item
def ensure_not_referenced_by_any_favor_item
unless favor_items.empty?
errors.add(:base, 'Line Items present')
throw :abort
end
end
5.3 加入收藏夾
在 store 界面上,除了有“加入購物車”之外,應(yīng)當(dāng)有“加入收藏夾”的功能,可以如此修改 store 界面
<%= button_to 'Add to Favourite', favor_items_path(product_id: product) %>
可以看到需要修改 favor_item 的 create 方法
def create
# 根據(jù)傳入的 product_id 查找 product
product = Product.find(params[:product_id])
@favor_item = @user.add_product(product)
respond_to do |format|
if @favor_item.save
format.html { redirect_to store_index_url }
format.json { render :show, status: :created, location: @favor_item }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @favor_item.errors, status: :unprocessable_entity }
end
end
end
可以看到需要獲得一個收藏夾的 @user,與 @cart 類似,所以需要一個 set_user 的過程
def set_user
@user = User.find(session[:user_id])
rescue ActiveRecord::RecordNotFound
redirect_to store_index_url
end
5.4 收藏夾展示
類似于一個產(chǎn)品目錄的子集,可以寫 index.html.erb
<%= notice %>
可以看到除了外面套了一個表之外,主體是對于每個 favor_item 的渲染,所以需要修改 _favor_item.html.erb
<%= image_tag(favor_item.product.image_url, class: 'list_image') %>
<%= number_to_currency(favor_item.product.price) %>
<%= button_to 'Add to Cart', line_items_path(product_id: favor_item.product), remote:true %>
<%= button_to 'Remove', favor_item, :method => :delete %>
6 促銷活動 Activity
6.1 Activity 模型
促銷活動只有一個屬性就是名字,具體的促銷也在這里體現(xiàn)體現(xiàn),所以應(yīng)當(dāng)在終端中輸入如下示例
rails generate scaffold Activity name:string disconut:integer
rake db:migrate
6.2 Prompt 促銷項(xiàng)模型
促銷項(xiàng)都是外鍵,用于關(guān)聯(lián) Product 產(chǎn)品和 Activity 活動,形成“活動-產(chǎn)品” 的多對多關(guān)系。
rails generate scaffold Prompt product:references activity:belongs_to
rake db:migrate
同時完善 Product 模型
has_many :prompts, dependent: :destroy
和 Activity 模型
has_many :prompts, dependent: :destroy
6.3 新建活動
一個活動由本身和它包括的商品組成,在創(chuàng)建的時候,可以先創(chuàng)建好活動,確定活動的名稱和折扣力度,然后再向這個活動添加涉及的商品。創(chuàng)建活動這個操作只有管理員可以干,所以我們用一個管理鏈接指向 activities 的展示頁面
然后對于這個頁面,我們只是展示其名稱和折扣
<%= notice %>
<% @activities.each do |activity| %>
Name:
<%= activity.name %>
Discount:
<%= number_to_percentage(activity.discount, precision: 0) %>
<%= link_to "Show this activity", activity %>
<% end %>
<%= link_to "New activity", new_activity_path %>
在活動中添加商品的操作,可以考慮在具體的活動界面進(jìn)行添加,也就是 _activity.html.erb 中添加
Name:
<%= activity.name %>
Discount:
<%= number_to_percentage(activity.discount, precision: 0) %>
<% Product.all.each do |product| %>
<%= button_to 'Add to Activity', prompts_path(product_id: product, activity_id: activity)%>
<% end %>
<% activity.prompts.each do |prompt| %>
<%= prompt.product.title %>
<% end %>
對于創(chuàng)建一個 prompt,與以往不同,需要傳入兩個參數(shù),也就是 prompt 的兩端 product 和 activity,也就是這樣
<%= button_to 'Add to Activity', prompts_path(product_id: product, activity_id: activity)%>
所以需要修改 prompt 的 create 方法
# POST /prompts or /prompts.json
def create
# 根據(jù)傳入的 product_id 查找 product
product = Product.find(params[:product_id])
activity = Activity.find(params[:activity_id])
@prompt = activity.add_product(product)
respond_to do |format|
if @prompt.save
format.html { redirect_to activity_url(activity), notice: "Prompt was successfully created." }
format.json { render :show, status: :created, location: @prompt }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @prompt.errors, status: :unprocessable_entity }
end
end
end
可以看到進(jìn)行了兩次查找,最終利用兩次查找的信息建立了 prompt。和 favor_item 的 add_product 一致,對于加入活動的產(chǎn)品,只能加入一次
def add_product(product)
# 根據(jù) product_id 查找 current_item
current_item = prompts.find_by(product: product.id)
if current_item
# 查找到了,就啥都不干
else
# 沒查找到,就創(chuàng)建 prompt
current_item = prompts.build(product_id: product.id)
end
# 返回 current_item
current_item
end
6.4 實(shí)現(xiàn)折扣
促銷大概需要兩個兩個機(jī)制
在 store 界面上展示折扣在加入購物車或者訂單的時候?qū)嶋H發(fā)生折扣
展示折扣這個功能很好實(shí)現(xiàn),只需要在 store 界面中利用 product 檢索 prompt 進(jìn)而檢索 activity.discount 即可。
<% product.prompts.each do |prompt| %>
× <%= number_to_percentage(prompt.activity.discount, precision: 0) %>
<% end %>
注意這里用到了 number_to_percentage 方法,可以將普通整數(shù)轉(zhuǎn)換成百分制(除以 100 加百分號)。
發(fā)生實(shí)際折扣,需要更改 price 的計算方式,原來對于價格的計算,是 Cart 或者 Order 對于其所有的 item 的 price 進(jìn)行求和,line_item 的價格計算,是 Product.price 和 quantity 的乘積,如下所示
def total_price
product.price * quantity
end
可見,只要修改 price 即可,所以在 product 中新寫一個方法去獲得折扣價格
def getDiscountPrice
discount = price
prompts.each do |prompt|
discount *= (prompt.activity.discount / 100.0)
end
discount
end
然后修改 total_price 為
def total_price
product.getDiscountPrice * quantity
end
即可。
6.5 展示折扣
對于普通用戶來說,沒有權(quán)限建立活動,但是有權(quán)限瀏覽活動,所以可以實(shí)現(xiàn)一個活動界面用于瀏覽,但是考慮到 index.html.erb 已經(jīng)用于給管理者新建活動使用了,所以考慮新開設(shè)一個界面,在控制器中新定義一個方法
def show_activity
@show_activities = Activity.all
end
然后建立對應(yīng)的 show_activity.html.erb 這個頁面,其內(nèi)容如下
<% @show_activities.each do |activity| %>
<%= activity.name %>
折扣力度:
<%= number_to_percentage(activity.discount, precision: 0) %>
促銷產(chǎn)品:
<% activity.prompts.each do |prompt| %>
<%= prompt.product.title %>,
<% end %>
<% end %>
其后完善鏈接即可。
柚子快報激活碼778899分享:博客 Ruby設(shè)計-開發(fā)日志
參考文章
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。
請?jiān)谥黝}配置——文章設(shè)置里上傳
掃描二維碼手機(jī)訪問