project packages feature

dev_forum
p31729568 5 years ago
parent cc7b01287a
commit 398cdcd29a

@ -91,3 +91,5 @@ gem 'bulk_insert'
# elasticsearch
gem 'searchkick'
gem 'aasm'

@ -8,6 +8,8 @@ PATH
GEM
remote: https://gems.ruby-china.com/
specs:
aasm (5.0.5)
concurrent-ruby (~> 1.0)
actioncable (5.2.1)
actionpack (= 5.2.1)
nio4r (~> 2.0)
@ -324,6 +326,7 @@ PLATFORMS
ruby
DEPENDENCIES
aasm
active_decorator
acts-as-taggable-on (~> 6.0)
awesome_print

@ -0,0 +1,24 @@
class BiddingUsersController < ApplicationController
before_action :require_login, :check_auth
def create
ProjectPackages::BiddingService.call(current_package, current_user)
render_ok
rescue ProjectPackages::BiddingService::Error => ex
render_error(ex.message)
end
def win
package = current_user.project_packages.find(params[:project_package_id])
ProjectPackages::WinBiddingService.call(package, params)
render_ok
rescue ProjectPackages::WinBiddingService::Error => ex
render_error(ex.message)
end
private
def current_package
@_current_package ||= ProjectPackage.find(params[:project_package_id])
end
end

@ -0,0 +1,6 @@
class ProjectPackageCategoriesController < ApplicationController
def index
categories = ProjectPackageCategory.cached_data
render_ok(count: categories.size, categories: categories)
end
end

@ -0,0 +1,78 @@
class ProjectPackagesController < ApplicationController
include PaginateHelper
before_action :require_login, :check_auth, only: %i[create update destroy]
helper_method :current_package, :package_manageable?
def index
packages = ProjectPackage.where(status: %w(published bidding_ended bidding_finished))
packages = packages.where(project_package_category_id: params[:category_id]) if params[:category_id].present?
keyword = params[:keyword].to_s.strip
packages = packages.where('title LIKE ?', "%#{keyword}%") if keyword.present?
@count = packages.count
direction = params[:sort_direction] == 'asc' ? 'asc' : 'desc'
sort = params[:sort_by] == 'price' ? 'min_price' : 'published_at'
packages = packages.order("#{sort} #{direction}")
@packages = paginate packages.includes(:creator, :attachments, :project_package_category, bidding_users: :user)
end
def show
return render_forbidden unless current_package.visitable? || package_manageable?
current_package.increment!(:visit_count)
end
def create
package = current_user.project_packages.new
ProjectPackages::SaveService.call(package, save_params)
package.increment!(:visit_count)
render_ok(id: package.id)
rescue ProjectPackages::SaveService::Error => ex
render_error(ex.message)
end
def update
package = current_user.project_packages.find(params[:id])
return render_error('该状态下不能编辑') unless package.editable?
ProjectPackages::SaveService.call(package, save_params)
package.increment!(:visit_count)
render_ok(id: package.id)
rescue ProjectPackages::SaveService::Error => ex
render_error(ex.message)
end
def destroy
package = ProjectPackage.find(params[:id])
return render_forbidden unless package.deletable? && package_manageable?
package.destroy!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1, container_id: package.id,
container_type: 'ProjectPackage', tiding_type: 'Destroyed', extra: package.title)
render_ok
end
private
def current_package
@_current_package ||= ProjectPackage.find(params[:id])
end
def package_manageable?
current_user&.id == current_package.creator_id || admin_or_business?
end
def save_params
params.permit(*%i[category_id title content attachment_ids deadline_at min_price max_price
contact_name contact_phone code publish])
end
end

@ -0,0 +1,5 @@
module ProjectPackageDecorator
extend ApplicationDecorator
display_time_method :updated_at, :deadline_at, :published_at
end

@ -0,0 +1,36 @@
class ProjectPackages::AgreeApplyService < ApplicationService
Error = Class.new(StandardError)
attr_reader :apply, :package
def initialize(apply)
@apply = apply
@package = apply.project_package
end
def call
raise Error, '该状态下不能进行此操作' unless apply.may_agree? && package.may_publish?
ActiveRecord::Base.transaction do
apply.agree!
# 发布
package.publish
package.published_at = Time.now
package.save!
# 消息
send_agree_notify!
end
end
private
def send_agree_notify!
Tiding.where(container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0).update_all(status: 1)
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'System', status: 1)
end
end

@ -0,0 +1,31 @@
class ProjectPackages::ApplyPublishService < ApplicationService
Error = Class.new(StandardError)
attr_reader :package
def initialize(package)
@package = package
end
def call
return if package.applying?
raise Error, '该状态下不能申请发布' unless package.may_apply?
ActiveRecord::Base.transaction do
package.apply!
package.project_package_applies.create!
send_project_package_apply_notify!
end
end
private
def send_project_package_apply_notify!
Tiding.create!(user_id: 1, trigger_user_id: package.creator_id,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0)
end
end

@ -0,0 +1,29 @@
class ProjectPackages::BiddingService < ApplicationService
Error = Class.new(StandardError)
attr_reader :package, :user
def initialize(package, user)
@package = package
@user = user
end
def call
raise Error, '竞标已截止' if package.bidding_end?
raise Error, '不能参与自己发布的竞标' if package.creator_id == user.id
raise Error, '您已参与竞标' if package.bidding_users.exists?(user_id: user.id)
ActiveRecord::Base.transaction do
package.bidding_users.create!(user_id: user.id)
send_bidding_notify!
end
end
private
def send_bidding_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: user.id,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'Bidding')
end
end

@ -0,0 +1,26 @@
class ProjectPackages::EndBiddingService < ApplicationService
attr_reader :package
def initialize(package)
@package = package
end
def call
return unless package_deadline?
package.end_bidding!
send_bidding_end_notify!
end
private
def send_bidding_end_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'BiddingEnd')
end
def package_deadline?
package.may_end_bidding? && package.deadline_at < Time.now
end
end

@ -0,0 +1,38 @@
class ProjectPackages::RefuseApplyService < ApplicationService
Error = Class.new(StandardError)
attr_reader :apply, :package, :params
def initialize(apply, params)
@apply = apply
@package = apply.project_package
@params = params
end
def call
raise Error, '该状态下不能进行此操作' unless apply.may_refuse? && package.may_refuse?
ActiveRecord::Base.transaction do
apply.refuse
apply.reason = params[:reason].to_s.strip
apply.save!
# 发布
package.refuse!
# 消息
send_refuse_notify!
end
end
private
def send_refuse_notify!
Tiding.where(container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0).update_all(status: 1)
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'System', status: 2, extra: apply.reason)
end
end

@ -0,0 +1,79 @@
class ProjectPackages::SaveService < ApplicationService
Error = Class.new(StandardError)
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def call
ProjectPackages::SaveForm.new(params).validate!
check_code_valid! if need_check_code?
is_create = package.new_record?
raise Error, '类型不存在' unless ProjectPackageCategory.where(id: params[:category_id]).exists?
params[:project_package_category_id] = params[:category_id].to_i
raise Error, '竞标截止时间不能小于当前时间' if params[:deadline_at].present? && params[:deadline_at].to_time < Time.now
if params[:min_price].blank? && params[:max_price].present?
params[:min_price] = params[:max_price]
params[:max_price] = nil
end
ActiveRecord::Base.transaction do
package.assign_attributes(params)
package.save!
# 处理附件
deal_attachments
send_create_notify! if is_create
ProjectPackages::ApplyPublishService.call(package) if with_publish?
end
package
rescue ProjectPackages::ApplyPublishService::Error => ex
raise Error, ex.message
end
private
def need_check_code?
(package.new_record? && params[:contact_phone] != package.creator.phone) ||
(!package.new_record? && package.contact_phone != params[:contact_phone])
end
def check_code_valid!
raise Error, '验证码不能为空' if params[:code].blank?
code = VerificationCode.where(phone: params[:contact_phone], code_type: 9, code: params[:code]).last
raise Error, '无效的验证码' if code.blank? || !code.valid_code?
end
def deal_attachments
attachment_ids = Array.wrap(params[:attachment_ids]).compact.map(&:to_i) || []
old_attachment_ids = package.attachments.pluck(:id)
destroy_ids = old_attachment_ids - attachment_ids
package.attachments.where(id: destroy_ids).delete_all
new_ids = attachment_ids - old_attachment_ids
if new_ids.present?
Attachment.where(id: new_ids, container_id: nil).update_all(container_id: package.id, container_type: 'ProjectPackage')
end
end
def send_create_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'Created')
end
def with_publish?
params[:publish].to_s == 'true'
end
end

@ -0,0 +1,50 @@
class ProjectPackages::WinBiddingService < ApplicationService
Error = Class.new(StandardError)
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def call
raise Error, '竞标报名还未结束' unless package.bidding_end?
raise Error, '该状态下不能选择中标者' unless package.may_finish_bidding?
win_user_ids = Array.wrap(params[:user_ids]).compact.map(&:to_i)
bidding_user_ids = package.bidding_users.pluck(:user_id)
win_user_ids = bidding_user_ids & win_user_ids
raise Error, '请选择中标者' if win_user_ids.blank?
ActiveRecord::Base.transaction do
package.finish_bidding!
# win bidding users
package.bidding_users.where(user_id: win_user_ids).update_all(status: :bidding_won)
# lose bidding users
lost_user_ids = bidding_user_ids - win_user_ids
package.bidding_users.where(user_id: lost_user_ids).update_all(status: :bidding_lost)
send_bidding_result_notify!('BiddingWon', win_user_ids)
send_bidding_result_notify!('BiddingLost', lost_user_ids)
end
package
end
private
def send_bidding_result_notify!(type, user_ids)
columns = %i[user_id trigger_user_id container_id container_type tiding_type created_at updated_at]
Tiding.bulk_insert(*columns) do |worker|
base_attr = { trigger_user_id: package.creator_id, container_id: package.id,
container_type: 'ProjectPackage', tiding_type: type }
user_ids.each do |user_id|
worker.add(base_attr.merge(user_id: user_id))
end
end
end
end

@ -0,0 +1,15 @@
class ProjectPackages::SaveForm
include ActiveModel::Model
attr_accessor :category_id, :title, :content, :attachment_ids, :deadline_at,
:min_price, :max_price, :contact_name, :contact_phone, :code, :publish
validates :category_id, presence: true
validates :title, presence: true
validates :content, presence: true
validates :deadline_at, presence: true
validates :min_price, numericality: { greater_than: 0 }, allow_blank: true
validates :max_price, numericality: { greater_than: ->(obj){ obj.min_price.to_i } }, allow_blank: true
validates :contact_name, presence: true
validates :contact_phone, presence: true
end

@ -0,0 +1,24 @@
class BiddingUser < ApplicationRecord
include AASM
belongs_to :user
belongs_to :project_package, counter_cache: true
aasm(:status) do
state :pending, initiali: true
state :bidding_won
state :bidding_lost
event :win do
transitions from: [:pending], to: :bid_won
end
event :lose do
transitions from: [:pending], to: :bid_lost
end
end
def status_text
I18n.t("bidding_user.status.#{status}")
end
end

@ -0,0 +1,78 @@
class ProjectPackage < ApplicationRecord
include AASM
belongs_to :creator, class_name: 'User'
belongs_to :project_package_category
has_many :project_package_applies, dependent: :destroy
has_one :process_project_package_apply, -> { where(status: :pending) }, class_name: 'ProjectPackageApply'
has_many :bidding_users, dependent: :delete_all
has_many :win_bidding_users, -> { where(status: :bidding_won) }, class_name: 'BiddingUser'
has_many :lose_bidding_users, -> { where(status: :bidding_lost) }, class_name: 'BiddingUser'
has_many :attachments, as: :container, dependent: :destroy
aasm(:status) do
state :pending, initiali: true
state :applying
state :refused
state :published
state :bidding_ended
state :bidding_finished
event :apply do
transitions from: [:pending, :refused], to: :applying
end
event :refuse do
transitions from: :applying, to: :refused
end
event :publish do
transitions from: :applying, to: :published
end
event :end_bidding do
transitions from: :published, to: :bidding_ended
end
event :finish_bidding do
transitions from: [:bidding_ended], to: :bidding_finished
end
end
def category_name
project_package_category.name
end
def visitable?
!editable?
end
def editable?
pending? || applying? || refused?
end
def deletable?
pending? || refused?
end
def deadline?
deadline_at < Time.now
end
def bidding_end?
flag = deadline?
end_bidding! if flag && may_end_bidding?
flag
end
def can_bidding?(user)
published? && !bidding_end? && user.id != creator_id && !bidding_users.exists?(user_id: user.id)
end
def status_text
I18n.t("project_package.status.#{status}")
end
end

@ -0,0 +1,19 @@
class ProjectPackageApply < ApplicationRecord
include AASM
belongs_to :project_package
aasm(:status) do
state :pending, initiali: true
state :refused
state :agreed
event :refuse do
transitions from: :pending, to: :refused
end
event :agree do
transitions from: :pending, to: :agreed
end
end
end

@ -0,0 +1,23 @@
class ProjectPackageCategory < ApplicationRecord
default_scope { order(position: :asc) }
has_many :project_packages, dependent: :destroy
after_commit :reset_cache_data
def self.cached_data
Rails.cache.fetch(data_cache_key, expires_in: 1.days) do
ProjectPackageCategory.select(:id, :name).as_json
end
end
def self.data_cache_key
'project_package_category/cached_data'
end
private
def reset_cache_data
Rails.cache.delete(self.class.data_cache_key)
end
end

@ -0,0 +1,79 @@
class ProjectPackages::SaveService
Error = Class.new(StandardError)
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def call
ProjectPackages::SaveForm.new(params).validate!
check_code_valid! if need_check_code?
is_create = package.new_record?
raise Error, '类型不存在' unless ProjectPackageCategory.where(id: params[:category_id]).exists?
params[:project_package_category_id] = params[:category_id].to_i
raise Error, '竞标截止时间不能小于当前时间' if params[:deadline_at].present? && params[:deadline_at].to_time < Time.now
if params[:min_price].blank? && params[:max_price].present?
params[:min_price] = params[:max_price]
params[:max_price] = nil
end
ActiveRecord::Base.transaction do
package.assign_attributes(params)
package.save!
# 处理附件
deal_attachments
send_create_notify! if is_create
ProjectPackages::ApplyPublishService.call(package) if with_publish?
end
package
rescue ProjectPackages::ApplyPublishService::Error => ex
raise Error, ex.message
end
private
def need_check_code?
(package.new_record? && params[:contact_phone] != package.creator.phone) ||
(!package.new_record? && package.contact_phone != params[:contact_phone])
end
def check_code_valid!
raise Error, '验证码不能为空' if params[:code].blank?
code = VerificationCode.where(phone: params[:contact_phone], code_type: 9, code: params[:code]).last
raise Error, '无效的验证码' if code.blank? || !code.valid_code?
end
def deal_attachments
attachment_ids = Array.wrap(params[:attachment_ids]).compact.map(&:to_i) || []
old_attachment_ids = package.attachments.pluck(:id)
destroy_ids = old_attachment_ids - attachment_ids
package.attachments.where(id: destroy_ids).delete_all
new_ids = attachment_ids - old_attachment_ids
if new_ids.present?
Attachment.where(id: new_ids, container_id: nil).update_all(container_id: package.id, container_type: 'ProjectPackage')
end
end
def send_create_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'Created')
end
def with_publish?
params[:publish].to_s == 'true'
end
end

@ -0,0 +1,13 @@
json.count @count
json.project_packages do
json.array! @packages.each do |package|
json.extract! package, :id, :title, :content, :category_name, :status,
:visit_count, :bidding_users_count, :min_price, :max_price
json.category_id package.project_package_category_id
json.updated_at package.display_updated_at
json.deadline_at package.display_deadline_at
json.published_at package.display_published_at
end
end

@ -0,0 +1,43 @@
package = current_package
json.extract! package, :id, :title, :content, :category_name, :status,
:visit_count, :bidding_users_count, :min_price, :max_price
json.category_id package.project_package_category_id
# 只有自己和管理员才返回私人信息
if package_manageable?
json.contact_name package.contact_name
json.contact_phone package.contact_phone
end
json.updated_at package.display_updated_at
json.deadline_at package.display_deadline_at
json.published_at package.display_published_at
json.creator do
json.partial! 'users/user_simple', user: package.creator
end
json.attachments do
json.array! package.attachments, partial: 'attachments/attachment_simple', as: :attachment
end
json.bidding_users do
json.array! package.bidding_users.includes(:user).each do |bidding_user|
json.partial! 'users/user_simple', user: bidding_user.user
json.status bidding_user.status
end
end
json.operation do
if current_user
manageable = package_manageable?
json.can_bidding package.can_bidding?(current_user)
json.can_select_bidding_user package.bidding_end? && package.bidding_ended? && manageable
json.can_edit package.editable? && manageable
json.can_delete package.deletable? && manageable
end
end

@ -0,0 +1,6 @@
'zh-CN':
bidding_user:
status:
pending: 竞标中
bidding_won: 已中标
bidding_lost: 未中标

@ -0,0 +1,13 @@
'zh-CN':
activemodel:
attributes:
project_packages/save_form:
category_id: 类型
title: 标题
content: 描述
deadline_at: 截止日期
min_price: 最小价格
max_price: 最大价格
contact_name: 联系人姓名
contact_phone: 联系人电话
code: 验证码

@ -0,0 +1,9 @@
zh-CN:
project_package:
status:
pending: 已创建
applying: 审核中
refused: 已拒绝
published: 竞标中
bidding_ended: 待选标
bidding_finished: 已完成

@ -682,6 +682,13 @@ Rails.application.routes.draw do
resources :students, only: [:index]
end
end
resources :project_package_categories, only: [:index]
resources :project_packages, only: [:index, :show, :create, :update, :destroy] do
resources :bidding_users, only: [:create] do
post :win, on: :collection
end
end
end
#git 认证回调

Loading…
Cancel
Save