From 896ca84c722dde96ae8b1bb33778216bdf6bb69e Mon Sep 17 00:00:00 2001
From: p31729568 <winse.wang@foxmail.com>
Date: Thu, 18 Jul 2019 11:24:50 +0800
Subject: [PATCH] competition api

---
 app/controllers/application_controller.rb     |  4 +
 .../competitions/base_controller.rb           |  2 +-
 .../competition_modules_controller.rb         | 28 ++++++
 .../competition_teams_controller.rb           | 49 ++++++++++
 .../competitions/competitions_controller.rb   |  6 ++
 .../competitions/students_controller.rb       | 22 +++++
 .../competitions/teachers_controller.rb       | 22 +++++
 app/forms/competitions/save_team_form.rb      | 98 +++++++++++++++++++
 app/models/competition.rb                     | 25 +++++
 app/models/competition_module_md_content.rb   |  3 +
 app/models/competition_team.rb                | 25 +++++
 app/models/team_member.rb                     |  8 ++
 .../competitions/join_team_service.rb         | 35 +++++++
 .../competitions/save_team_service.rb         | 70 +++++++++++++
 .../competition_modules/index.json.jbuilder   |  7 ++
 .../competition_modules/show.json.jbuilder    |  8 ++
 .../competition_teams/index.json.jbuilder     | 23 +++++
 .../competitions/students/index.json.jbuilder |  9 ++
 .../competitions/teachers/index.json.jbuilder |  9 ++
 config/locales/competitions/zh-CN.yml         |  8 ++
 config/locales/forms/save_team_form.zh-CN.yml | 25 +++++
 config/routes.rb                              |  8 +-
 22 files changed, 491 insertions(+), 3 deletions(-)
 create mode 100644 app/controllers/competitions/competition_modules_controller.rb
 create mode 100644 app/controllers/competitions/competition_teams_controller.rb
 create mode 100644 app/controllers/competitions/students_controller.rb
 create mode 100644 app/controllers/competitions/teachers_controller.rb
 create mode 100644 app/forms/competitions/save_team_form.rb
 create mode 100644 app/services/competitions/join_team_service.rb
 create mode 100644 app/services/competitions/save_team_service.rb
 create mode 100644 app/views/competitions/competition_modules/index.json.jbuilder
 create mode 100644 app/views/competitions/competition_modules/show.json.jbuilder
 create mode 100644 app/views/competitions/competition_teams/index.json.jbuilder
 create mode 100644 app/views/competitions/students/index.json.jbuilder
 create mode 100644 app/views/competitions/teachers/index.json.jbuilder
 create mode 100644 config/locales/competitions/zh-CN.yml
 create mode 100644 config/locales/forms/save_team_form.zh-CN.yml

diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4bc3d5133..875050642 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -148,6 +148,10 @@ class ApplicationController < ActionController::Base
 		normal_status(403, "") unless User.current.admin?
 	end
 
+	def require_business
+		normal_status(403, "") unless admin_or_business?
+	end
+
 	# 前端会捕捉401,弹登录弹框
 	# 未授权的捕捉407,弹试用申请弹框
 	def require_login
diff --git a/app/controllers/competitions/base_controller.rb b/app/controllers/competitions/base_controller.rb
index 127474ee7..b42a4ce8f 100644
--- a/app/controllers/competitions/base_controller.rb
+++ b/app/controllers/competitions/base_controller.rb
@@ -6,6 +6,6 @@ class Competitions::BaseController < ApplicationController
   helper_method :current_competition
 
   def current_competition
-    @_current_competition ||= Competition.find_by!(identifier: params[:competition_id].presence || params[:id])
+    @_current_competition ||= Competition.find_by!(identifier: params[:competition_id])
   end
 end
\ No newline at end of file
diff --git a/app/controllers/competitions/competition_modules_controller.rb b/app/controllers/competitions/competition_modules_controller.rb
new file mode 100644
index 000000000..da9873c43
--- /dev/null
+++ b/app/controllers/competitions/competition_modules_controller.rb
@@ -0,0 +1,28 @@
+class Competitions::CompetitionModulesController < Competitions::BaseController
+  skip_before_action :require_login, only: [:index, :show]
+
+  before_action :require_business, only: [:update]
+
+  def index
+    @modules = current_competition.unhidden_competition_modules.order(position: :asc)
+  end
+
+  def show
+    @module = current_module
+  end
+
+  def update
+    md = current_module.competition_module_md_content || current_module.build_competition_module_md_content
+    md.name    = params[:md_name]
+    md.content = params[:md_content]
+    md.save!
+
+    render_ok
+  end
+
+  private
+
+  def current_module
+    @_current_module ||= current_competition.unhidden_competition_modules.find(params[:id])
+  end
+end
diff --git a/app/controllers/competitions/competition_teams_controller.rb b/app/controllers/competitions/competition_teams_controller.rb
new file mode 100644
index 000000000..34c80d24c
--- /dev/null
+++ b/app/controllers/competitions/competition_teams_controller.rb
@@ -0,0 +1,49 @@
+class Competitions::CompetitionTeamsController < Competitions::BaseController
+  def index
+    admin_or_business? ? all_competition_teams : user_competition_teams
+  end
+
+  def create
+    team = current_competition.competition_teams.new(user: current_user)
+    Competitions::SaveTeamService.call(team, save_params)
+    render_ok
+  end
+
+  def update
+    team = current_competition.competition_teams.where(user: current_user).find(params[:id])
+    Competitions::SaveTeamService.call(team, save_params)
+    render_ok
+  end
+
+  def join
+    Competitions::JoinTeamService.call(current_competition, current_user, params)
+    render_ok
+  rescue Competitions::JoinTeamService::Error => ex
+    render_error(ex.message)
+  end
+
+  private
+
+  def all_competition_teams
+    teams = current_competition.competition_teams
+
+    keyword = params[:keyword].to_s.strip
+    if keyword.present?
+      teams = teams.joins(users: { user_extension: :school }).where('schools.name LIKE ?', "%#{keyword}%")
+    end
+
+    @count = teams.count
+    @teams = paginate(teams.includes(:user, users: :user_extension))
+  end
+
+  def user_competition_teams
+    teams  = current_competition.competition_teams
+    teams  = teams.joins(:team_members).where(team_members: { user_id: current_user.id })
+    @teams = teams.includes(:user, users: :user_extension).to_a
+    @count = @teams.size
+  end
+
+  def save_params
+    params.permit(:name, teacher_ids: [], member_ids: [])
+  end
+end
diff --git a/app/controllers/competitions/competitions_controller.rb b/app/controllers/competitions/competitions_controller.rb
index f962682a1..34dac7350 100644
--- a/app/controllers/competitions/competitions_controller.rb
+++ b/app/controllers/competitions/competitions_controller.rb
@@ -29,4 +29,10 @@ class Competitions::CompetitionsController < Competitions::BaseController
       return
     end
   end
+
+  private
+
+  def current_competition
+    @_current_competition ||= Competition.find_by!(identifier: params[:id])
+  end
 end
\ No newline at end of file
diff --git a/app/controllers/competitions/students_controller.rb b/app/controllers/competitions/students_controller.rb
new file mode 100644
index 000000000..8fd235fd1
--- /dev/null
+++ b/app/controllers/competitions/students_controller.rb
@@ -0,0 +1,22 @@
+class Competitions::StudentsController < Competitions::BaseController
+  def index
+    keyword = params[:keyword].to_s.strip
+    if keyword.blank?
+      @students = []
+      return
+    end
+
+    students = User.joins(:user_extension).where(status: 1, user_extensions: { identity: 1 })
+    students = students.where.not(id: params[:student_ids]) if params[:student_ids].present?
+    students = students.where('LOWER(CONCAT(lastname, firstname, login, nickname)) LIKE ?', "%#{keyword}%")
+    @students = students.includes(user_extension: :school).limit(20)
+
+    # 队员多次报名限制
+    if current_competition.member_multiple_limited?
+      ids = @students.map(&:id)
+      members = current_competition.team_members.where(user_id: ids)
+      members = members.where.not(competition_team_id: params[:team_id]) if params[:team_id].present?
+      @enrolled_map = members.group(:user_id).count
+    end
+  end
+end
diff --git a/app/controllers/competitions/teachers_controller.rb b/app/controllers/competitions/teachers_controller.rb
new file mode 100644
index 000000000..76c7cfe6a
--- /dev/null
+++ b/app/controllers/competitions/teachers_controller.rb
@@ -0,0 +1,22 @@
+class Competitions::TeachersController < Competitions::BaseController
+  def index
+    keyword = params[:keyword].to_s.strip
+    if keyword.blank?
+      @teachers = []
+      return
+    end
+
+    teachers = User.joins(:user_extension).where(status: 1, user_extensions: { identity: 0 })
+    teachers = teachers.where.not(id: params[:teacher_ids]) if params[:teacher_ids].present?
+    teachers = teachers.where('LOWER(CONCAT(lastname, firstname, login, nickname)) LIKE ?', "%#{keyword}%")
+    @teachers = teachers.includes(user_extension: :school).limit(10)
+
+    # 老师多次报名限制
+    if current_competition.teacher_multiple_limited?
+      ids = @teachers.map(&:id)
+      members = current_competition.team_members.where(user_id: ids)
+      members = members.where.not(competition_team_id: params[:team_id]) if params[:team_id].present?
+      @enrolled_map = members.group(:user_id).count
+    end
+  end
+end
diff --git a/app/forms/competitions/save_team_form.rb b/app/forms/competitions/save_team_form.rb
new file mode 100644
index 000000000..10685d260
--- /dev/null
+++ b/app/forms/competitions/save_team_form.rb
@@ -0,0 +1,98 @@
+class Competitions::SaveTeamForm
+  include ActiveModel::Model
+
+  attr_accessor :competition, :team, :creator
+  attr_accessor :name, :teacher_ids, :member_ids
+
+  validates :name, presence: true
+
+  validate :check_creator_enrollable
+  def check_creator_enrollable
+    return unless check_creator_identity_enrollable
+
+    check_creator_multiple_enrollable
+  end
+
+  validate :check_teachers_enrollable
+  def check_teachers_enrollable
+    if competition.teacher_enroll_forbidden? && teacher_ids.present?
+      errors.add(:teacher_ids, :enroll_forbidden)
+      return
+    end
+
+    self.teacher_ids = teacher_ids.map(&:to_i)
+    all_teachers = creator.is_teacher? ? teacher_ids + [creator.id] : teacher_ids
+    all_teachers.uniq!
+
+    if all_teachers.size < competition.teacher_staff.minimum || all_teachers.size > competition.teacher_staff.maximum
+      errors.add(:teacher_ids, :invalid_count, minimum: competition.teacher_staff.minimum, maximum: competition.teacher_staff.maximum)
+      return
+    end
+
+    # 老师可多次报名,不检查
+    return unless competition.teacher_multiple_limited?
+
+    # 存在已报名老师
+    enrolled_teacher_members = competition.team_members.where(user_id: all_teachers)
+                                 .where.not(competition_team_id: team.id).includes(:user)
+    if enrolled_teacher_members.present?
+      errors.add(:teacher_ids, :enrolled, names: enrolled_teacher_members.map { |m| m.user.real_name }.join(','))
+      return
+    end
+  end
+
+  validate :check_members_enrollable
+  def check_members_enrollable
+    if competition.member_enroll_forbidden? && member_ids.present?
+      errors.add(:member_ids, :enroll_forbidden)
+      return
+    end
+
+    self.member_ids = member_ids.map(&:to_i)
+    all_members = creator.is_teacher? ? member_ids : member_ids + [creator.id]
+    all_members.uniq!
+
+    if all_members.size < competition.member_staff.minimum || all_members.size > competition.member_staff.maximum
+      errors.add(:member_ids, :invalid_count, minimum: competition.member_staff.minimum, maximum: competition.member_staff.maximum)
+      return
+    end
+
+    # 成员可多次报名,不检查
+    return unless competition.member_multiple_limited?
+
+    # 存在已报名成员
+    enrolled_members = competition.team_members.where(user_id: all_members)
+                         .where.not(competition_team_id: team.id).includes(:user)
+    if enrolled_members.present?
+      errors.add(:member_ids, :enrolled, names: enrolled_members.map { |m| m.user.real_name }.join(','))
+      return
+    end
+  end
+
+  private
+
+  # 竞赛是否限制了职业
+  def check_creator_identity_enrollable
+    if user.is_teacher? && competition.teacher_enroll_forbidden?
+      errors.add(:creator, :teacher_enroll_forbidden)
+      return false
+    elsif !user.is_teacher? && competition.member_enroll_forbidden?
+      errors.add(:creator, :member_enroll_forbidden)
+      return false
+    end
+
+    true
+  end
+
+  # 创建者是否能多次报名
+  def check_creator_multiple_enrollable
+    return unless competition.enrolled?(user)
+
+    if (user.is_teacher? && competition.teacher_multiple_limited?) || (!user.is_teacher? && competition.member_multiple_limited?)
+      errors.add(:creator, :enrolled)
+      return false
+    end
+
+    true
+  end
+end
\ No newline at end of file
diff --git a/app/models/competition.rb b/app/models/competition.rb
index 9a85bda58..f5d146ab4 100644
--- a/app/models/competition.rb
+++ b/app/models/competition.rb
@@ -1,10 +1,15 @@
 class Competition < ApplicationRecord
 
   has_many :competition_modules, dependent: :destroy
+  has_many :unhidden_competition_modules, -> { where(hidden: false) }, class_name: 'CompetitionModule'
+
   has_many :competition_stages, dependent: :destroy
   has_many :competition_stage_sections, dependent: :destroy
   has_one :current_stage_section, -> { where('end_time > NOW()') }, class_name: 'CompetitionStageSection'
+
+  has_many :competition_teams, dependent: :destroy
   has_many :team_members, dependent: :destroy
+
   has_many :competition_staffs, dependent: :destroy
   has_one :teacher_staff, -> { where(category: :teacher) }, class_name: 'CompetitionStaff'
   has_one :member_staff, -> { where.not(category: :teacher) }, class_name: 'CompetitionStaff'
@@ -28,6 +33,26 @@ class Competition < ApplicationRecord
     team_members.exists?(user_id: user.id)
   end
 
+  # 是否禁止教师报名
+  def teacher_enroll_forbidden?
+    teacher_staff.blank? || teacher_staff.maximum.zero?
+  end
+
+  # 是否禁止学生报名
+  def member_enroll_forbidden?
+    member_staff.blank? || member_staff.maximum.zero?
+  end
+
+  # 老师是否能多次报名
+  def teacher_multiple_limited?
+    teacher_staff.mutiple_limited?
+  end
+
+  # 队员是否能多次报名
+  def member_multiple_limited?
+    member_staff.mutiple_limited?
+  end
+
   private
 
   def create_competition_modules
diff --git a/app/models/competition_module_md_content.rb b/app/models/competition_module_md_content.rb
index cbf5a829a..9dfcfca84 100644
--- a/app/models/competition_module_md_content.rb
+++ b/app/models/competition_module_md_content.rb
@@ -2,4 +2,7 @@ class CompetitionModuleMdContent < ApplicationRecord
   belongs_to :competition_module
 
   has_many :attachments, as: :container, dependent: :destroy
+
+  validates :name, presence: true
+  validates :content, presence: true
 end
\ No newline at end of file
diff --git a/app/models/competition_team.rb b/app/models/competition_team.rb
index aa19db3b0..625b29421 100644
--- a/app/models/competition_team.rb
+++ b/app/models/competition_team.rb
@@ -1,8 +1,33 @@
 class CompetitionTeam < ApplicationRecord
+
+  CODE_CHARS = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z).freeze
+
   belongs_to :user
   belongs_to :competition
 
   has_many :team_members, dependent: :destroy
+  has_many :users, through: :team_members, source: :user
   has_many :members, -> { without_teachers }, class_name: 'TeamMember'
   has_many :teachers, -> { only_teachers }, class_name: 'TeamMember'
+
+  def group_team_type?
+    team_type.zero?
+  end
+
+  def personal_team_type?
+    team_type == 1
+  end
+
+  def en_team_type
+    group_team_type? ? 'group' : 'personal'
+  end
+
+  def generate_invite_code
+    code = CODE_CHARS.sample(6).join
+    while self.class.exists?(invite_code: code)
+      code = CODE_CHARS.sample(6).join
+    end
+    self.code = code
+    code
+  end
 end
\ No newline at end of file
diff --git a/app/models/team_member.rb b/app/models/team_member.rb
index 3909325e8..31890ea2e 100644
--- a/app/models/team_member.rb
+++ b/app/models/team_member.rb
@@ -5,4 +5,12 @@ class TeamMember < ApplicationRecord
 
   scope :only_teachers, -> { where(is_teacher: true) }
   scope :without_teachers, -> { where(is_teacher: false) }
+
+  def creator?
+    role == 1
+  end
+
+  def en_role
+    is_teacher? ? 'teacher' : 'member'
+  end
 end
\ No newline at end of file
diff --git a/app/services/competitions/join_team_service.rb b/app/services/competitions/join_team_service.rb
new file mode 100644
index 000000000..df889abe2
--- /dev/null
+++ b/app/services/competitions/join_team_service.rb
@@ -0,0 +1,35 @@
+class Competitions::JoinTeamService < ApplicationService
+  Error = Class.new(StandardError)
+
+  attr_reader :competition, :user, :params
+
+  def initialize(competition, user, params)
+    @competition = competition
+    @user        = user
+    @params      = params
+  end
+
+  def call
+    invite_code = params[:invite_code].to_s.strip
+    raise Error, '战队邀请码不能为空' if invite_code.blank?
+
+    is_teacher = user.is_teacher?
+    raise Error, '本竞赛的参赛者限定为:学生' if is_teacher && competition.teacher_enroll_forbidden?
+    raise Error, '本竞赛的参赛者限定为:教师' if !is_teacher && competition.member_enroll_forbidden?
+
+    team = competition.competition_teams.find_by(invite_code: invite_code)
+    raise Error, '战队邀请码无效' if team.blank?
+    raise Error, '您已加入该战队' if team.team_members.exists?(user_id: user.id)
+
+    enrolled = competition.team_members.exists?(user_id: user.id)
+    if enrolled && (is_teacher && competition.teacher_multiple_limited?) || (!is_teacher && competition.member_multiple_limited?)
+      raise Error, '您已加入其它战队'
+    end
+
+    raise Error, '该战队教师人数已满' if is_teacher && team.teachers.count == competition.teacher_staff.maximum
+    raise Error, '该战队队员人数已满' if !is_teacher && team.members.count == competition.member_staff.maximum
+
+    role = is_teacher ? 3 : 2
+    team.team_members.create!(competition_id: competition.id, user_id: user, role: role, is_teacher: is_teacher)
+  end
+end
\ No newline at end of file
diff --git a/app/services/competitions/save_team_service.rb b/app/services/competitions/save_team_service.rb
new file mode 100644
index 000000000..c134e70d7
--- /dev/null
+++ b/app/services/competitions/save_team_service.rb
@@ -0,0 +1,70 @@
+class Competitions::SaveTeamService < ApplicationService
+  attr_reader :competition, :team, :creator, :params
+
+  TEAM_MEMBER_ATTRIBUTES = %i[competition_id competition_team_id user_id role is_teacher created_at updated_at]
+
+  def initialize(team, params)
+    @team        = team
+    @competition = team.competition
+    @creator     = team.user
+    @params      = params
+  end
+
+  def call
+    Competitions::SaveTeamForm.new(form_params).validate!
+
+    new_record = team.new_record?
+    is_teacher = team.user.is_teacher?
+    ActiveRecord::Base.transaction do
+      team.generate_invite_code if new_record
+      team.name = params[:name].to_s.strip
+      team.save!
+
+      # 创建者
+      team.team_members.create!(user_id: creator.id, competition_id: competition.id, role: 1, is_teacher: is_teacher) if new_record
+
+      update_teacher_team_members!
+      update_member_team_members!
+    end
+  end
+
+  private
+
+  def update_teacher_team_members!
+    teacher_ids = Array.wrap(params[:teacher_ids]).map(:to_i)
+    old_teacher_ids = team.team_members.where(role: 3).pluck(:user_id)
+
+    destroy_teacher_ids = old_teacher_ids - teacher_ids
+    team.team_members.where(role: 3).where(user_id: destroy_teacher_ids).delete_all
+
+    new_teacher_ids = teacher_ids - old_teacher_ids
+    TeamMember.bulk_insert(*TEAM_MEMBER_ATTRIBUTES) do |worker|
+      base_attr = { competition_id: competition.id, competition_team_id: team.id, role: 3, is_teacher: true }
+      new_teacher_ids.each do |teacher_id|
+        next if teacher_id == creator.id
+        worker.add(base_attr.merge(user_id: teacher_id))
+      end
+    end
+  end
+
+  def update_member_team_members!
+    member_ids = Array.wrap(params[:member_ids]).map(:to_i)
+    old_member_ids = team.team_members.where(role: 2).pluck(:user_id)
+
+    destroy_member_ids = old_member_ids - member_ids
+    team.team_members.where(role: 2).where(user_id: destroy_member_ids).delete_all
+
+    new_member_ids = member_ids - old_member_ids
+    TeamMember.bulk_insert(*TEAM_MEMBER_ATTRIBUTES) do |worker|
+      base_attr = { competition_id: competition.id, competition_team_id: team.id, role: 2, is_teacher: false }
+      new_member_ids.each do |member_id|
+        next if member_id == creator.id
+        worker.add(base_attr.merge(user_id: member_id))
+      end
+    end
+  end
+
+  def form_params
+    params.merge(competition: competition, team: team, creator: creator)
+  end
+end
\ No newline at end of file
diff --git a/app/views/competitions/competition_modules/index.json.jbuilder b/app/views/competitions/competition_modules/index.json.jbuilder
new file mode 100644
index 000000000..decfcb415
--- /dev/null
+++ b/app/views/competitions/competition_modules/index.json.jbuilder
@@ -0,0 +1,7 @@
+
+json.modules do
+  json.array! @modules.each do |m|
+    json.extract! m, :id, :name, :position, :url
+  end
+end
+json.count @modules.size
diff --git a/app/views/competitions/competition_modules/show.json.jbuilder b/app/views/competitions/competition_modules/show.json.jbuilder
new file mode 100644
index 000000000..d47742cf0
--- /dev/null
+++ b/app/views/competitions/competition_modules/show.json.jbuilder
@@ -0,0 +1,8 @@
+json.extract! @module, :id, :name, :position, :url, :md_edit
+
+md = @module.competition_module_md_content
+if md.present?
+  json.md_name md.name
+  json.md_content md.content
+  json.created_at md.created_at.strftime('%Y-%m-%d %H:%M:%S')
+end
\ No newline at end of file
diff --git a/app/views/competitions/competition_teams/index.json.jbuilder b/app/views/competitions/competition_teams/index.json.jbuilder
new file mode 100644
index 000000000..86bb86a1c
--- /dev/null
+++ b/app/views/competitions/competition_teams/index.json.jbuilder
@@ -0,0 +1,23 @@
+json.count @count
+json.competition_teams do
+  json.array! @teams.each do |team|
+    json.extract! team, :id, :name, :invite_code
+    json.team_type team.en_team_type
+    json.school_name team.user.school_name
+
+    json.manage_permission current_user.id == team.user_id
+
+    json.creator do
+      json.partial! 'users/user_simple', user: team.user
+      json.role team.team_members.find(&:creator?).en_role
+    end
+
+    json.team_members do
+      json.array! team.team_members.each do |member|
+        json.partial! 'users/user_simple', user: member.user
+        json.user_id member.user_id
+        json.role member.en_role
+      end
+    end
+  end
+end
diff --git a/app/views/competitions/students/index.json.jbuilder b/app/views/competitions/students/index.json.jbuilder
new file mode 100644
index 000000000..73ccd7d2d
--- /dev/null
+++ b/app/views/competitions/students/index.json.jbuilder
@@ -0,0 +1,9 @@
+json.teachers do
+  json.array! @students.each do |student|
+    json.id student.id
+    json.name student.full_name
+    json.student_id student.student_id
+    json.school_name student.school_name
+    json.enrollable !current_competition.member_multiple_limited? || !@enrolled_map.key?(student.id)
+  end
+end
\ No newline at end of file
diff --git a/app/views/competitions/teachers/index.json.jbuilder b/app/views/competitions/teachers/index.json.jbuilder
new file mode 100644
index 000000000..4a8d2961d
--- /dev/null
+++ b/app/views/competitions/teachers/index.json.jbuilder
@@ -0,0 +1,9 @@
+json.teachers do
+  json.array! @teachers.each do |teacher|
+    json.id teacher.id
+    json.name teacher.full_name
+    json.identity teacher.identity
+    json.school_name teacher.school_name
+    json.enrollable !current_competition.teacher_multiple_limited? || !@enrolled_map.key?(teacher.id)
+  end
+end
\ No newline at end of file
diff --git a/config/locales/competitions/zh-CN.yml b/config/locales/competitions/zh-CN.yml
new file mode 100644
index 000000000..9c3ff2f1e
--- /dev/null
+++ b/config/locales/competitions/zh-CN.yml
@@ -0,0 +1,8 @@
+'zh-CN':
+  activerecord:
+    models:
+      competition_module_md_content: ''
+    attributes:
+      competition_module_md_content:
+        name: '标题'
+        content: '内容'
diff --git a/config/locales/forms/save_team_form.zh-CN.yml b/config/locales/forms/save_team_form.zh-CN.yml
new file mode 100644
index 000000000..106527687
--- /dev/null
+++ b/config/locales/forms/save_team_form.zh-CN.yml
@@ -0,0 +1,25 @@
+'zh-CN':
+  activemodel:
+    attributes:
+      competitions/save_team_form:
+        competition: ''
+        name: '战队名称'
+        creator: ''
+        teacher_ids: ''
+        member_ids: ''
+    errors:
+      models:
+        competitions/save_team_form:
+          attributes:
+            creator:
+              teacher_enroll_forbidden: "本竞赛的参赛者限定为:学生"
+              member_enroll_forbidden: "本竞赛的参赛者限定为:教师"
+            teacher_ids:
+              enroll_forbidden: "本竞赛的参赛者限定为:学生"
+              invalid_count: "教师数量应为%{minimum}~%{maximum}人"
+              enrolled: "教师 ${names} 已加入其它战队了"
+            member_ids:
+              enroll_forbidden: "本竞赛的参赛者限定为:教师"
+              invalid_count: "队员数量应为%{minimum}~%{maximum}人"
+              enrolled: "队员 ${names} 已加入其它战队了"
+
diff --git a/config/routes.rb b/config/routes.rb
index 71cfb8c55..dc521266c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -672,9 +672,13 @@ Rails.application.routes.draw do
 
     scope module: :competitions do
       resources :competitions, only: [:index, :show] do
-        resources :competition_modules, only: [:index, :show]
+        resources :competition_modules, only: [:index, :show, :update]
         resource :competition_staff
-        resources :competition_teams, only: [:index, :show]
+        resources :competition_teams, only: [:index, :show] do
+          post :join, on: :collection
+        end
+        resources :teachers, only: [:index]
+        resources :students, only: [:index]
       end
     end
   end