From 8ed7588b4128eb1a186f768b559cc0fff540f3b7 Mon Sep 17 00:00:00 2001 From: zhoushixu <410928612@qq.com> Date: Thu, 19 May 2022 18:56:45 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9C=8B=E5=AE=8Crails-practice=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=AE=9E=E4=BE=8B=E5=92=8C=E5=86=85=E5=AE=B9=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9C=A8=E8=AE=A2=E5=8D=95=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=B9=8B=E5=90=8E=E5=BC=82=E6=AD=A5=E9=98=9F=E5=88=97=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=82=AE=E4=BB=B6=EF=BC=8C=E5=AE=8C=E6=88=90=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E8=B4=AD=E7=89=A9=E8=BD=A6=E5=92=8C=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/modules.xml | 1 - .idea/shop.iml | 22 +- Gemfile | 24 +- Gemfile.lock | 55 +++++ app/assets/javascripts/products.coffee | 2 + app/controllers/application_controller.rb | 6 + app/controllers/checkout_controller.rb | 78 +++++++ app/controllers/orders_controller.rb | 46 ++++ app/controllers/products_controller.rb | 7 +- app/jobs/order_create_job.rb | 7 + app/mailers/order_mailer.rb | 7 + app/models/ability.rb | 40 ++++ app/models/address.rb | 2 +- app/models/cart_item.rb | 4 + app/models/catalog.rb | 5 + app/models/line_item.rb | 2 + app/models/order.rb | 19 +- app/models/person.rb | 28 +++ app/models/product.rb | 37 ++++ app/models/user.rb | 4 +- app/models/variant.rb | 4 + app/views/checkout/address.html.erb | 53 +++++ app/views/checkout/cart.html.erb | 44 ++++ app/views/layouts/application.html.erb | 3 +- app/views/order_mailer/confirm_email.html.erb | 4 + app/views/order_mailer/confirm_email.text.erb | 1 + app/views/orders/_form.html.erb | 17 ++ app/views/orders/edit.html.erb | 6 + app/views/orders/index.html.erb | 32 +++ app/views/orders/index.json.jbuilder | 4 + app/views/orders/new.html.erb | 5 + app/views/orders/show.html.erb | 35 +++ app/views/orders/show.json.jbuilder | 1 + app/views/products/show.html.erb | 37 +++- config/application.rb | 7 +- config/environments/development.rb | 14 +- config/initializers/sidekiq.rb | 13 ++ config/locales/bootstrap/zh-CN.yml | 29 +++ config/locales/defaults/zh-CN.yml | 205 ++++++++++++++++++ config/locales/devise/en.yml | 60 +++++ config/locales/devise/zh-CN.yml | 61 ++++++ config/locales/en.yml | 10 - config/locales/models/order/zh-CN.yml | 7 + config/locales/models/product/zh-CN.yml | 11 + config/locales/models/user/zh-CN.yml | 13 ++ config/locales/models/variant/zh-CN.yml | 10 + config/locales/responders.en.yml | 12 + config/locales/responders/en.yml | 12 + config/locales/responders/zh-CN.yml | 12 + config/locales/will_paginate/zh-CN.yml | 17 ++ config/locales/zh-CN.yml | 2 + config/routes.rb | 9 + config/sidekiq.yml | 11 + .../20150628073804_create_cart_items.rb | 11 + .../20220519020037_create_join_table.rb | 8 + db/migrate/20220519021031_create_catalogs.rb | 10 + .../20220519054151_add_on_hand_to_variants.rb | 5 + .../20220519073140_add_role_to_users.rb | 5 + db/schema.rb | 42 +++- lib/application_responder.rb | 8 + spec/controllers/product_controller_spec.rb | 136 ++++++++++++ spec/jobs/order_create_job_spec.rb | 5 + spec/mailers/order_mailer_spec.rb | 5 + spec/mailers/previews/order_mailer_preview.rb | 4 + spec/models/catalog_spec.rb | 5 + spec/models/user_spec.rb | 2 +- 66 files changed, 1371 insertions(+), 32 deletions(-) create mode 100644 app/controllers/checkout_controller.rb create mode 100644 app/controllers/orders_controller.rb create mode 100644 app/jobs/order_create_job.rb create mode 100644 app/mailers/order_mailer.rb create mode 100644 app/models/ability.rb create mode 100644 app/models/cart_item.rb create mode 100644 app/models/catalog.rb create mode 100644 app/models/person.rb create mode 100644 app/views/checkout/address.html.erb create mode 100644 app/views/checkout/cart.html.erb create mode 100644 app/views/order_mailer/confirm_email.html.erb create mode 100644 app/views/order_mailer/confirm_email.text.erb create mode 100644 app/views/orders/_form.html.erb create mode 100644 app/views/orders/edit.html.erb create mode 100644 app/views/orders/index.html.erb create mode 100644 app/views/orders/index.json.jbuilder create mode 100644 app/views/orders/new.html.erb create mode 100644 app/views/orders/show.html.erb create mode 100644 app/views/orders/show.json.jbuilder create mode 100644 config/initializers/sidekiq.rb create mode 100644 config/locales/bootstrap/zh-CN.yml create mode 100644 config/locales/defaults/zh-CN.yml create mode 100644 config/locales/devise/en.yml create mode 100644 config/locales/devise/zh-CN.yml create mode 100644 config/locales/models/order/zh-CN.yml create mode 100644 config/locales/models/product/zh-CN.yml create mode 100644 config/locales/models/user/zh-CN.yml create mode 100644 config/locales/models/variant/zh-CN.yml create mode 100644 config/locales/responders.en.yml create mode 100644 config/locales/responders/en.yml create mode 100644 config/locales/responders/zh-CN.yml create mode 100644 config/locales/will_paginate/zh-CN.yml create mode 100644 config/locales/zh-CN.yml create mode 100644 config/sidekiq.yml create mode 100644 db/migrate/20150628073804_create_cart_items.rb create mode 100644 db/migrate/20220519020037_create_join_table.rb create mode 100644 db/migrate/20220519021031_create_catalogs.rb create mode 100644 db/migrate/20220519054151_add_on_hand_to_variants.rb create mode 100644 db/migrate/20220519073140_add_role_to_users.rb create mode 100644 lib/application_responder.rb create mode 100644 spec/controllers/product_controller_spec.rb create mode 100644 spec/jobs/order_create_job_spec.rb create mode 100644 spec/mailers/order_mailer_spec.rb create mode 100644 spec/mailers/previews/order_mailer_preview.rb create mode 100644 spec/models/catalog_spec.rb diff --git a/.idea/modules.xml b/.idea/modules.xml index 3f7001d..8a364ce 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,7 +3,6 @@ - \ No newline at end of file diff --git a/.idea/shop.iml b/.idea/shop.iml index 6be2feb..6d20634 100644 --- a/.idea/shop.iml +++ b/.idea/shop.iml @@ -31,6 +31,8 @@ + + @@ -46,6 +48,7 @@ + @@ -54,6 +57,7 @@ + @@ -63,12 +67,12 @@ - - + + @@ -92,6 +96,7 @@ + @@ -99,6 +104,7 @@ + @@ -108,6 +114,13 @@ + + + + + + + @@ -116,6 +129,7 @@ + @@ -124,6 +138,8 @@ + + @@ -144,6 +160,8 @@ + + diff --git a/Gemfile b/Gemfile index 51fb0f9..55caae7 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 8e72246..11ecea5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/javascripts/products.coffee b/app/assets/javascripts/products.coffee index 950cb46..98ddc66 100644 --- a/app/assets/javascripts/products.coffee +++ b/app/assets/javascripts/products.coffee @@ -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") diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 27f8e08..c98f9e8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb new file mode 100644 index 0000000..18e8092 --- /dev/null +++ b/app/controllers/checkout_controller.rb @@ -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 diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb new file mode 100644 index 0000000..159cef6 --- /dev/null +++ b/app/controllers/orders_controller.rb @@ -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 diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index c1f70d6..6eb9508 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -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." } diff --git a/app/jobs/order_create_job.rb b/app/jobs/order_create_job.rb new file mode 100644 index 0000000..ed10462 --- /dev/null +++ b/app/jobs/order_create_job.rb @@ -0,0 +1,7 @@ +class OrderCreateJob < ApplicationJob + queue_as :default + + def perform(order) + OrderMailer.confirm_email(order).deliver_now + end +end diff --git a/app/mailers/order_mailer.rb b/app/mailers/order_mailer.rb new file mode 100644 index 0000000..79c358b --- /dev/null +++ b/app/mailers/order_mailer.rb @@ -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 diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..ed77436 --- /dev/null +++ b/app/models/ability.rb @@ -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 diff --git a/app/models/address.rb b/app/models/address.rb index 89b92b4..d2dac0a 100644 --- a/app/models/address.rb +++ b/app/models/address.rb @@ -1,3 +1,3 @@ class Address < ApplicationRecord - belongs_to :user + belongs_to :user,inverse_of: :address end diff --git a/app/models/cart_item.rb b/app/models/cart_item.rb new file mode 100644 index 0000000..c4e7e89 --- /dev/null +++ b/app/models/cart_item.rb @@ -0,0 +1,4 @@ +class CartItem < ActiveRecord::Base + belongs_to :user + belongs_to :variant +end diff --git a/app/models/catalog.rb b/app/models/catalog.rb new file mode 100644 index 0000000..46c23a2 --- /dev/null +++ b/app/models/catalog.rb @@ -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 diff --git a/app/models/line_item.rb b/app/models/line_item.rb index b52b1cb..2c56cf5 100644 --- a/app/models/line_item.rb +++ b/app/models/line_item.rb @@ -1,4 +1,6 @@ class LineItem < ApplicationRecord belongs_to :order belongs_to :variant + belongs_to :user + delegate :price, to: :variant, prefix: true end diff --git a/app/models/order.rb b/app/models/order.rb index 4f547be..59aa5c9 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -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 diff --git a/app/models/person.rb b/app/models/person.rb new file mode 100644 index 0000000..e65c47b --- /dev/null +++ b/app/models/person.rb @@ -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 \ No newline at end of file diff --git a/app/models/product.rb b/app/models/product.rb index 86060c7..eb3c548 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 1aa58f3..51de1e7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/variant.rb b/app/models/variant.rb index 965665f..f67ae71 100644 --- a/app/models/variant.rb +++ b/app/models/variant.rb @@ -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 diff --git a/app/views/checkout/address.html.erb b/app/views/checkout/address.html.erb new file mode 100644 index 0000000..e0dd8d3 --- /dev/null +++ b/app/views/checkout/address.html.erb @@ -0,0 +1,53 @@ +<%- model_class = Product -%> + + +<%= form_for @address, url: confirm_path do |f| %> +
+
+
+ <%= f.label :state, class: "control-label" %>
+ <%= f.text_field :state, class: "form-control" %> +
+ +
+ <%= f.label :city, class: "control-label" %>
+ <%= f.text_field :city, class: "form-control" %> +
+ +
+ <%= f.label :address, class: "control-label" %>
+ <%= f.text_field :address, class: "form-control" %> +
+ +
+ <%= f.label :address2, class: "control-label" %>
+ <%= f.text_field :address2, class: "form-control" %> +
+ +
+ <%= f.label :zipcode, class: "control-label" %>
+ <%= f.text_field :zipcode, class: "form-control" %> +
+ +
+ <%= f.label :receiver, class: "control-label" %>
+ <%= f.text_field :receiver, class: "form-control" %> +
+ +
+ <%= f.label :phone, class: "control-label" %>
+ <%= f.text_field :phone, class: "form-control" %> +
+
+
+
+ +
+
+
+<% end %> + diff --git a/app/views/checkout/cart.html.erb b/app/views/checkout/cart.html.erb new file mode 100644 index 0000000..ea76c87 --- /dev/null +++ b/app/views/checkout/cart.html.erb @@ -0,0 +1,44 @@ +<%- model_class = Product -%> + + +<% if @cart_items.empty? %> + 购物车为空 +<% else %> + <%= form_tag checkout_path do %> +
+
+
+ + + + + + + + + + + <% @cart_items.each do |item| %> + + + + + + + + <% end %> +
商品大小价格数量合计
<%= item.variant.product.name %><%= item.variant.size %><%= item.variant.price %><%= text_field_tag "item[#{item.id}][quantity]", item.quantity %><%= number_to_currency item.quantity * item.variant.price %>
+
+
+
+
+ +
+
+
+ <% end %> +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 373bbae..e4b7241 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -50,8 +50,7 @@
diff --git a/app/views/order_mailer/confirm_email.html.erb b/app/views/order_mailer/confirm_email.html.erb new file mode 100644 index 0000000..9d15115 --- /dev/null +++ b/app/views/order_mailer/confirm_email.html.erb @@ -0,0 +1,4 @@ +

你好, <%= @user.email %>

+

+ 您的订单 <%= @order.number %> 已经确认。 +

diff --git a/app/views/order_mailer/confirm_email.text.erb b/app/views/order_mailer/confirm_email.text.erb new file mode 100644 index 0000000..6066ee7 --- /dev/null +++ b/app/views/order_mailer/confirm_email.text.erb @@ -0,0 +1 @@ +你好, <%= @user.email %>,您的订单 <%= @order.number %> 已经确认。 diff --git a/app/views/orders/_form.html.erb b/app/views/orders/_form.html.erb new file mode 100644 index 0000000..15cdbdb --- /dev/null +++ b/app/views/orders/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_for(@order) do |f| %> + <% if @order.errors.any? %> +
+

<%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:

+ +
    + <% @order.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= f.submit %> +
+<% end %> diff --git a/app/views/orders/edit.html.erb b/app/views/orders/edit.html.erb new file mode 100644 index 0000000..c621bd2 --- /dev/null +++ b/app/views/orders/edit.html.erb @@ -0,0 +1,6 @@ +

Editing Order

+ +<%= render 'form' %> + +<%= link_to 'Show', @order %> | +<%= link_to 'Back', orders_path %> diff --git a/app/views/orders/index.html.erb b/app/views/orders/index.html.erb new file mode 100644 index 0000000..ba467dd --- /dev/null +++ b/app/views/orders/index.html.erb @@ -0,0 +1,32 @@ + + +
+ + + + + + + + + + + <% @orders.each do |order| %> + + + + + + + + <% end %> +
订单总额状态创建时间
<%= link_to order.number, order %><%= number_to_currency order.line_items.map{|i| i.quantity * i.variant_price}.sum %><%= order.payment_state %><%= order.created_at.to_date %><%= link_to "支付", pay_path(number: order.number), method: :post if order.payment_state == "confirm" %>
+
+ +
+ <%= page_entries_info @orders %> +
+<%= will_paginate @orders, renderer: BootstrapPagination::Rails %> + diff --git a/app/views/orders/index.json.jbuilder b/app/views/orders/index.json.jbuilder new file mode 100644 index 0000000..217c653 --- /dev/null +++ b/app/views/orders/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@orders) do |order| + json.extract! order, :id + json.url order_url(order, format: :json) +end diff --git a/app/views/orders/new.html.erb b/app/views/orders/new.html.erb new file mode 100644 index 0000000..a7214a0 --- /dev/null +++ b/app/views/orders/new.html.erb @@ -0,0 +1,5 @@ +

New Order

+ +<%= render 'form' %> + +<%= link_to 'Back', orders_path %> diff --git a/app/views/orders/show.html.erb b/app/views/orders/show.html.erb new file mode 100644 index 0000000..09400d2 --- /dev/null +++ b/app/views/orders/show.html.erb @@ -0,0 +1,35 @@ + + +
+ + + + + + + + + + + <% @order.line_items.each do |item| %> + + + + + + + + <% end %> + + + + + +
商品大小价格数量合计
<%= link_to item.variant.product_name, product_path(item.variant.product_id) %><%= item.variant.size %><%= item.variant_price %><%= item.quantity %><%= number_to_currency item.quantity * item.variant_price %>
<%= number_to_currency @order.line_items.map{|item| item.quantity * item.variant_price}.sum %>
+ +
+ diff --git a/app/views/orders/show.json.jbuilder b/app/views/orders/show.json.jbuilder new file mode 100644 index 0000000..e7be84b --- /dev/null +++ b/app/views/orders/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @order, :id, :created_at, :updated_at diff --git a/app/views/products/show.html.erb b/app/views/products/show.html.erb index f16a482..276d411 100644 --- a/app/views/products/show.html.erb +++ b/app/views/products/show.html.erb @@ -11,7 +11,42 @@
<%= model_class.human_attribute_name(:description) %>:
<%= @product.description %>
- +<%= form_tag buy_product_path(@product) do %> +
+
+
+
选择您想购买的商品
+
    + <% @product.variants.each_with_index do |variant, index| %> +
  • + +
  • + <% end%> +
+
+
+
+
+
+ <%= number_field_tag :quantity, 1, :class => 'form-control', :min => 1, style: "text-align: center;" %> + + <%= button_tag :class => 'btn btn-success', :type => :submit do %> + 添加到购物车 + <% end %> + +
+ +
+
+
+<% 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")), diff --git a/config/application.rb b/config/application.rb index c2c7b00..8e11054 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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 diff --git a/config/environments/development.rb b/config/environments/development.rb index 2ef037a..87cc745 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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 diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..566a896 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -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 diff --git a/config/locales/bootstrap/zh-CN.yml b/config/locales/bootstrap/zh-CN.yml new file mode 100644 index 0000000..750b7d1 --- /dev/null +++ b/config/locales/bootstrap/zh-CN.yml @@ -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}" diff --git a/config/locales/defaults/zh-CN.yml b/config/locales/defaults/zh-CN.yml new file mode 100644 index 0000000..3d4f9d6 --- /dev/null +++ b/config/locales/defaults/zh-CN.yml @@ -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: 下午 diff --git a/config/locales/devise/en.yml b/config/locales/devise/en.yml new file mode 100644 index 0000000..26a10f2 --- /dev/null +++ b/config/locales/devise/en.yml @@ -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:" diff --git a/config/locales/devise/zh-CN.yml b/config/locales/devise/zh-CN.yml new file mode 100644 index 0000000..166c4d2 --- /dev/null +++ b/config/locales/devise/zh-CN.yml @@ -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}保存失败:" diff --git a/config/locales/en.yml b/config/locales/en.yml index decc5a8..0653957 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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. diff --git a/config/locales/models/order/zh-CN.yml b/config/locales/models/order/zh-CN.yml new file mode 100644 index 0000000..373fe2a --- /dev/null +++ b/config/locales/models/order/zh-CN.yml @@ -0,0 +1,7 @@ +zh-CN: + activerecord: + models: + order: 订单 + attributes: + order: + number: 订单号 diff --git a/config/locales/models/product/zh-CN.yml b/config/locales/models/product/zh-CN.yml new file mode 100644 index 0000000..cc54b8a --- /dev/null +++ b/config/locales/models/product/zh-CN.yml @@ -0,0 +1,11 @@ +zh-CN: + activerecord: + models: + product: 商品 + attributes: + product: + name: 名称 + description: 描述 + price: 价格 + created_at: 添加日期 + updated_at: 更新日期 diff --git a/config/locales/models/user/zh-CN.yml b/config/locales/models/user/zh-CN.yml new file mode 100644 index 0000000..cb0bca2 --- /dev/null +++ b/config/locales/models/user/zh-CN.yml @@ -0,0 +1,13 @@ +zh-CN: + activerecord: + models: + user: 用户 + attributes: + user: + email: "邮箱" + name: "用户名" + password: "密码" + password_confirmation: "确认密码" + remember_me: "记住我" + terms: "使用协议" + current_password: "当前密码" diff --git a/config/locales/models/variant/zh-CN.yml b/config/locales/models/variant/zh-CN.yml new file mode 100644 index 0000000..75a7623 --- /dev/null +++ b/config/locales/models/variant/zh-CN.yml @@ -0,0 +1,10 @@ +zh-CN: + activerecord: + models: + variant: 商品类型 + attributes: + variant: + price: 价格 + size: 大小 + created_at: 添加日期 + updated_at: 更新日期 diff --git a/config/locales/responders.en.yml b/config/locales/responders.en.yml new file mode 100644 index 0000000..c3e147a --- /dev/null +++ b/config/locales/responders.en.yml @@ -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.' diff --git a/config/locales/responders/en.yml b/config/locales/responders/en.yml new file mode 100644 index 0000000..c3e147a --- /dev/null +++ b/config/locales/responders/en.yml @@ -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.' diff --git a/config/locales/responders/zh-CN.yml b/config/locales/responders/zh-CN.yml new file mode 100644 index 0000000..6b3004f --- /dev/null +++ b/config/locales/responders/zh-CN.yml @@ -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} 删除失败' diff --git a/config/locales/will_paginate/zh-CN.yml b/config/locales/will_paginate/zh-CN.yml new file mode 100644 index 0000000..22c4964 --- /dev/null +++ b/config/locales/will_paginate/zh-CN.yml @@ -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} 条记录)' diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml new file mode 100644 index 0000000..7750d07 --- /dev/null +++ b/config/locales/zh-CN.yml @@ -0,0 +1,2 @@ +zh-CN: + hello: "你好" diff --git a/config/routes.rb b/config/routes.rb index 1fe2f04..238883d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 0000000..01c6c27 --- /dev/null +++ b/config/sidekiq.yml @@ -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] diff --git a/db/migrate/20150628073804_create_cart_items.rb b/db/migrate/20150628073804_create_cart_items.rb new file mode 100644 index 0000000..a486f5b --- /dev/null +++ b/db/migrate/20150628073804_create_cart_items.rb @@ -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 diff --git a/db/migrate/20220519020037_create_join_table.rb b/db/migrate/20220519020037_create_join_table.rb new file mode 100644 index 0000000..1a92bb6 --- /dev/null +++ b/db/migrate/20220519020037_create_join_table.rb @@ -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 diff --git a/db/migrate/20220519021031_create_catalogs.rb b/db/migrate/20220519021031_create_catalogs.rb new file mode 100644 index 0000000..a79f0ca --- /dev/null +++ b/db/migrate/20220519021031_create_catalogs.rb @@ -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 diff --git a/db/migrate/20220519054151_add_on_hand_to_variants.rb b/db/migrate/20220519054151_add_on_hand_to_variants.rb new file mode 100644 index 0000000..1a12260 --- /dev/null +++ b/db/migrate/20220519054151_add_on_hand_to_variants.rb @@ -0,0 +1,5 @@ +class AddOnHandToVariants < ActiveRecord::Migration[5.2] + def change + add_column :variants, :on_hand, :integer + end +end diff --git a/db/migrate/20220519073140_add_role_to_users.rb b/db/migrate/20220519073140_add_role_to_users.rb new file mode 100644 index 0000000..567ae13 --- /dev/null +++ b/db/migrate/20220519073140_add_role_to_users.rb @@ -0,0 +1,5 @@ +class AddRoleToUsers < ActiveRecord::Migration[5.2] + def change + add_column :users, :role, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 77bffa1..6f52f2f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/lib/application_responder.rb b/lib/application_responder.rb new file mode 100644 index 0000000..cc3e588 --- /dev/null +++ b/lib/application_responder.rb @@ -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 diff --git a/spec/controllers/product_controller_spec.rb b/spec/controllers/product_controller_spec.rb new file mode 100644 index 0000000..1cdd16f --- /dev/null +++ b/spec/controllers/product_controller_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/jobs/order_create_job_spec.rb b/spec/jobs/order_create_job_spec.rb new file mode 100644 index 0000000..b18afdb --- /dev/null +++ b/spec/jobs/order_create_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe OrderCreateJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/order_mailer_spec.rb b/spec/mailers/order_mailer_spec.rb new file mode 100644 index 0000000..47e2230 --- /dev/null +++ b/spec/mailers/order_mailer_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe OrderMailer, type: :mailer do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/previews/order_mailer_preview.rb b/spec/mailers/previews/order_mailer_preview.rb new file mode 100644 index 0000000..7be457e --- /dev/null +++ b/spec/mailers/previews/order_mailer_preview.rb @@ -0,0 +1,4 @@ +# Preview all emails at http://localhost:3000/rails/mailers/order_mailer +class OrderMailerPreview < ActionMailer::Preview + +end diff --git a/spec/models/catalog_spec.rb b/spec/models/catalog_spec.rb new file mode 100644 index 0000000..09e23f5 --- /dev/null +++ b/spec/models/catalog_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Catalog, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 47a31bb..bdab0a8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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