diff --git a/app/api/mobile/api.rb b/app/api/mobile/api.rb
index 894ba099..8d01bb5b 100644
--- a/app/api/mobile/api.rb
+++ b/app/api/mobile/api.rb
@@ -166,6 +166,21 @@ module Mobile
return user if user
nil
end
+
+ def paginate(objs)
+ page = params[:page].to_i <= 0 ? 1 : params[:page].to_i
+ per_page = params[:per_page].to_i > 0 ? params[:per_page].to_i : 20
+
+ Kaminari.paginate_array(objs).page(page).per(per_page)
+ end
+
+ def render_ok(data = {})
+ { status: 0, message: 'success' }.merge(data)
+ end
+
+ def render_error(message)
+ { status: -1, message: message }
+ end
end
mount Apis::Auth
@@ -197,7 +212,7 @@ module Mobile
mount Apis::Ecloud
mount Apis::Cnmooc
-
+ mount Apis::ProjectPackages
# add_swagger_documentation ({host: 'educoder.0bug.info', api_version: 'v1', base_path: '/api'}) if Rails.env.development?
add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development?
diff --git a/app/api/mobile/apis/project_packages.rb b/app/api/mobile/apis/project_packages.rb
new file mode 100644
index 00000000..e619ce74
--- /dev/null
+++ b/app/api/mobile/apis/project_packages.rb
@@ -0,0 +1,153 @@
+# encoding=utf-8
+
+module Mobile
+ module Apis
+ class ProjectPackages < Grape::API
+ use Mobile::Middleware::NotFoundHandler
+
+ helpers do
+ def current_package
+ @_current_package ||= ProjectPackage.find(params[:id])
+ end
+
+ def symbolize_params
+ params.to_hash.symbolize_keys
+ end
+ end
+
+ resources :project_packages do
+ desc 'project packages index'
+ params do
+ optional :category, type: String, desc: '类型'
+ optional :keyword, type: String, desc: '搜索关键字'
+ optional :sort_by, type: String, desc: '排序'
+ optional :sort_direction, type: String, desc: '排序方向'
+ optional :page, type: Integer, desc: '页数'
+ optional :per_page, type: Integer, desc: '分页大小'
+ end
+ get do
+ packages = ProjectPackage.where(status: %w(published bidding_ended bidding_finished))
+
+ packages = packages.where(category: params[:category]) if params[:category].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, bidding_users: :user)
+
+ present :count, count
+ present :project_packages, packages, with: Mobile::Entities::ProjectPackage, type: :index, user: current_user
+ end
+
+
+ desc 'project package show'
+ params do
+ requires :id, type: Integer, desc: 'ID'
+ end
+ route_param :id do
+ get do
+ authenticate!
+ user = current_user
+
+ unless current_package.visitable? || user.id == current_package.creator_id || user.admin?
+ error!('403 forbidden', 403)
+ end
+
+ present current_package, with: Mobile::Entities::ProjectPackage, type: :show, user: user
+ end
+
+ resource :bidding_users do
+ desc 'user bidding'
+ params {}
+ post do
+ authenticate!
+ begin
+ ::ProjectPackages::BiddingService.new(current_package, current_user).call
+ render_ok
+ rescue ::ProjectPackages::BiddingService::Error => ex
+ render_error(ex.message)
+ end
+ end
+
+ desc 'select bidding user win'
+ params do
+ requires :user_ids, type: Array[Integer], desc: '中标者用户ID数组'
+ end
+ post 'win' do
+ authenticate!
+ begin
+ package = current_user.project_packages.find(params[:id])
+ ProjectPackages::WinBiddingService.new(package, symbolize_params).call
+ rescue ProjectPackages::WinBiddingService::Error =>ex
+ render_error(ex.message)
+ end
+ end
+ end
+ end
+
+
+ desc 'create project packages'
+ params do
+ requires :category, type: String, desc: '类型'
+ requires :title, type: String, desc: '标题'
+ requires :content, type: String, desc: '描述'
+ optional :attachment_ids, type: Array[Integer], desc: '附件ID数组'
+ requires :deadline_at, type: DateTime, desc: '截止日期'
+ requires :min_price, type: Float, desc: '最小费用'
+ optional :max_price, type: Float, desc: '最大费用'
+ requires :contact_name, type: String, desc: '联系人姓名'
+ requires :contact_phone, type: String, desc: '联系人手机号'
+ optional :code, type: String, desc: '验证码'
+ optional :publish, type: Boolean, desc: '是否申请发布'
+ end
+ post do
+ authenticate!
+ begin
+ package = current_user.project_packages.new
+ ::ProjectPackages::SaveService.new(package, symbolize_params).call
+ package.increment_visit_count!
+ render_ok(id: package.id)
+ rescue ::ProjectPackages::SaveService::Error => ex
+ render_error(ex.message)
+ end
+ end
+
+
+ desc 'update project package'
+ params do
+ requires :id, type: Integer, desc: 'ID'
+ requires :category, type: String, desc: '类型'
+ requires :title, type: String, desc: '标题'
+ requires :content, type: String, desc: '描述'
+ optional :attachment_ids, type: Array[Integer], desc: '附件ID数组'
+ requires :deadline_at, type: DateTime, desc: '截止日期'
+ requires :min_price, type: Float, desc: '最小费用'
+ optional :max_price, type: Float, desc: '最大费用'
+ requires :contact_name, type: String, desc: '联系人姓名'
+ requires :contact_phone, type: String, desc: '联系人手机号'
+ optional :code, type: String, desc: '验证码'
+ optional :publish, type: Boolean, desc: '是否申请发布'
+ end
+ put ':id' do
+ authenticate!
+ begin
+ package = current_user.project_packages.find(params[:id])
+ return render_error('该状态下不能编辑') unless package.editable?
+
+ ::ProjectPackages::SaveService.new(package, symbolize_params).call
+ package.increment_visit_count!
+ render_ok(id: package.id)
+ rescue ::ProjectPackages::SaveService::Error => ex
+ render_error(ex.message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/api/mobile/entities/project_package.rb b/app/api/mobile/entities/project_package.rb
new file mode 100644
index 00000000..7313abfc
--- /dev/null
+++ b/app/api/mobile/entities/project_package.rb
@@ -0,0 +1,72 @@
+module Mobile
+ module Entities
+ class ProjectPackage < Grape::Entity
+ include ApplicationHelper
+ include ActionView::Helpers::NumberHelper
+
+ expose :id
+ expose :title
+ expose :category
+ expose :status
+ expose :visit_count, if: { type: :index }
+ expose :bidding_users_count, if: { type: :index }
+ expose :min_price
+ expose :max_price
+ expose :contact_name, if: ->(package, opts){ opts[:user].id == package.creator_id || opts[:user].admin? || opts[:user].business? }
+ expose :contact_phone, if: ->(package, opts){ opts[:user].id == package.creator_id || opts[:user].admin? || opts[:user].business? }
+
+ expose :deadline_at do |package, _|
+ package.deadline_at.try(:utc).try(:iso8601)
+ end
+ expose :published_at do |package, _|
+ package.published_at.try(:utc).try(:iso8601)
+ end
+
+ expose :creator, if: { type: :show } do |package, _|
+ {
+ id: package.creator.id,
+ login: package.creator.login,
+ name: package.creator.show_name,
+ avatar_url: "/images/#{url_to_avatar(package.creator)}"
+ }
+ end
+
+ expose :attachments, if: { type: :show } do |package, _|
+ package.attachments.map do |attachment|
+ {
+ id: attachment.id,
+ title: attachment.title,
+ filesize: number_to_human_size(attachment.filesize),
+ url: "attachments/download/#{attachment.id}",
+ }
+ end
+ end
+
+ expose :bidding_users, if: { type: :show } do |package, _|
+ package.bidding_users.map do |bidding_user|
+ user = bidding_user.user
+ {
+ id: user.id,
+ login: user.login,
+ name: user.show_name,
+ avatar_url: "/images/#{url_to_avatar(user)}",
+ status: bidding_user.status
+ }
+ end
+ end
+
+ expose :operation, if: { type: :show } do |package, opts|
+ user = opts[:user]
+ is_creator = user.id == package.creator_id
+ is_admin = user.admin? || user.business?
+
+ {
+ can_bidding: package.can_bidding?(user),
+ can_select_bidding_user: package.bidding_end? && package.end_bidding? && (is_creator || is_admin),
+ can_edit: package.editable? && (is_creator || is_admin),
+ can_delete: package.deletable? && (is_creator || is_admin)
+ }
+ end
+ end
+ end
+end
diff --git a/app/api/mobile/middleware/not_found_handler.rb b/app/api/mobile/middleware/not_found_handler.rb
new file mode 100644
index 00000000..155b6f6a
--- /dev/null
+++ b/app/api/mobile/middleware/not_found_handler.rb
@@ -0,0 +1,20 @@
+#coding=utf-8
+
+
+module Mobile
+ module Middleware
+ class NotFoundHandler < Grape::Middleware::Base
+ def call!(env)
+ @env = env
+ begin
+ @app.call(@env)
+ rescue ActiveRecord::RecordNotFound => e
+ message = {status: 404, message: e.message }.to_json
+ status = 200
+ headers = { 'Content-Type' => content_type }
+ Rack::Response.new([message], status, headers).finish
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index 3368da12..7e474611 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -681,7 +681,7 @@ class AccountController < ApplicationController
render :json => req
end
- # 发送验证码:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码
+ # 发送验证码:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码 9:验证手机号有效
# 验证码是否有效
def valid_verification_code
req = Hash.new(false)
@@ -700,7 +700,7 @@ class AccountController < ApplicationController
render :json => req
end
- # 发送验证码:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码
+ # 发送验证码:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码 9:验证手机号有效
def get_verification_code
code = %W(0 1 2 3 4 5 6 7 8 9)
type = params[:type].to_i
@@ -787,6 +787,22 @@ class AccountController < ApplicationController
else
req[:status] = 2
end
+ elsif params[:type] == 9
+ if params[:value] =~ /^1\d{10}$/
+ begin
+ verification_code = code.sample(6).join
+ status = Trustie::Sms.send(mobile: params[:value], code: verification_code)
+ if status == 0
+ VerificationCode.create(:phone => params[:value], :status => 1, :code_type => type, :code => verification_code)
+ end
+ req[:msg] = code_msg status
+ rescue => e
+ Rails.logger.error "发送验证码出错: #{e}"
+ end
+ req[:status] = 1
+ else
+ req[:status] = 2
+ end
else
if params[:value] =~ /^[a-zA-Z0-9]+([._\\]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/
if User.where(:mail => params[:value]).count > 0
diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb
index 428e1157..b0a7be31 100644
--- a/app/controllers/attachments_controller.rb
+++ b/app/controllers/attachments_controller.rb
@@ -895,6 +895,8 @@ class AttachmentsController < ApplicationController
@school = @attachment.container
elsif @attachment.container_type == 'Library'
@library = @attachment.container
+ elsif @attachment.container_type == 'ProjectPackage'
+ @package = @attachment.container
else
unless @attachment.container_type == 'Syllabus' || @attachment.container_type == 'Bid' || @attachment.container_type == 'Organization' || @attachment.container_type == 'HomeworkAttach' || @attachment.container_type == 'Memo' || @attachment.container_type == 'Softapplication' || @attachment.container_type == 'PhoneAppVersion' || @attachment.container_type == 'StudentWorksScore' || @attachment.container_type == 'StudentWork' || @attachment.container_type == 'Work'|| @attachment.container_type == 'ContestantWork'|| @attachment.container_type == 'Contest' || @attachment.container_type == 'HomeworkBank' || @attachment.container_type == 'Exercise' || @attachment.container_type == 'ExerciseBank' || @attachment.container_type == 'Career' || @attachment.container_type == 'EcTemplate'
@project = @attachment.project
diff --git a/app/controllers/managements/project_package_applies_controller.rb b/app/controllers/managements/project_package_applies_controller.rb
new file mode 100644
index 00000000..6394b745
--- /dev/null
+++ b/app/controllers/managements/project_package_applies_controller.rb
@@ -0,0 +1,46 @@
+class Managements::ProjectPackageAppliesController < Managements::BaseController
+ before_filter :set_menu_type
+
+ def index
+ applies = ProjectPackageApply.order('project_package_applies.updated_at desc')
+
+ search = params[:search].to_s.strip
+ if search.present?
+ applies = applies.joins(:project_package).where('project_packages.title like :search', search: "%#{search}%")
+ end
+
+ applies = applies.where(status: params[:status].presence || :pending)
+
+ @applies = paginateHelper applies.includes(project_package: { creator: :user_extensions })
+
+ respond_to do |format|
+ format.js
+ format.html
+ end
+ end
+
+ def agree
+ ProjectPackages::AgreeApplyService.new(current_apply).call
+ render json: { status: 0 }
+ rescue ProjectPackages::AgreeApplyService::Error => e
+ render json: { status: -1, message: e.message }
+ end
+
+ def refuse
+ ProjectPackages::RefuseApplyService.new(current_apply, reason: params[:reason]).call
+ render json: { status: 0 }
+ rescue ProjectPackages::RefuseApplyService::Error => e
+ render json: { status: -1, message: e.message }
+ end
+
+ private
+
+ def current_apply
+ @_current_apply ||= ProjectPackageApply.find(params[:id])
+ end
+
+ def set_menu_type
+ @menu_type = 10
+ @sub_type = 9
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/managements/project_packages_controller.rb b/app/controllers/managements/project_packages_controller.rb
new file mode 100644
index 00000000..3ea574c0
--- /dev/null
+++ b/app/controllers/managements/project_packages_controller.rb
@@ -0,0 +1,55 @@
+class Managements::ProjectPackagesController < Managements::BaseController
+ before_filter :set_menu_type, only: [:index]
+
+ def index
+ packages = ProjectPackage.where(nil)
+
+ # 任务标题
+ keyword = params[:keyword].to_s.strip
+ packages = packages.where('title LIKE ?', "%#{keyword}%") if keyword.present?
+
+ # 发布者姓名
+ creator_name = params[:creator_name].to_s.strip
+ if creator_name.present?
+ sql = 'LOWER(concat(users.lastname, users.firstname)) LIKE ?'
+ packages = packages.joins(:creator).where(sql, "%#{creator_name}%")
+ end
+
+ # 状态
+ status =
+ case params[:status]
+ when 'pending' then %w(pending refused)
+ when 'applying' then %w(applying)
+ when 'published' then %w(published bidding_end)
+ when 'finished' then %w(bidding_finished)
+ end
+ packages = packages.where(status: status) if status.present?
+
+ # 发布时间
+ begin_date = (params[:begin_date].to_time.beginning_of_day rescue nil)
+ end_date = (params[:end_date].to_time.end_of_day rescue nil)
+ packages = packages.where('published_at >= ?', begin_date) if begin_date.present?
+ packages = packages.where('published_at <= ?', end_date) if end_date.present?
+
+ @count = packages.count
+
+ # 排序
+ params[:sort_by] ||= 'created_at'
+ params[:sort_direction] ||= 'desc'
+ packages = packages.order("#{params[:sort_by]} #{params[:sort_direction]}")
+
+ @packages = paginateHelper packages.preload(:creator)
+ end
+
+ def destroy
+ ProjectPackage.find(params[:id]).destroy
+ render json: { status: 0 }
+ end
+
+ private
+
+ def set_menu_type
+ @menu_type = 14
+ @sub_type = 1
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/project_packages_controller.rb b/app/controllers/project_packages_controller.rb
new file mode 100644
index 00000000..fe2c1c13
--- /dev/null
+++ b/app/controllers/project_packages_controller.rb
@@ -0,0 +1,45 @@
+# encoding=utf-8
+# For react
+class ProjectPackagesController < ApplicationController
+ before_filter :require_login, :except => [:index]
+
+ include ApplicationHelper
+
+ def show
+ render_react
+ end
+
+ def new
+ render_react
+ end
+
+ def index
+ render_react
+ end
+
+ def edit
+ render_react
+ end
+
+ def apply_success
+ render_react
+ end
+
+ def destroy
+ package = ProjectPackage.find(params[:id])
+ return render_403 unless package.deletable? && (admin_or_business? || package.creator_id == current_user.id)
+
+ 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 json: { status: 0, message: 'success' }
+ end
+
+ private
+ def render_react
+ render file: 'public/react/build/index.html', :layout => false
+ end
+
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4fae7ff7..66b6fde0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -2425,6 +2425,8 @@ class UsersController < ApplicationController
when "a_bank", "m_bank", "p_bank"
@sort = params[:sort] || "updated_at"
@p = params[:p] || "Common"
+ when 'a_package', 'l_package', 'p_package'
+ @p = params[:p] || 'a'
end
case @type
@@ -2558,6 +2560,59 @@ class UsersController < ApplicationController
@tag = params[:tag]
@objects = @objects.where(:course_list_id => params[:tag]).order("#{@sort} #{order}")
end
+ when 'a_package'
+ bidding_packages = @user.bidding_project_packages
+ bidding_packages = bidding_packages.none if @show_all && params[:q] == 0 # 参与的不存在未发布的
+ packages = @user.project_packages
+
+ if @show_all
+ status =
+ case params[:p]
+ when '0' then %w(pending applying refused)
+ when '1' then %w(published)
+ when '2' then %w(bidding_ended bidding_finished)
+ end
+ if status.present?
+ packages = packages.where(status: status)
+ bidding_packages = bidding_packages.where(status: status)
+ end
+ else
+ packages = packages.where(status: %w(published bidding_ended bidding_finished))
+ end
+
+ @per_page = 20
+ ids = bidding_packages.pluck(:id) + packages.pluck(:id)
+ @objects = ProjectPackage.where(id: ids).order("published_at #{order}")
+ when 'p_package'
+ packages = @user.project_packages
+ if @show_all
+ status =
+ case params[:p]
+ when '0' then %w(pending applying refused)
+ when '1' then %w(published)
+ when '2' then %w(bidding_ended bidding_finished)
+ end
+ if status.present?
+ packages = packages.where(status: status)
+ end
+ else
+ packages = packages.where(status: %w(published bidding_ended bidding_finished))
+ end
+ @per_page = 20
+ @objects = packages.order("published_at #{order}")
+ when 'l_package'
+ packages = @user.bidding_project_packages
+ if @show_all
+ status =
+ case params[:p]
+ when '0' then %w(bidding_lost)
+ when '1' then %w(bidding_won)
+ end
+
+ packages = packages.where(bidding_users: { status: status }) if status.present?
+ end
+ @per_page = 20
+ @objects = packages.order("published_at #{order}")
end
@objects_count = @objects.size
@@ -2575,7 +2630,7 @@ class UsersController < ApplicationController
@objects = @objects.to_a
@objects.unshift("new") if @new_icon
- @objects = paginateHelper @objects, 16
+ @objects = paginateHelper @objects, @per_page || 16
respond_to do |format|
format.js
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index b65a2fa8..87a0e22b 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -754,6 +754,7 @@ module ApplicationHelper
when 6 then '实训课程发布'
when 7 then '职业认证'
when 8 then '教学案例发布'
+ when 9 then '众包需求发布'
else '职业认证'
end
when 11
@@ -771,6 +772,10 @@ module ApplicationHelper
)
)
)
+ when 14
+ case sub_type
+ when 1 then '任务列表'
+ end
end
end
@@ -4370,6 +4375,8 @@ module ApplicationHelper
candown = User.current.member_of_course?(attachment.container.courses.first) || (course.is_public == 1 && attachment.is_public == 1)
elsif attachment.container_type == "Library" #教学案例允许下载
candown = true
+ elsif attachment.container_type == "ProjectPackage" #教学案例允许下载
+ candown = true
else
candown = (attachment.is_public == 1 || attachment.is_public == true)
end
@@ -7453,6 +7460,8 @@ def tiding_url tiding
my_account_path
when 'Library'
tiding.tiding_type == 'Apply' ? library_applies_path : library_path(tiding.container_id)
+ when 'ProjectPackage'
+ tiding.container.present? ? "/project_packages/#{tiding.container_id}" : 'javascript:void(0)'
end
end
diff --git a/app/models/bidding_user.rb b/app/models/bidding_user.rb
new file mode 100644
index 00000000..9f368b2a
--- /dev/null
+++ b/app/models/bidding_user.rb
@@ -0,0 +1,24 @@
+class BiddingUser < ActiveRecord::Base
+ 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
\ No newline at end of file
diff --git a/app/models/project_package.rb b/app/models/project_package.rb
new file mode 100644
index 00000000..8f9035f6
--- /dev/null
+++ b/app/models/project_package.rb
@@ -0,0 +1,96 @@
+class ProjectPackage < ActiveRecord::Base
+ include AASM
+
+ acts_as_attachable
+
+ CATEGORY_VALUES = %w(front backend mobile database cloud_compute_and_big_data devops_and_test ai other)
+
+ attr_accessible :title, :content, :category, :deadline_at, :contact_name, :contact_phone, :min_price, :max_price
+
+ belongs_to :creator, class_name: 'User'
+
+ has_many :project_package_applies, dependent: :destroy
+ has_one :process_project_package_apply, conditions: { status: :pending }, class_name: 'ProjectPackageApply'
+
+ has_many :bidding_users, dependent: :delete_all
+ has_many :win_bidding_users, conditions: { status: :bidding_won }, class_name: 'BiddingUser'
+ has_many :lose_bidding_users, conditions: { status: :bidding_lost }, class_name: 'BiddingUser'
+
+ has_many :attachments, as: :container, dependent: :destroy
+
+ validates :category, presence: true, inclusion: { in: CATEGORY_VALUES }
+ validates :title, presence: true, length: { maximum: 60 }
+ validates :content, presence: true
+ validates :deadline_at, presence: true
+ validates :contact_name, presence: true, length: { maximum: 20 }
+ validates :contact_phone, presence: true, format: { with: /1\d{10}/ }
+ validates :min_price, numericality: { greater_than: 0 }
+ validates :max_price, numericality: { greater_than: ->(obj){ obj.min_price } }, allow_blank: true
+
+ 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 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? && !bidding_users.exists?(user_id: user.id)
+ end
+
+ def increment_visit_count!
+ ProjectPackage.connection.execute("update project_packages set visit_count = COALESCE(visit_count, 0) + 1 where id = #{id}")
+ end
+
+ def category_text
+ I18n.t("project_package.category.#{category}")
+ end
+
+ def status_text
+ I18n.t("project_package.status.#{status}")
+ end
+end
\ No newline at end of file
diff --git a/app/models/project_package_apply.rb b/app/models/project_package_apply.rb
new file mode 100644
index 00000000..06c7493f
--- /dev/null
+++ b/app/models/project_package_apply.rb
@@ -0,0 +1,21 @@
+class ProjectPackageApply < ActiveRecord::Base
+ include AASM
+
+ attr_accessible :project_package_id, :reason
+
+ 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
\ No newline at end of file
diff --git a/app/models/tiding.rb b/app/models/tiding.rb
index 5107374e..986d9cef 100644
--- a/app/models/tiding.rb
+++ b/app/models/tiding.rb
@@ -365,6 +365,27 @@ class Tiding < ActiveRecord::Base
elsif tiding_type == 'System'
text = status == 1 ? "审核已通过" : "审核未通过,
原因:#{extra}"
"你提交的发布教学案例申请:#{library.try(:title)},#{text}"
+ end
+ when 'ProjectPackage'
+ package = ProjectPackage.find_by_id(container_id)
+ case tiding_type
+ when 'Apply' then
+ "申请发布众包需求:#{package.try(:title)}"
+ when 'System' then
+ text = status == 1 ? "审核已通过" : "审核未通过,
原因:#{extra}"
+ "你提交的众包需求申请:#{package.try(:title)},#{text}"
+ when 'Created' then
+ "你创建了众包需求:#{package.try(:title)}"
+ when 'Destroyed' then
+ "你删除了众包需求:#{extra}"
+ when 'Bidding' then
+ "应征了你发布的众包任务:#{package.try(:title)}"
+ when 'BiddingEnd' then
+ "你发布的众包任务:#{package.try(:title)},已进入选标阶段,请尽快进行选择确认!"
+ when 'BiddingWon' then
+ "恭喜,你应征的众包任务:#{package.try(:title)},在评选环节中标了"
+ when 'BiddingLost' then
+ "很遗憾,你应征投稿的众包任务:#{package.try(:title)},未中标"
end
else
logger.error "error type: 1"
diff --git a/app/models/user.rb b/app/models/user.rb
index c206d51e..0e034d32 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -274,6 +274,11 @@ class User < Principal
has_one :user_source
+ # 众包
+ has_many :project_packages, foreign_key: :creator_id, dependent: :destroy
+ has_many :bidding_users, dependent: :destroy
+ has_many :bidding_project_packages, through: :bidding_users, source: :project_package
+
## end
# default_scope -> { includes(:user_extensions, :user_score) }
diff --git a/app/models/verification_code.rb b/app/models/verification_code.rb
index e3fefc83..4bc473b3 100644
--- a/app/models/verification_code.rb
+++ b/app/models/verification_code.rb
@@ -2,4 +2,8 @@ class VerificationCode < ActiveRecord::Base
#status:发送状态
#code_type:发送类型:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码
attr_accessible :code, :code_type, :email, :phone, :status
+
+ def valid_code?
+ (Time.now.to_i - created_at.to_i) <= 10*60
+ end
end
diff --git a/app/services/careers_service.rb b/app/services/careers_service.rb
index 6f085b64..a548c74d 100644
--- a/app/services/careers_service.rb
+++ b/app/services/careers_service.rb
@@ -506,7 +506,7 @@ class CareersService
{username: current_user.show_name, login: current_user.login,
user_id: current_user.id, image_url: url_to_avatar(current_user),
admin: current_user.admin?, is_teacher: current_user.user_extensions.try(:identity) == 0,
- tidding_count: count}
+ tidding_count: count, phone: current_user.phone}
end
def find_career id
diff --git a/app/services/project_packages/agree_apply_service.rb b/app/services/project_packages/agree_apply_service.rb
new file mode 100644
index 00000000..c8d8ebde
--- /dev/null
+++ b/app/services/project_packages/agree_apply_service.rb
@@ -0,0 +1,36 @@
+class ProjectPackages::AgreeApplyService
+ 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
\ No newline at end of file
diff --git a/app/services/project_packages/apply_publish_service.rb b/app/services/project_packages/apply_publish_service.rb
new file mode 100644
index 00000000..0cf54259
--- /dev/null
+++ b/app/services/project_packages/apply_publish_service.rb
@@ -0,0 +1,31 @@
+class ProjectPackages::ApplyPublishService
+ 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
diff --git a/app/services/project_packages/bidding_service.rb b/app/services/project_packages/bidding_service.rb
new file mode 100644
index 00000000..9ad9ce4f
--- /dev/null
+++ b/app/services/project_packages/bidding_service.rb
@@ -0,0 +1,29 @@
+class ProjectPackages::BiddingService
+ 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
\ No newline at end of file
diff --git a/app/services/project_packages/end_bidding_service.rb b/app/services/project_packages/end_bidding_service.rb
new file mode 100644
index 00000000..f7fa7b59
--- /dev/null
+++ b/app/services/project_packages/end_bidding_service.rb
@@ -0,0 +1,26 @@
+class ProjectPackages::EndBiddingService
+ 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
\ No newline at end of file
diff --git a/app/services/project_packages/refuse_apply_service.rb b/app/services/project_packages/refuse_apply_service.rb
new file mode 100644
index 00000000..ee4c5bb7
--- /dev/null
+++ b/app/services/project_packages/refuse_apply_service.rb
@@ -0,0 +1,38 @@
+class ProjectPackages::RefuseApplyService
+ 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
diff --git a/app/services/project_packages/save_service.rb b/app/services/project_packages/save_service.rb
new file mode 100644
index 00000000..9011722c
--- /dev/null
+++ b/app/services/project_packages/save_service.rb
@@ -0,0 +1,67 @@
+class ProjectPackages::SaveService
+ Error = Class.new(StandardError)
+
+ attr_reader :package, :params
+
+ def initialize(package, params)
+ @package = package
+ @params = params
+ end
+
+ def call
+ check_code_valid! if need_check_code?
+
+ is_create = package.new_record?
+
+ ActiveRecord::Base.transaction do
+ package.assign_attributes(params)
+ package.save!
+
+ # 处理附件
+ deal_attachments
+
+ send_create_notify! if is_create
+
+ ProjectPackages::ApplyPublishService.new(package).call 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.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
\ No newline at end of file
diff --git a/app/services/project_packages/win_bidding_service.rb b/app/services/project_packages/win_bidding_service.rb
new file mode 100644
index 00000000..f3efdcb7
--- /dev/null
+++ b/app/services/project_packages/win_bidding_service.rb
@@ -0,0 +1,47 @@
+class ProjectPackages::WinBiddingService
+ 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.can_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)
+ sql = 'insert into tidings(user_id, trigger_user_id, container_id, container_type, tiding_type) values'
+ base_opts = [package.creator_id, package.id, 'ProjectPackage', type]
+
+ values = '(' + user_ids.map { |id| base_opts.clone.unshift(id).join(',') }.join('),(') + ')'
+
+ sql + values
+ end
+end
\ No newline at end of file
diff --git a/app/tasks/check_project_package_deadline_task.rb b/app/tasks/check_project_package_deadline_task.rb
new file mode 100644
index 00000000..4ccec257
--- /dev/null
+++ b/app/tasks/check_project_package_deadline_task.rb
@@ -0,0 +1,12 @@
+class CheckProjectPackageDeadlineTask
+ def call
+ ProjectPackage.where(status: :published).where('deadline_at < ?', Time.now).find_each do |package|
+ begin
+ ProjectPackages::EndBiddingService.new(package).call
+ rescue => ex
+ Rails.logger.error ex.message
+ Rails.logger.error ex.backtrace.join('\n')
+ end
+ end
+ end
+end
diff --git a/app/views/layouts/base_management.html.erb b/app/views/layouts/base_management.html.erb
index d17a05de..f0fefec5 100644
--- a/app/views/layouts/base_management.html.erb
+++ b/app/views/layouts/base_management.html.erb
@@ -77,6 +77,11 @@
众包需求发布
++ <%= link_to "全部", project_package_applies_path(status: [:refused, :agreed]), :class => "edu-filter-cir-grey mr5 fl font-12 active", :id => "project_package_all_authentication", :remote => true %> + <%= link_to "同意", project_package_applies_path(status: :agreed), :class => "edu-filter-cir-grey mr5 fl font-12", :id => "project_package_agree_authentication", :remote => true %> + <%= link_to "拒绝", project_package_applies_path(status: :refused), :class => "edu-filter-cir-grey mr5 fl font-12", :id => "project_package_reject_authentication", :remote => true %> +
+ + +序号 | +任务标题 | +状态 | +竞标人数 | +发布者 | +<%= sort_tag('创建时间', name: 'created_at', path: managements_project_packages_path) %> | +<%= sort_tag('发布时间', name: 'published_at', path: managements_project_packages_path) %> | +<%= sort_tag('竞标截止时间', name: 'deadline_at', path: managements_project_packages_path) %> | +操作 | +|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<%= (@obj_pages.page - 1) * @obj_pages.per_page + index + 1 %> | ++ <%= link_to package.title, project_package_path(package), target: '_blank', + style: "display: block;max-width: 250px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;", + data: { tip_down: package.title } %> + | +<%= package.status_text %> | +<%= package.bidding_users_count %> | +<%= package.creator.show_real_name %> | +<%= package.created_at.strftime('%Y-%m-%d %H:%M') %> | +<%= package.published_at.try(:strftime, '%Y-%m-%d %H:%M') || '--' %> | +<%= package.deadline_at.try(:strftime, '%Y-%m-%d %H:%M') || '--' %> | +<%= link_to '删除', 'javascript:void(0)', class: 'delete-btn', data: { id: package.id } %> | +|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
+ <%= render :partial => "welcome/no_data" %> + | +