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 "竞赛列表", competition_managements_path %>
  • +
  • 众包+ + +
  • 单位
  • 认证+ diff --git a/app/views/managements/project_package_applies/_project_package_apply_list.html.erb b/app/views/managements/project_package_applies/_project_package_apply_list.html.erb new file mode 100644 index 00000000..6e55967f --- /dev/null +++ b/app/views/managements/project_package_applies/_project_package_apply_list.html.erb @@ -0,0 +1,76 @@ +<% if @applies.present? %> + <% @applies.each do |apply| %> + <% user = apply.project_package.creator %> + <% project_package = apply.project_package %> +
    + + <%= image_tag(url_to_avatar(user), :class => "fl with10 edu-ad-user", :alt => "头像", :width => "50", :height => "50" ) %> + +
    + +
    +
    + <% end %> +
    +
    +
      + <%= pagination_links_full @obj_pages, @obj_count, :per_page_links => false, :remote => true, :flag => true, :is_new => true %> +
    +
    +
    +
    +<% else %> + <%= render :partial => "welcome/no_data" %> +<% end %> + + \ No newline at end of file diff --git a/app/views/managements/project_package_applies/index.html.erb b/app/views/managements/project_package_applies/index.html.erb new file mode 100644 index 00000000..8d2b955a --- /dev/null +++ b/app/views/managements/project_package_applies/index.html.erb @@ -0,0 +1,120 @@ +
    +
    +

    众包需求发布

    +
    +
    +
    +
      +
    • + <%= link_to "待审批", project_package_applies_path(status: :pending), class: 'tab_type', remote: true %> +
    • +
    • + <%= link_to "已审批", project_package_applies_path(status: [:refused, :agreed]), class: 'tab_type', remote: true %> +
    • +
    +
    +
    +
    +
    + + +
    +
    +
    + <%= render :partial => "managements/project_package_applies/project_package_apply_list"%> +
    +
    +
    + +
    +
    +

    + <%= 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 %> +

    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + \ No newline at end of file diff --git a/app/views/managements/project_package_applies/index.js.erb b/app/views/managements/project_package_applies/index.js.erb new file mode 100644 index 00000000..4e6d737d --- /dev/null +++ b/app/views/managements/project_package_applies/index.js.erb @@ -0,0 +1,30 @@ +var nTabIcon_1 = $("#edu-tab-con-1"); +var nTabIcon_2 = $("#edu-tab-con-2"); +var nTabNav_1 = $("#edu-tab-nav-1"); +var nTabNav_2 = $("#edu-tab-nav-2"); +var nAudit = $("#project_package_all_authentication").parent(); + +<% if params[:status].to_s == 'pending' %> +$("#authentication_list").html("<%= j( render :partial => "managements/project_package_applies/project_package_apply_list" ) %>"); +nTabNav_1.addClass("background-orange"); +nTabNav_2.removeClass("background-orange"); +nTabIcon_1.show(); +nTabIcon_2.hide(); +<% else %> +$("#project_package_authentication_list").html("<%= j( render :partial => "managements/project_package_applies/project_package_apply_list" ) %>"); +nTabNav_1.removeClass("background-orange"); +nTabNav_2.addClass("background-orange"); +nTabIcon_1.hide(); +nTabIcon_2.show(); +/* -------------------------- 未审批(全部、同意、拒绝点击时动态样式) ------------------------------ */ +if(<%= params[:status].to_s == 'agreed' %>){ + nAudit.find(".active").removeClass("active"); + $("#project_package_agree_authentication").addClass("active"); +}else if(<%= params[:status].to_s == 'refused' %>){ + nAudit.find(".active").removeClass("active"); + $("#project_package_reject_authentication").addClass("active"); +}else{ + nAudit.find(".active").removeClass("active"); + $("#project_package_all_authentication").addClass("active"); +} +<% end %> \ No newline at end of file diff --git a/app/views/managements/project_packages/_project_package_list.html.erb b/app/views/managements/project_packages/_project_package_list.html.erb new file mode 100644 index 00000000..4dab9d3d --- /dev/null +++ b/app/views/managements/project_packages/_project_package_list.html.erb @@ -0,0 +1,53 @@ +
    <%= @obj_count %>条搜索结果
    + + + + + + + + + + + + + + + + + <% if @packages.present? %> + <% @packages.each_with_index do |package, index| %> + + + + + + + + + + + + <% end %> + <% else %> + + + + <% end %> + +
    序号任务标题状态竞标人数发布者<%= 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" %> +
    + +
    +
    +
      + <%= pagination_links_full @obj_pages, @obj_count, per_page_links: false, remote: true, flag: true, is_new: true, path: managements_project_packages_path(params.except(:page)) %> +
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/managements/project_packages/index.html.erb b/app/views/managements/project_packages/index.html.erb new file mode 100644 index 00000000..01f38b7b --- /dev/null +++ b/app/views/managements/project_packages/index.html.erb @@ -0,0 +1,115 @@ +
    + <%= form_tag(managements_project_packages_path,method: :get, remote: true, id: '', class: 'management-project-packages-form' ) do %> + + + <%= text_field_tag :keyword, params[:keyword], placeholder: '请输入任务标题进行搜索', class: 'fl winput-200-30 ml10' %> + + <%= text_field_tag :creator_name, params[:creator_name], placeholder: '请输入发布者进行搜索', + class: 'fl winput-200-30 ml10' %> + +
    + 发布时间: + <%= text_field_tag :begin_date, params[:begin_date], + class: 'winput-150-30', placeholder: '请选择开始时间'%> + <%= text_field_tag :end_date, params[:end_date], + class: 'winput-150-30', placeholder: '请选择结束时间'%> +
    + + <%= link_to '清除', 'javascript:clearSearchCondition()', class: 'fr task-btn ml5 reset-btn' %> + <%= link_to '搜索', 'javascript:void(0)', class: 'fr task-btn task-btn-orange ml5 search-btn' %> + <% end %> +
    + +
    + <%= render partial: 'managements/project_packages/project_package_list' %> +
    + + \ No newline at end of file diff --git a/app/views/managements/project_packages/index.js.erb b/app/views/managements/project_packages/index.js.erb new file mode 100644 index 00000000..e21fa9dc --- /dev/null +++ b/app/views/managements/project_packages/index.js.erb @@ -0,0 +1 @@ +$("#managements-project-packages-list").html("<%= j(render 'managements/project_packages/project_package_list') %>") \ No newline at end of file diff --git a/app/views/users/_project_package.html.erb b/app/views/users/_project_package.html.erb new file mode 100644 index 00000000..db21fd0a --- /dev/null +++ b/app/views/users/_project_package.html.erb @@ -0,0 +1,142 @@ +<% if @type == 'p_package' && User.current == @user %> +
    +
  • <%= link_to "全部", user_path(@user, :type => @type), :remote => true %>
  • +
  • <%= link_to "未发布", user_path(@user, :type => @type, :p => "0"), :remote => true %>
  • +
  • <%= link_to "竞标中", user_path(@user, :type => @type, :p => "1"), :remote => true %>
  • +
  • <%= link_to "已完成", user_path(@user, :type => @type, :p => "2"), :remote => true %>
  • + +<% elsif @type == 'l_package' && User.current == @user %> +
    +
  • <%= link_to "全部", user_path(@user, :type => @type, :p => "a"), :remote => true %>
  • +
  • <%= link_to "未中标", user_path(@user, :type => @type, :p => "0"), :remote => true %>
  • +
  • <%= link_to "已中标", user_path(@user, :type => @type, :p => "1"), :remote => true %>
  • +
    +<% end %> + +
    + 共<%= @objects_count %>个 +
    + <%= link_to '发布时间', user_path(@user, :order => @new_order, :sort => @sort, :type => @type), :class => "fl color-grey-9", :remote => true %> +
    +
    + +<% if @objects_count > 0 %> +
    + <% @objects.each do |object| %> + <% can_manage = @type == 'p_package' && (admin_or_business? || current_user.id == @user.id) %> +
    +
    + <%= image_tag("educoder/project_packages/#{object.category}.png") %> +
    +
    +
    +
    + <%= link_to object.title, project_package_path(object) %> +
    +
    + <% if object.creator_id != @user.id %> + <% bidding_user = object.bidding_users.find_by_user_id(@user.id) %> + <%= raw content_tag(:span, bidding_user.status_text, class: bidding_user.status) %> + <% end %> +
    +
    +
    + <% if object.max_price && object.max_price != object.min_price %> + <%= object.min_price %>~<%= object.max_price %> + <% else %> + <%= object.min_price %> + <% end %> +
    +
    +
    +
    <%= object.category_text %>
    +
    +
    +
    + + <%= object.visit_count %>人浏览 +
    +
    + <% if object.published? %> + + + <% if Time.now + 10.days > object.deadline_at %> + <%= time_from_future(object.deadline_at) %>内竞标截止 + <% else %> + <%= object.deadline_at.strftime('%Y-%m-%d') %> 竞标截止 + <% end %> + + <% end %> +
    +
    + <% if object.bidding_users_count > 0 %> + + <%= object.bidding_users_count %>人竞标 + <% end %> +
    +
    +
    + <% if object.published_at.present? %> + 发布日期:<%= object.published_at.try(:strftime, '%Y-%m-%d') %> + <% end %> +
    +
    +
    + <% if can_manage && (object.editable? || object.deletable?) %> +
    + <% if object.editable? %> + + <% end %> + <% if object.deletable? %> + + <% end %> +
    + <% end %> +
    + <% end %> +
    + +
    +
    + +
    +
    +
    +<% else %> + <%= render :partial => "welcome/no_data" %> +<% end %> + + \ No newline at end of file diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 6d42f201..fb459160 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,6 +1,8 @@ <% current_user = @user == User.current %> <% str = current_user ? '我' : 'TA' %> +<% show_package = @user.project_packages.count.nonzero? || @user.bidding_project_packages.count.nonzero? %> +