看完rails-practice所有实例和内容,实现在订单创建之后异步队列发送邮件,完成加入购物车和订单列表等

This commit is contained in:
zhoushixu 2022-05-19 18:56:45 +08:00
parent 121a18dbd0
commit 8ed7588b41
66 changed files with 1371 additions and 32 deletions

View File

@ -3,7 +3,6 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/shop.iml" filepath="$PROJECT_DIR$/.idea/shop.iml" />
<module fileurl="file://$USER_HOME$/下载/shop3.3/.idea/shop3.3.iml" filepath="$USER_HOME$/下载/shop3.3/.idea/shop3.3.iml" />
</modules>
</component>
</project>

View File

@ -31,6 +31,8 @@
<orderEntry type="library" scope="PROVIDED" name="actioncable (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionmailer (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionpack (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionpack-action_caching (v1.2.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionpack-page_caching (v1.2.4, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionview (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activejob (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activemodel (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
@ -46,6 +48,7 @@
<orderEntry type="library" scope="PROVIDED" name="builder (v3.2.4, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.3.13, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="byebug (v11.0.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="cancan (v1.6.10, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.15.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="childprocess (v3.0.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="chromedriver-helper (v2.1.1, rbenv: 2.3.7) [gem]" level="application" />
@ -54,6 +57,7 @@
<orderEntry type="library" scope="PROVIDED" name="coffee-script-source (v1.12.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="commonjs (v0.2.7, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.1.10, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.2.5, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="database_cleaner (v2.0.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="database_cleaner-active_record (v2.0.1, rbenv: 2.3.7) [gem]" level="application" />
@ -63,12 +67,12 @@
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.10.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="erubis (v2.7.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="execjs (v2.8.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="factory_girl (v4.9.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="factory_girl_rails (v4.9.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="factory_bot (v5.2.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ffi (v1.15.5, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="globalid (v0.4.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="haml (v5.2.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="haml-rails (v2.0.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="hiredis (v0.6.3, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="html2haml (v2.2.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.10.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="io-like (v0.3.1, rbenv: 2.3.7) [gem]" level="application" />
@ -92,6 +96,7 @@
<orderEntry type="library" scope="PROVIDED" name="mini_portile2 (v2.4.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.15.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="msgpack (v1.3.3, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="mustermann (v1.1.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="mysql2 (v0.5.4, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.5.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.10.10, rbenv: 2.3.7) [gem]" level="application" />
@ -99,6 +104,7 @@
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v4.0.7, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="puma (v3.12.6, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack (v2.2.3, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack-protection (v2.2.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack-test (v1.1.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rails (v5.2.8, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rails-dom-testing (v2.0.3, rbenv: 2.3.7) [gem]" level="application" />
@ -108,6 +114,13 @@
<orderEntry type="library" scope="PROVIDED" name="ransack (v2.4.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rb-fsevent (v0.11.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rb-inotify (v0.10.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis (v4.1.4, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-actionpack (v5.3.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-activesupport (v5.3.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-namespace (v1.6.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-rack (v2.1.4, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-rails (v5.0.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="redis-store (v1.9.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ref (v2.0.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v1.8.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="responders (v2.4.1, rbenv: 2.3.7) [gem]" level="application" />
@ -116,6 +129,7 @@
<orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.11.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rspec-rails (v5.1.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.11.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby_dep (v1.5.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby_parser (v3.19.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v1.3.0, rbenv: 2.3.7) [gem]" level="application" />
@ -124,6 +138,8 @@
<orderEntry type="library" scope="PROVIDED" name="sass-rails (v5.0.7, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v3.142.7, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sexp_processor (v4.16.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sidekiq (v5.2.9, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sinatra (v2.2.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="spring (v2.0.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="spring-watcher-listen (v2.0.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sprockets (v3.7.2, rbenv: 2.3.7) [gem]" level="application" />
@ -144,6 +160,8 @@
<orderEntry type="library" scope="PROVIDED" name="web-console (v3.7.0, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.7.5, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="will_paginate (v3.3.1, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="will_paginate-bootstrap (v1.0.2, rbenv: 2.3.7) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="xpath (v3.2.0, rbenv: 2.3.7) [gem]" level="application" />
</component>
<component name="RModuleSettingsStorage">

24
Gemfile
View File

@ -43,8 +43,30 @@ gem "haml"
gem "html2haml"
gem "haml-rails"
gem 'liquid-rails'
# 请求
gem "responders"
#权限控制
gem "cancan"
#分页
gem 'will_paginate-bootstrap'
# Cache
gem 'actionpack-page_caching'
gem 'actionpack-action_caching'
#Redis
gem 'redis-rails'
gem 'redis-namespace'
gem "hiredis"
#队列
gem 'sidekiq', '~> 5.2.9'
gem 'sinatra', :require => nil
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
gem 'redis', '~> 4.1.4'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

View File

@ -18,6 +18,10 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionpack-action_caching (1.2.2)
actionpack (>= 4.0.0)
actionpack-page_caching (1.2.4)
actionpack (>= 4.0.0)
actionview (5.2.8)
activesupport (= 5.2.8)
builder (~> 3.1)
@ -53,6 +57,7 @@ GEM
msgpack (~> 1.2)
builder (3.2.4)
byebug (11.0.1)
cancan (1.6.10)
capybara (3.15.1)
addressable
mini_mime (>= 0.1.3)
@ -74,6 +79,7 @@ GEM
coffee-script-source (1.12.2)
commonjs (0.2.7)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
crass (1.0.6)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
@ -105,6 +111,7 @@ GEM
haml (>= 4.0.6, < 6.0)
html2haml (>= 1.0.1)
railties (>= 5.1)
hiredis (0.6.3)
html2haml (2.2.0)
erubis (~> 2.7.0)
haml (>= 4.0, < 6)
@ -159,6 +166,8 @@ GEM
mini_portile2 (2.4.0)
minitest (5.15.0)
msgpack (1.3.3)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
mysql2 (0.5.4)
nio4r (2.5.2)
nokogiri (1.10.10)
@ -167,6 +176,8 @@ GEM
public_suffix (4.0.7)
puma (3.12.6)
rack (2.2.3)
rack-protection (2.2.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.8)
@ -201,6 +212,25 @@ GEM
rb-fsevent (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.1.4)
redis-actionpack (5.3.0)
actionpack (>= 5, < 8)
redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.3.0)
activesupport (>= 3, < 8)
redis-store (>= 1.3, < 2)
redis-namespace (1.6.0)
redis (>= 3.0.4)
redis-rack (2.1.4)
rack (>= 2.0.8, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.9.1)
redis (>= 4, < 5)
ref (2.0.0)
regexp_parser (1.8.2)
responders (2.4.1)
@ -223,6 +253,7 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.11.0)
ruby2_keywords (0.0.5)
ruby_dep (1.5.0)
ruby_parser (3.19.1)
sexp_processor (~> 4.16)
@ -242,6 +273,16 @@ GEM
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
sexp_processor (4.16.1)
sidekiq (5.2.9)
connection_pool (~> 2.2, >= 2.2.2)
rack (~> 2.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 4.2)
sinatra (2.2.0)
mustermann (~> 1.0)
rack (~> 2.2)
rack-protection (= 2.2.0)
tilt (~> 2.0)
spring (2.0.2)
activesupport (>= 4.2)
spring-watcher-listen (2.0.1)
@ -289,6 +330,9 @@ GEM
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (3.3.1)
will_paginate-bootstrap (1.0.2)
will_paginate (>= 3.0.3)
xpath (3.2.0)
nokogiri (~> 1.8)
@ -296,8 +340,11 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
actionpack-action_caching
actionpack-page_caching
bootsnap (>= 1.1.0)
byebug
cancan
capybara (>= 2.15)
chromedriver-helper
coffee-rails (~> 4.2)
@ -306,6 +353,7 @@ DEPENDENCIES
factory_bot
haml
haml-rails
hiredis
html2haml
jbuilder (~> 2.5)
jquery-rails
@ -316,9 +364,15 @@ DEPENDENCIES
puma (~> 3.11)
rails (~> 5.2.8)
ransack
redis (~> 4.1.4)
redis-namespace
redis-rails
responders
rspec-rails (~> 5.1.2)
sass-rails (~> 5.0)
selenium-webdriver
sidekiq (~> 5.2.9)
sinatra
spring
spring-watcher-listen (~> 2.0.0)
therubyracer
@ -329,6 +383,7 @@ DEPENDENCIES
tzinfo-data
uglifier (>= 1.3.0)
web-console (>= 3.3.0)
will_paginate-bootstrap
RUBY VERSION
ruby 2.3.7p456

View File

@ -10,6 +10,8 @@ jQuery ->
$('#editProductDescription').val(data['description'])
$('#editProductPrice').val(data['price'])
$("#editProductForm").attr('action', '/products/'+data['id'])
.on "ajax:error", (e, xhr, status, error) ->
alert(JSON.parse(xhr.responseText).error)
$("#editProductForm")

View File

@ -1,4 +1,10 @@
require "application_responder"
class ApplicationController < ActionController::Base
self.responder = ApplicationResponder
respond_to :html, :xml, :json, :js
before_action :authenticate_user!
skip_before_action :authenticate_user!, only: [:index, :show, :top]
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters

View File

@ -0,0 +1,78 @@
class CheckoutController < ApplicationController
def cart
@cart_items = current_user.cart_items
end
# Cart => Checkout
def checkout
# OPTIMIZE
items_params.each do |item|
current_user.cart_items.update(item[0], quantity: item[1]["quantity"])
end
redirect_to address_path, notice: "确认你的收货地址"
end
# GET
# Checkout => Address
def address
@address = current_user.address
if !@address
@address=Address.new
end
end
# PATCH
# Address => Confirm
def confirm
current_user.address.update(address_params)
@order = Order.create(user: current_user, number: Time.zone.now.strftime("%y%m%d%H%M%S"), payment_state: "confirm")
current_user.cart_items.each do |item|
@order.line_items << LineItem.new(quantity: item.quantity, variant: item.variant)
end
current_user.cart_items.destroy
redirect_to order_path(@order.number), notice: "订单已确认,请支付。"
end
# 点击支付按钮,跳转到支付宝页面
def pay
order = Order.find_by(number: params[:number])
alipay_url = Alipay::Service.create_partner_trade_by_buyer_url(
out_trade_no: order.number,
subject: order.number,
price: '10.00',
quantity: 12,
logistics_type: 'DIRECT',
logistics_fee: '0',
logistics_payment: 'SELLER_PAY',
return_url: order_url(order.number),
notify_url: notify_url
)
redirect_to alipay_url, notice: "应该跳转到支付页面"
end
# 实时到账交易成功,返回 TRADE_SUCCESS
def notify
notify_params = params.except(*request.path_parameters.keys)
logger.info notify_params
if Alipay::Notify.verify?(notify_params) and ['TRADE_FINISHED', 'TRADE_SUCCESS'].include?(notify_params[:trade_status])
out_trade_no = notify_params[:out_trade_no]
order = Order.find_by(number: out_trade_no) || raise(ActiveRecord::RecordNotFound)
if order.payment_state == "confirm"
order.update(payment_state: "paid")
end
render text: "success"
else
render text: "fail"
end
end
private
def address_params
params.require(:address).permit(:state, :city, :address, :address2, :zipcode, :receiver, :phone)
end
def items_params
params[:item].permit!
end
end

View File

@ -0,0 +1,46 @@
class OrdersController < ApplicationController
before_action :set_order, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
def index
@orders = Order.paginate(:page => params[:page], :per_page => 3)
respond_with(@orders)
end
def show
respond_with(@order)
end
def new
@order = Order.new
respond_with(@order)
end
def edit
end
def create
@order = Order.new(order_params)
@order.save
respond_with(@order)
end
def update
@order.update(order_params)
respond_with(@order)
end
def destroy
@order.destroy
respond_with(@order)
end
private
def set_order
@order = Order.find_by(number: params[:id])
end
def order_params
params[:order]
end
end

View File

@ -1,5 +1,6 @@
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update destroy ]
caches_action :index, layout:false
# GET /products or /products.json
def index
@ -20,6 +21,11 @@ class ProductsController < ApplicationController
def top
end
def buy
@product = Product.find(params[:product_id])
@variant = @product.variants.find(params[:variant_id])
cart_item = CartItem.find_or_create_by(variant: @variant, user: current_user)
cart_item.increment!(:quantity, params[:quantity].to_i)
redirect_to @product, notice: "已添加到购物车"
end
# GET /products/new
def new
@ -38,7 +44,6 @@ class ProductsController < ApplicationController
# POST /products or /products.json
def create
@product = Product.new(product_params)
respond_to do |format|
if @product.save
format.html { redirect_to product_url(@product), notice: "Product was successfully created." }

View File

@ -0,0 +1,7 @@
class OrderCreateJob < ApplicationJob
queue_as :default
def perform(order)
OrderMailer.confirm_email(order).deliver_now
end
end

View File

@ -0,0 +1,7 @@
class OrderMailer < ApplicationMailer
def confirm_email(order)
@user = order.user
@order = order
mail(to: @user.email, subject: "您的订单 #{@order.number} 已经确认")
end
end

40
app/models/ability.rb Normal file
View File

@ -0,0 +1,40 @@
class Ability
include CanCan::Ability
def initialize(user)
# Define abilities for the passed in user here. For example:
#
# user ||= User.new # guest user (not logged in)
# if user.admin?
# can :manage, :all
# else
# can :read, :all
# end
#
# The first argument to `can` is the action you are giving the user
# permission to do.
# If you pass :manage it will apply to every action. Other common actions
# here are :read, :create, :update and :destroy.
#
# The second argument is the resource the user can perform the action on.
# If you pass :all it will apply to every resource. Otherwise pass a Ruby
# class of the resource.
#
# The third argument is an optional hash of conditions to further filter the
# objects.
# For example, here the user can only update published articles.
#
# can :update, Article, :published => true
#
# See the wiki for details:
# https://github.com/ryanb/cancan/wiki/Defining-Abilities
#
user ||= User.new # guest user (not logged in)
if user.role=='admin'
can :manage, :all
else
can :read, :all
can :manage, Address, :user_id => user.id
end
end
end

View File

@ -1,3 +1,3 @@
class Address < ApplicationRecord
belongs_to :user
belongs_to :user,inverse_of: :address
end

4
app/models/cart_item.rb Normal file
View File

@ -0,0 +1,4 @@
class CartItem < ActiveRecord::Base
belongs_to :user
belongs_to :variant
end

5
app/models/catalog.rb Normal file
View File

@ -0,0 +1,5 @@
class Catalog < ApplicationRecord
has_many :subcatalogs, class_name: "Catalog", foreign_key: "parent_catalog_id"
belongs_to :parent_catalog, class_name: "Catalog"
has_many :products
end

View File

@ -1,4 +1,6 @@
class LineItem < ApplicationRecord
belongs_to :order
belongs_to :variant
belongs_to :user
delegate :price, to: :variant, prefix: true
end

View File

@ -1,5 +1,20 @@
class Order < ApplicationRecord
class Order < ActiveRecord::Base
belongs_to :user,counter_cache: true
has_many :line_items
has_many :line_items, -> { includes :variant }
has_many :variants, through: :line_items
validates :card_number, presence: true, if: :paid_with_card?
after_create :send_create_email
def send_create_email
OrderCreateJob.perform_later(self)
end
def paid_with_card?
payment_state == "card"
end
after_create do
line_items.each do |line_item|
line_item.variant.decrement!(:on_hand, line_item.quantity)
end
end
end

28
app/models/person.rb Normal file
View File

@ -0,0 +1,28 @@
class Person
extend ActiveModel::Callbacks
define_model_callbacks :create
def create
run_callbacks :create do
puts "I am in create method."
end
end
before_create :action_before_create
def action_before_create
puts "I am in before action of create."
end
after_create :action_after_create
def action_after_create
puts "I am in after action of create."
end
around_create :action_around_create
def action_around_create
puts "I am in around action of create."
yield
puts "I am in around action of create."
end
end

View File

@ -3,4 +3,41 @@ class Product < ActiveRecord::Base
scope :state, -> { where(state:1) }
scope :is_delete, -> { where("deleted_at is not null") }
has_many :variants
accepts_nested_attributes_for :variants
before_validation do
puts "before_validation"
end
after_validation do
puts "after_validation"
end
before_save do
puts "before_save"
end
around_save :test_around_save
def test_around_save
puts "begin around_save"
yield
puts "end around_save"
end
before_create do
puts "before_create"
end
around_create :test_around_create
def test_around_create
puts "begin around_create"
yield
puts "end around_create"
end
after_create do
puts "after_create"
end
after_save do
puts "after_save"
end
after_commit do
puts "after_commit"
end
after_rollback do
puts "after_rollback"
end
end

View File

@ -4,7 +4,9 @@ class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable
has_one :address,dependent: :destroy
has_one :address,dependent: :destroy,inverse_of: :user
accepts_nested_attributes_for :address, update_only: true
has_many :orders
has_and_belongs_to_many :variants
has_many :cart_items, -> { includes(:variant).order("cart_items.updated_at asc") }
end

View File

@ -1,2 +1,6 @@
class Variant < ApplicationRecord
belongs_to :product
has_many :line_items
has_many :orders, through: :line_items
has_and_belongs_to_many :users
end

View File

@ -0,0 +1,53 @@
<%- model_class = Product -%>
<div class="page-header">
<h1>收货地址</h1>
</div>
<%= form_for @address, url: confirm_path do |f| %>
<div class="row">
<div class="col col-md-8">
<div class="form-group">
<%= f.label :state, class: "control-label" %><br />
<%= f.text_field :state, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :city, class: "control-label" %><br />
<%= f.text_field :city, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :address, class: "control-label" %><br />
<%= f.text_field :address, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :address2, class: "control-label" %><br />
<%= f.text_field :address2, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :zipcode, class: "control-label" %><br />
<%= f.text_field :zipcode, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :receiver, class: "control-label" %><br />
<%= f.text_field :receiver, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :phone, class: "control-label" %><br />
<%= f.text_field :phone, class: "form-control" %>
</div>
</div>
<div class="col col-md-4">
<div class="panel panel-default">
<div class="panel-footer">
<%= submit_tag "确认订单", :class => 'btn btn-info btn-block' %>
</div>
</div>
</div>
</div>
<% end %>

View File

@ -0,0 +1,44 @@
<%- model_class = Product -%>
<div class="page-header">
<h1>购物车</h1>
</div>
<% if @cart_items.empty? %>
购物车为空
<% else %>
<%= form_tag checkout_path do %>
<div class="row">
<div class="col col-md-8">
<div class="panel panel-default">
<table class="table">
<thead>
<tr>
<th width="50%">商品</th>
<th width="10%">大小</th>
<th class="text-right" width="10%">价格</th>
<th width="10%">数量</th>
<th width="20%" class="text-right">合计</th>
</tr>
</thead>
<% @cart_items.each do |item| %>
<tr>
<td><%= item.variant.product.name %></td>
<td><%= item.variant.size %></td>
<td class="text-right"><%= item.variant.price %></td>
<td><%= text_field_tag "item[#{item.id}][quantity]", item.quantity %></td>
<td class="text-right"><%= number_to_currency item.quantity * item.variant.price %></td>
</tr>
<% end %>
</table>
</div>
</div>
<div class="col col-md-4">
<div class="panel panel-default">
<div class="panel-footer">
<%= submit_tag "确认收货地址", :class => 'btn btn-info btn-block' %>
</div>
</div>
</div>
</div>
<% end %>
<% end %>

View File

@ -50,8 +50,7 @@
<div class="navbar-collapse collapse navbar-responsive-collapse">
<ul class="nav navbar-nav">
<li><%= link_to "网店首页", "/" %></li>
<li><%= link_to "我的订单", "/path2" %></li>
<li><%= link_to "分类", "/path3" %></li>
<li><%= link_to "我的订单", "/orders" %></li>
</ul>
</div>
<div>

View File

@ -0,0 +1,4 @@
<h1>你好, <%= @user.email %></h1>
<p>
您的订单 <%= @order.number %> 已经确认。
</p>

View File

@ -0,0 +1 @@
你好, <%= @user.email %>,您的订单 <%= @order.number %> 已经确认。

View File

@ -0,0 +1,17 @@
<%= form_for(@order) do |f| %>
<% if @order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>
<ul>
<% @order.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

View File

@ -0,0 +1,6 @@
<h1>Editing Order</h1>
<%= render 'form' %>
<%= link_to 'Show', @order %> |
<%= link_to 'Back', orders_path %>

View File

@ -0,0 +1,32 @@
<div class="page-header">
<h1>我的订单</h1>
</div>
<div class="panel panel-default">
<table class="table">
<thead>
<tr>
<th width="50%">订单</th>
<th width="10%">总额</th>
<th width="10%">状态</th>
<th width="20%">创建时间</th>
<th width="10%"></th>
</tr>
</thead>
<% @orders.each do |order| %>
<tr>
<td><%= link_to order.number, order %></td>
<td><%= number_to_currency order.line_items.map{|i| i.quantity * i.variant_price}.sum %></td>
<td><%= order.payment_state %></td>
<td><%= order.created_at.to_date %></td>
<td><%= link_to "支付", pay_path(number: order.number), method: :post if order.payment_state == "confirm" %></td>
</tr>
<% end %>
</table>
</div>
<div class="well">
<%= page_entries_info @orders %>
</div>
<%= will_paginate @orders, renderer: BootstrapPagination::Rails %>

View File

@ -0,0 +1,4 @@
json.array!(@orders) do |order|
json.extract! order, :id
json.url order_url(order, format: :json)
end

View File

@ -0,0 +1,5 @@
<h1>New Order</h1>
<%= render 'form' %>
<%= link_to 'Back', orders_path %>

View File

@ -0,0 +1,35 @@
<div class="page-header">
<h1>订单:<%= @order.number %></h1>
</div>
<div class="panel panel-default">
<table class="table">
<thead>
<tr>
<th width="50%">商品</th>
<th width="10%">大小</th>
<th class="text-right" width="10%">价格</th>
<th class="text-center" width="10%">数量</th>
<th width="20%" class="text-right">合计</th>
</tr>
</thead>
<% @order.line_items.each do |item| %>
<tr>
<td><%= link_to item.variant.product_name, product_path(item.variant.product_id) %></td>
<td><%= item.variant.size %></td>
<td class="text-right"><%= item.variant_price %></td>
<td class="text-center"><%= item.quantity %></td>
<td class="text-right"><%= number_to_currency item.quantity * item.variant_price %></td>
</tr>
<% end %>
<tfoot>
<tr>
<td class="text-right" colspan="5"><%= number_to_currency @order.line_items.map{|item| item.quantity * item.variant_price}.sum %></td>
</tr>
</tfoot>
</table>
<div class="panel-footer">
<%= link_to "使用 支付宝 支付该订单", pay_path(number: @order.number), method: :post, data: { confirm: "使用支付宝支付" }, class: "btn btn-info btn-block" %>
</div>
</div>

View File

@ -0,0 +1 @@
json.extract! @order, :id, :created_at, :updated_at

View File

@ -11,7 +11,42 @@
<dt><strong><%= model_class.human_attribute_name(:description) %>:</strong></dt>
<dd><%= @product.description %></dd>
</dl>
<%= form_tag buy_product_path(@product) do %>
<div class="row">
<div class="col col-md-8">
<div class="panel panel-default">
<div class="panel-heading product-section-title">选择您想购买的商品</div>
<ul class="list-group">
<% @product.variants.each_with_index do |variant, index| %>
<li class="list-group-item">
<label>
<%= hidden_field_tag "product_id", @product.id %>
<%= radio_button_tag "variant_id", variant.id, index == 0 %>
<%= variant.size %>
<%= number_to_currency variant.price %>
</label>
</li>
<% end%>
</ul>
</div>
</div>
<div class="col col-md-4">
<div class="panel panel-default">
<div class="panel-body input-group">
<%= number_field_tag :quantity, 1, :class => 'form-control', :min => 1, style: "text-align: center;" %>
<span class="input-group-btn">
<%= button_tag :class => 'btn btn-success', :type => :submit do %>
添加到购物车
<% end %>
</span>
</div>
<div class="panel-footer">
<%= link_to "查看购物车", cart_path, :class => 'btn btn-info btn-block' %>
</div>
</div>
</div>
</div>
<% end %>
<%= link_to t('.back', :default => t("helpers.links.back")),
products_path, :class => 'btn btn-default' %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),

View File

@ -8,12 +8,17 @@ Bundler.require(*Rails.groups)
module Shop
class Application < Rails::Application
# Use the responders controller from the responders gem
config.app_generators.scaffold_controller :responders_controller
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**/*.{rb,yml}').to_s]
config.i18n.default_locale = :"zh-CN"
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.active_job.queue_adapter = :sidekiq
end
end

View File

@ -31,7 +31,7 @@ Rails.application.configure do
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_caching = false
@ -60,14 +60,14 @@ Rails.application.configure do
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
# 邮箱配置
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.163.com',
port: 25,
user_name: 'a410928612@163.com',
password: '834752287ZSXzsx',
authentication: :plain,
address: 'smtp.163.com',
port: 25,
user_name: 'a410928612@163.com',
password: 'RHSUARDKLUPZJVJA',
authentication: :plain,
enable_starttls_auto: true
}
config.action_controller.page_cache_directory = "#{Rails.root.to_s}/public"
end

View File

@ -0,0 +1,13 @@
require "sidekiq"
redis_hash = {
namespace: 'sidekiq',
host: "127.0.0.1",
port: 6379
}
Sidekiq.configure_server do |config|
config.redis = redis_hash
end
Sidekiq.configure_client do |config|
config.redis = redis_hash
end

View File

@ -0,0 +1,29 @@
# Sample localization file for English. Add more files in this directory for other locales.
# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
zh-CN:
breadcrumbs:
application:
root: "首页"
pages:
pages: "Pages"
helpers:
actions: "动作"
links:
back: "返回"
cancel: "取消"
confirm: "确认"
destroy: "删除"
new: "添加"
edit: "编辑"
sign_in: 登录
sign_up: 注册
update: 更新
forgot_password: 找回密码
send_password: 发送找回邮件
change_password: 修改密码
titles:
edit: "编辑 %{model}"
save: "保存 %{model}"
new: "新建 %{model}"
delete: "删除 %{model}"

View File

@ -0,0 +1,205 @@
# https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/zh-CN.yml
---
zh-CN:
date:
abbr_day_names:
- 周日
- 周一
- 周二
- 周三
- 周四
- 周五
- 周六
abbr_month_names:
-
- 1月
- 2月
- 3月
- 4月
- 5月
- 6月
- 7月
- 8月
- 9月
- 10月
- 11月
- 12月
day_names:
- 星期日
- 星期一
- 星期二
- 星期三
- 星期四
- 星期五
- 星期六
formats:
default: "%Y-%m-%d"
long: "%Y年%b%d日"
short: "%b%d日"
month_names:
-
- 一月
- 二月
- 三月
- 四月
- 五月
- 六月
- 七月
- 八月
- 九月
- 十月
- 十一月
- 十二月
order:
- :year
- :month
- :day
datetime:
distance_in_words:
about_x_hours:
one: 大约一小时
other: 大约 %{count} 小时
about_x_months:
one: 大约一个月
other: 大约 %{count} 个月
about_x_years:
one: 大约一年
other: 大约 %{count} 年
almost_x_years:
one: 接近一年
other: 接近 %{count} 年
half_a_minute: 半分钟
less_than_x_minutes:
one: 不到一分钟
other: 不到 %{count} 分钟
less_than_x_seconds:
one: 不到一秒
other: 不到 %{count} 秒
over_x_years:
one: 一年多
other: "%{count} 年多"
x_days:
one: 一天
other: "%{count} 天"
x_minutes:
one: 一分钟
other: "%{count} 分钟"
x_months:
one: 一个月
other: "%{count} 个月"
x_seconds:
one: 一秒
other: "%{count} 秒"
prompts:
day:
hour:
minute:
month:
second:
year:
errors:
format: "%{attribute}%{message}"
messages:
accepted: 必须是可被接受的
blank: 不能为空字符
present: 必须是空白
confirmation: 与确认值不匹配
empty: 不能留空
equal_to: 必须等于 %{count}
even: 必须为双数
exclusion: 是保留关键字
greater_than: 必须大于 %{count}
greater_than_or_equal_to: 必须大于或等于 %{count}
inclusion: 不包含于列表中
invalid: 是无效的
less_than: 必须小于 %{count}
less_than_or_equal_to: 必须小于或等于 %{count}
not_a_number: 不是数字
not_an_integer: 必须是整数
odd: 必须为单数
record_invalid: '验证失败: %{errors}'
restrict_dependent_destroy:
one: 由于 %{record} 需要此记录,所以无法移除记录
many: 由于 %{record} 需要此记录,所以无法移除记录
taken: 已经被使用
too_long:
one: 过长(最长为一个字符)
other: 过长(最长为 %{count} 个字符)
too_short:
one: 过短(最短为一个字符)
other: 过短(最短为 %{count} 个字符)
wrong_length:
one: 长度非法(必须为一个字符)
other: 长度非法(必须为 %{count} 个字符)
other_than: 长度非法(不可为 %{count} 个字符
template:
body: 如下字段出现错误:
header:
one: 有 1 个错误发生导致「%{model}」无法被保存。
other: 有 %{count} 个错误发生导致「%{model}」无法被保存。
helpers:
select:
prompt: 请选择
submit:
create: 新增%{model}
submit: 储存%{model}
update: 更新%{model}
number:
currency:
format:
delimiter: ","
format: "%u %n"
precision: 2
separator: "."
significant: false
strip_insignificant_zeros: false
unit: CN¥
format:
delimiter: ","
precision: 3
separator: "."
significant: false
strip_insignificant_zeros: false
human:
decimal_units:
format: "%n %u"
units:
billion: 十亿
million: 百万
quadrillion: 千兆
thousand:
trillion:
unit: ''
format:
delimiter: ''
precision: 1
significant: false
strip_insignificant_zeros: false
storage_units:
format: "%n %u"
units:
byte:
one: Byte
other: Bytes
gb: GB
kb: KB
mb: MB
tb: TB
percentage:
format:
delimiter: ''
precision:
format:
delimiter: ''
support:
array:
last_word_connector: ", 和 "
two_words_connector: " 和 "
words_connector: ", "
time:
am: 上午
formats:
default: "%Y年%b%d日 %A %H:%M:%S"
long: "%Y年%b%d日 %H:%M"
short: "%b%d日 %H:%M"
pm: 下午

View File

@ -0,0 +1,60 @@
# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
en:
devise:
confirmations:
confirmed: "Your email address has been successfully confirmed."
send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
failure:
already_authenticated: "You are already signed in."
inactive: "Your account is not activated yet."
invalid: "Invalid %{authentication_keys} or password."
locked: "Your account is locked."
last_attempt: "You have one more attempt before your account is locked."
not_found_in_database: "Invalid %{authentication_keys} or password."
timeout: "Your session expired. Please sign in again to continue."
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
mailer:
confirmation_instructions:
subject: "Confirmation instructions"
reset_password_instructions:
subject: "Reset password instructions"
unlock_instructions:
subject: "Unlock instructions"
omniauth_callbacks:
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
success: "Successfully authenticated from %{kind} account."
passwords:
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully."
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully."
signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
updated: "Your account has been updated successfully."
sessions:
signed_in: "Signed in successfully."
signed_out: "Signed out successfully."
already_signed_out: "Signed out successfully."
unlocks:
send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
unlocked: "Your account has been unlocked successfully. Please sign in to continue."
errors:
messages:
already_confirmed: "was already confirmed, please try signing in"
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
expired: "has expired, please request a new one"
not_found: "not found"
not_locked: "was not locked"
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"

View File

@ -0,0 +1,61 @@
# Chinese (China) translations for Devise(3.2.2)
# by Kenrick-Zhou (https://github.com/Kenrick-Zhou)
# https://gist.github.com/Kenrick-Zhou/7909822
zh-CN:
devise:
confirmations:
confirmed: "您的帐号已经确认,您现在已登录。"
send_instructions: "几分钟后,您将收到确认帐号的电子邮件。"
send_paranoid_instructions: "如果您的邮箱存在于我们的数据库中,您将收到一封确认账号的邮件。"
failure:
already_authenticated: "您已经登录。"
inactive: "您还没有激活帐户。"
invalid: "邮箱或密码错误。"
locked: "您的帐号已被锁定。"
last_attempt: "您还有最后一次尝试机会,再次失败您的账号将被锁定。"
not_found_in_database: "邮箱或密码错误。"
timeout: "您已登录超时,请重新登录。"
unauthenticated: "继续操作前请注册或者登录。"
unconfirmed: "继续操作前请先确认您的帐号。"
mailer:
confirmation_instructions:
subject: "来自ezcms的注册确认邮件"
reset_password_instructions:
subject: "重置密码信息"
unlock_instructions:
subject: "解锁信息"
omniauth_callbacks:
failure: "因为%{reason},所以您无法从%{kind}获得授权。"
success: "成功地从%{kind}获得授权。"
passwords:
no_token: "这是密码重置页面未重置邮件不得访问此页面。如果您是通过重置邮件而来的请确保您访问的URL是完整的。"
send_instructions: "几分钟后,您将收到重置密码的电子邮件。"
send_paranoid_instructions: "如果您的邮箱存在于我们的数据库中,您将收到一封找回密码的邮件。"
updated: "您的密码已修改成功,您现在已登录。"
updated_not_active: "您的密码已修改成功。"
registrations:
destroyed: "再见!您的帐户已成功注销。我们希望很快可以再见到您。"
signed_up: "欢迎您!您已注册成功。"
signed_up_but_inactive: "您已注册,但尚未激活账号。"
signed_up_but_locked: "您已注册,但账号被锁定了。"
signed_up_but_unconfirmed: "一封带有确认链接的邮件已经发送至您的邮箱,请检查邮箱(包括垃圾邮箱),并点击该链接激活您的账号。"
update_needs_confirmation: "信息更新成功,但我们需要染整您的新电子邮件地址,请检查邮箱(包括垃圾邮箱),并点击该链接激活您的账号。"
updated: "帐号资料更新成功。"
sessions:
signed_in: "登录成功."
signed_out: "退出成功."
unlocks:
send_instructions: "很快,您将收到一封解锁帐号的邮件。"
send_paranoid_instructions: "如果您的邮箱存在于我们的数据库中,您将收到一封解锁账号的邮件。"
unlocked: "您的帐号已成功解锁,您现在已登录。"
errors:
messages:
already_confirmed: "已经确认,请重新登录。"
confirmation_period_expired: "请在%{period}内确认注册, 请重新注册。"
expired: "邮件确认已过期,请重新注册。"
not_found: "没有找到。"
not_locked: "未锁定。"
not_saved:
one: "因为1个错误导致此%{resource}保存失败:"
other: "因为%{count}个错误导致此%{resource}保存失败:"

View File

@ -16,16 +16,6 @@
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# 'true': 'foo'
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.

View File

@ -0,0 +1,7 @@
zh-CN:
activerecord:
models:
order: 订单
attributes:
order:
number: 订单号

View File

@ -0,0 +1,11 @@
zh-CN:
activerecord:
models:
product: 商品
attributes:
product:
name: 名称
description: 描述
price: 价格
created_at: 添加日期
updated_at: 更新日期

View File

@ -0,0 +1,13 @@
zh-CN:
activerecord:
models:
user: 用户
attributes:
user:
email: "邮箱"
name: "用户名"
password: "密码"
password_confirmation: "确认密码"
remember_me: "记住我"
terms: "使用协议"
current_password: "当前密码"

View File

@ -0,0 +1,10 @@
zh-CN:
activerecord:
models:
variant: 商品类型
attributes:
variant:
price: 价格
size: 大小
created_at: 添加日期
updated_at: 更新日期

View File

@ -0,0 +1,12 @@
en:
flash:
actions:
create:
notice: '%{resource_name} was successfully created.'
# alert: '%{resource_name} could not be created.'
update:
notice: '%{resource_name} was successfully updated.'
# alert: '%{resource_name} could not be updated.'
destroy:
notice: '%{resource_name} was successfully destroyed.'
alert: '%{resource_name} could not be destroyed.'

View File

@ -0,0 +1,12 @@
en:
flash:
actions:
create:
notice: '%{resource_name} was successfully created.'
# alert: '%{resource_name} could not be created.'
update:
notice: '%{resource_name} was successfully updated.'
# alert: '%{resource_name} could not be updated.'
destroy:
notice: '%{resource_name} was successfully destroyed.'
alert: '%{resource_name} could not be destroyed.'

View File

@ -0,0 +1,12 @@
zh-CN:
flash:
actions:
create:
notice: '%{resource_name} 创建成功'
alert: '%{resource_name} 创建失败'
update:
notice: '%{resource_name} 更新成功'
alert: '%{resource_name} 更新失败'
destroy:
notice: '%{resource_name} 删除成功'
alert: '%{resource_name} 删除失败'

View File

@ -0,0 +1,17 @@
zh-CN:
will_paginate:
next_label: 下一页
previous_label: '上一页'
page_gap:
page_entries_info:
single_page:
zero: '暂无记录'
one: '1条记录'
other: '共 %{count} 条记录'
single_page_html:
zero: '暂无记录'
one: '1 条记录'
other: '所有共 %{count} 条记录'
multi_page: '第 %{from} 至第 %{to} 条记录(共 %{count} 条记录)'
multi_page_html: '第 %{from} 至第 %{to} 条记录(共 %{count} 条记录)'

2
config/locales/zh-CN.yml Normal file
View File

@ -0,0 +1,2 @@
zh-CN:
hello: "你好"

View File

@ -1,4 +1,7 @@
Rails.application.routes.draw do
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
resources :orders
devise_for :users
get '/',to: 'products#index'
get 'home/:id', to: 'home#welcome', constraints: { id: /[A-Z]\d{5}/ }
@ -15,6 +18,12 @@ Rails.application.routes.draw do
post :buy
end
end
get "/cart", to: 'checkout#cart', as: :cart
post "/checkout", to: 'checkout#checkout', as: :checkout
get "/address", to: 'checkout#address', as: :address
post "/confirm", to: 'checkout#confirm', as: :confirm
post "/:number/pay", to: 'checkout#pay', as: :pay
get "/:number/notify", to: 'checkout#notify', as: :notify
namespace :admin do
resources :products
end

11
config/sidekiq.yml Normal file
View File

@ -0,0 +1,11 @@
# Options here can still be overridden by cmd line args.
# sidekiq -C sidekiq.yml
---
:logfile: ./log/sidekiq.log
:verbose: false
:concurrency: 5
:queues:
- [a, 5]
- [b, 3]
- [c, 2]
- [default, 3]

View File

@ -0,0 +1,11 @@
class CreateCartItems < ActiveRecord::Migration[5.2]
def change
create_table :cart_items do |t|
t.references :user
t.references :variant
t.integer :quantity, default: 0
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,8 @@
class CreateJoinTable < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :variants do |t|
# t.index [:user_id, :variant_id]
t.index [:variant_id, :user_id], unique: true
end
end
end

View File

@ -0,0 +1,10 @@
class CreateCatalogs < ActiveRecord::Migration[5.2]
def change
create_table :catalogs do |t|
t.string :name
t.boolean :parent
t.timestamps
end
end
end

View File

@ -0,0 +1,5 @@
class AddOnHandToVariants < ActiveRecord::Migration[5.2]
def change
add_column :variants, :on_hand, :integer
end
end

View File

@ -0,0 +1,5 @@
class AddRoleToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :role, :string
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_05_18_100755) do
ActiveRecord::Schema.define(version: 2022_05_19_073140) do
create_table "addresses", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.bigint "user_id"
@ -26,6 +26,35 @@ ActiveRecord::Schema.define(version: 2022_05_18_100755) do
t.index ["user_id"], name: "index_addresses_on_user_id"
end
create_table "cart_items", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.bigint "user_id"
t.bigint "variant_id"
t.integer "quantity", default: 0
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_cart_items_on_user_id"
t.index ["variant_id"], name: "index_cart_items_on_variant_id"
end
create_table "catalogs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.string "name"
t.boolean "parent"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "parent_catalog_id"
t.index ["parent_catalog_id"], name: "catalogs_catalogs_id_fk"
end
create_table "line_items", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.bigint "order_id"
t.bigint "variant_id"
t.integer "quantity"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["order_id"], name: "index_line_items_on_order_id"
t.index ["variant_id"], name: "index_line_items_on_variant_id"
end
create_table "orders", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.bigint "user_id"
t.string "number"
@ -59,18 +88,29 @@ ActiveRecord::Schema.define(version: 2022_05_18_100755) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "orders_count"
t.string "role"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
create_table "users_variants", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "variant_id", null: false
t.index ["variant_id", "user_id"], name: "index_users_variants_on_variant_id_and_user_id", unique: true
end
create_table "variants", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb3", force: :cascade do |t|
t.integer "product_id"
t.decimal "price", precision: 8, scale: 2
t.string "size"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "on_hand"
end
add_foreign_key "addresses", "users"
add_foreign_key "catalogs", "catalogs", column: "parent_catalog_id", name: "catalogs_catalogs_id_fk"
add_foreign_key "line_items", "orders"
add_foreign_key "line_items", "variants"
add_foreign_key "orders", "users"
end

View File

@ -0,0 +1,8 @@
class ApplicationResponder < ActionController::Responder
include Responders::FlashResponder
include Responders::HttpCacheResponder
# Redirects resources to the collection path (index action) instead
# of the resource path (show action) for POST/PUT/DELETE requests.
# include Responders::CollectionResponder
end

View File

@ -0,0 +1,136 @@
require 'rails_helper'
RSpec.describe ProductsController, type: :controller do
let(:valid_attributes) {
skip("Add a hash of attributes valid for your model")
}
let(:invalid_attributes) {
skip("Add a hash of attributes invalid for your model")
}
let(:valid_session) { {} }
login_user
describe "GET #index" do
it "assigns all products as @products" do
product = Product.create! valid_attributes
get :index, {}, valid_session
expect(assigns(:products)).to eq([product])
end
end
describe "GET #show" do
it "assigns the requested product as @product" do
product = Product.create! valid_attributes
get :show, {:id => product.to_param}, valid_session
expect(assigns(:product)).to eq(product)
end
end
describe "GET #new" do
it "assigns a new product as @product" do
get :new, {}, valid_session
expect(assigns(:product)).to be_a_new(Product)
end
end
describe "GET #edit" do
it "assigns the requested product as @product" do
product = Product.create! valid_attributes
get :edit, {:id => product.to_param}, valid_session
expect(assigns(:product)).to eq(product)
end
end
describe "POST #create" do
context "with valid params" do
it "creates a new Product" do
expect {
post :create, {:product => valid_attributes}, valid_session
}.to change(Product, :count).by(1)
end
it "assigns a newly created product as @product" do
post :create, {:product => valid_attributes}, valid_session
expect(assigns(:product)).to be_a(Product)
expect(assigns(:product)).to be_persisted
end
it "redirects to the created product" do
post :create, {:product => valid_attributes}, valid_session
expect(response).to redirect_to(Product.last)
end
end
context "with invalid params" do
it "assigns a newly created but unsaved product as @product" do
post :create, {:product => invalid_attributes}, valid_session
expect(assigns(:product)).to be_a_new(Product)
end
it "re-renders the 'new' template" do
post :create, {:product => invalid_attributes}, valid_session
expect(response).to render_template("new")
end
end
end
describe "PUT #update" do
context "with valid params" do
let(:new_attributes) {
skip("Add a hash of attributes valid for your model")
}
it "updates the requested product" do
product = Product.create! valid_attributes
put :update, {:id => product.to_param, :product => new_attributes}, valid_session
product.reload
skip("Add assertions for updated state")
end
it "assigns the requested product as @product" do
product = Product.create! valid_attributes
put :update, {:id => product.to_param, :product => valid_attributes}, valid_session
expect(assigns(:product)).to eq(product)
end
it "redirects to the product" do
product = Product.create! valid_attributes
put :update, {:id => product.to_param, :product => valid_attributes}, valid_session
expect(response).to redirect_to(product)
end
end
context "with invalid params" do
it "assigns the product as @product" do
product = Product.create! valid_attributes
put :update, {:id => product.to_param, :product => invalid_attributes}, valid_session
expect(assigns(:product)).to eq(product)
end
it "re-renders the 'edit' template" do
product = Product.create! valid_attributes
put :update, {:id => product.to_param, :product => invalid_attributes}, valid_session
expect(response).to render_template("edit")
end
end
end
describe "DELETE #destroy" do
it "destroys the requested product" do
product = Product.create! valid_attributes
expect {
delete :destroy, {:id => product.to_param}, valid_session
}.to change(Product, :count).by(-1)
end
it "redirects to the products list" do
product = Product.create! valid_attributes
delete :destroy, {:id => product.to_param}, valid_session
expect(response).to redirect_to(products_url)
end
end
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe OrderCreateJob, type: :job do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -0,0 +1,5 @@
require "rails_helper"
RSpec.describe OrderMailer, type: :mailer do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -0,0 +1,4 @@
# Preview all emails at http://localhost:3000/rails/mailers/order_mailer
class OrderMailerPreview < ActionMailer::Preview
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Catalog, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -1,5 +1,5 @@
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
it { should have_many(:orders) }
end