diff --git a/Gemfile b/Gemfile
index ac2778c59..7a37b7c44 100644
--- a/Gemfile
+++ b/Gemfile
@@ -98,3 +98,7 @@ gem 'aasm'
 gem 'enumerize'
 
 gem 'diffy'
+
+# oauth2
+gem 'omniauth', '~> 1.9.0'
+gem 'omniauth-oauth2', '~> 1.6.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index aabf3ffba..4b7902353 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -121,7 +121,7 @@ GEM
     grape-entity (0.7.1)
       activesupport (>= 4.0)
       multi_json (>= 1.3.2)
-    hashie (3.6.0)
+    hashie (3.5.7)
     htmlentities (4.3.4)
     httparty (0.16.2)
       multi_xml (>= 0.5.2)
@@ -179,6 +179,12 @@ GEM
       multi_json (~> 1.3)
       multi_xml (~> 0.5)
       rack (>= 1.2, < 3)
+    omniauth (1.9.0)
+      hashie (>= 3.4.6, < 3.7.0)
+      rack (>= 1.6.2, < 3)
+    omniauth-oauth2 (1.6.0)
+      oauth2 (~> 1.1)
+      omniauth (~> 1.9)
     pdfkit (0.8.4.1)
     popper_js (1.14.5)
     public_suffix (4.0.1)
@@ -381,6 +387,8 @@ DEPENDENCIES
   listen (>= 3.0.5, < 3.2)
   mysql2 (>= 0.4.4, < 0.6.0)
   oauth2
+  omniauth (~> 1.9.0)
+  omniauth-oauth2 (~> 1.6.0)
   pdfkit
   puma (~> 3.11)
   rack-cors
diff --git a/app/assets/javascripts/admins/laboratories/edit.js b/app/assets/javascripts/admins/laboratories/edit.js
new file mode 100644
index 000000000..63b26bbe0
--- /dev/null
+++ b/app/assets/javascripts/admins/laboratories/edit.js
@@ -0,0 +1,86 @@
+$(document).on('turbolinks:load', function() {
+  if ($('body.admins-laboratory-settings-show-page, body.admins-laboratory-settings-update-page').length > 0) {
+    var $container = $('.edit-laboratory-setting-container');
+    var $form = $container.find('.edit_laboratory');
+
+    $('.logo-item-left').on("change", 'input[type="file"]', function () {
+      var $fileInput = $(this);
+      var file = this.files[0];
+      var imageType = /image.*/;
+      if (file && file.type.match(imageType)) {
+        var reader = new FileReader();
+        reader.onload = function () {
+          var $box = $fileInput.parent();
+          $box.find('img').attr('src', reader.result).css('display', 'block');
+          $box.addClass('has-img');
+        };
+        reader.readAsDataURL(file);
+      } else {
+      }
+    });
+
+    createMDEditor('laboratory-footer-editor', { height: 200, placeholder: '请输入备案信息' });
+
+    $form.validate({
+      errorElement: 'span',
+      errorClass: 'danger text-danger',
+      errorPlacement:function(error,element){
+        if(element.parent().hasClass("input-group")){
+          element.parent().after(error);
+        }else{
+          element.after(error)
+        }
+      },
+      rules: {
+        identifier: {
+          required: true,
+          checkSite: true
+        },
+        name: {
+          required: true
+        }
+      }
+    });
+    $.validator.addMethod("checkSite",function(value,element,params){
+      var checkSite = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
+      return this.optional(element)||(checkSite.test(value + '.educoder.com'));
+    },"域名不合法!");
+
+    $form.on('click', '.submit-btn', function(){
+      $form.find('.submit-btn').attr('disabled', 'disabled');
+      $form.find('.error').html('');
+      var valid = $form.valid();
+
+      $('input[name="navbar[][name]"]').each(function(_, e){
+        var $ele = $(e);
+        if($ele.val() === undefined || $ele.val().length === 0){
+          $ele.addClass('danger text-danger');
+          valid = false;
+        } else {
+          $ele.removeClass('danger text-danger');
+        }
+      });
+
+      if(!valid) return;
+      $.ajax({
+        method: 'PATCH',
+        dataType: 'json',
+        url: $form.attr('action'),
+        data: new FormData($form[0]),
+        processData: false,
+        contentType: false,
+        success: function(data){
+          $.notify({ message: '保存成功' });
+          window.location.reload();
+        },
+        error: function(res){
+          var data = res.responseJSON;
+          $form.find('.error').html(data.message);
+        },
+        complete: function(){
+          $form.find('.submit-btn').attr('disabled', false);
+        }
+      });
+    })
+  }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/admins/laboratories/index.js b/app/assets/javascripts/admins/laboratories/index.js
new file mode 100644
index 000000000..abb7cb72d
--- /dev/null
+++ b/app/assets/javascripts/admins/laboratories/index.js
@@ -0,0 +1,164 @@
+$(document).on('turbolinks:load', function() {
+  if ($('body.admins-laboratories-index-page').length > 0) {
+    var $searchContainer = $('.laboratory-list-form');
+    var $searchForm = $searchContainer.find('form.search-form');
+    var $list = $('.laboratory-list-container');
+
+    // ============== 新建 ===============
+    var $modal = $('.modal.admin-create-laboratory-modal');
+    var $form = $modal.find('form.admin-create-laboratory-form');
+    var $schoolSelect = $modal.find('.school-select');
+
+    $form.validate({
+      errorElement: 'span',
+      errorClass: 'danger text-danger',
+      rules: {
+        school_id: {
+          required: true
+        }
+      },
+      messages: {
+        school_id: {
+          required: '请选择所属单位'
+        }
+      }
+    });
+
+    // modal ready fire
+    $modal.on('show.bs.modal', function () {
+      $schoolSelect.select2('val', ' ');
+    });
+
+    // ************** 学校选择 *************
+    var matcherFunc = function(params, data){
+      if ($.trim(params.term) === '') {
+        return data;
+      }
+      if (typeof data.text === 'undefined') {
+        return null;
+      }
+
+      if (data.name && data.name.indexOf(params.term) > -1) {
+        var modifiedData = $.extend({}, data, true);
+        return modifiedData;
+      }
+
+      // Return `null` if the term should not be displayed
+      return null;
+    };
+
+    var defineSchoolSelect = function(schools) {
+      $schoolSelect.select2({
+        theme: 'bootstrap4',
+        placeholder: '请选择单位',
+        minimumInputLength: 1,
+        data: schools,
+        templateResult: function (item) {
+          if(!item.id || item.id === '') return item.text;
+          return item.name;
+        },
+        templateSelection: function(item){
+          if (item.id) {
+            $('#school_id').val(item.id);
+          }
+          return item.name || item.text;
+        },
+        matcher: matcherFunc
+      });
+    }
+
+    $.ajax({
+      url: '/api/schools/for_option.json',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        defineSchoolSelect(data.schools);
+      }
+    });
+
+    $modal.on('click', '.submit-btn', function(){
+      $form.find('.error').html('');
+
+      if ($form.valid()) {
+        var url = $form.data('url');
+
+        $.ajax({
+          method: 'POST',
+          dataType: 'json',
+          url: url,
+          data: $form.serialize(),
+          success: function(){
+            $.notify({ message: '创建成功' });
+            $modal.modal('hide');
+
+            setTimeout(function(){
+              window.location.reload();
+            }, 500);
+          },
+          error: function(res){
+            var data = res.responseJSON;
+            $form.find('.error').html(data.message);
+          }
+        });
+      }
+    });
+
+    // ============= 添加管理员 ==============
+    var $addMemberModal = $('.admin-add-laboratory-user-modal');
+    var $addMemberForm = $addMemberModal.find('.admin-add-laboratory-user-form');
+    var $memberSelect = $addMemberModal.find('.laboratory-user-select');
+    var $laboratoryIdInput = $addMemberForm.find('input[name="laboratory_id"]')
+
+    $addMemberModal.on('show.bs.modal', function(event){
+      var $link = $(event.relatedTarget);
+      var laboratoryId = $link.data('laboratory-id');
+      $laboratoryIdInput.val(laboratoryId);
+
+      $memberSelect.select2('val', ' ');
+    });
+
+    $memberSelect.select2({
+      theme: 'bootstrap4',
+      placeholder: '请输入要添加的管理员姓名',
+      multiple: true,
+      minimumInputLength: 1,
+      ajax: {
+        delay: 500,
+        url: '/admins/users',
+        dataType: 'json',
+        data: function(params){
+          return { name: params.term };
+        },
+        processResults: function(data){
+          return { results: data.users }
+        }
+      },
+      templateResult: function (item) {
+        if(!item.id || item.id === '') return item.text;
+        return item.real_name;
+      },
+      templateSelection: function(item){
+        if (item.id) {
+        }
+        return item.real_name || item.text;
+      }
+    });
+
+    $addMemberModal.on('click', '.submit-btn', function(){
+      $addMemberForm.find('.error').html('');
+
+      var laboratoryId = $laboratoryIdInput.val();
+      var memberIds = $memberSelect.val();
+      if (laboratoryId && memberIds && memberIds.length > 0) {
+        $.ajax({
+          method: 'POST',
+          dataType: 'script',
+          url: '/admins/laboratories/' + laboratoryId + '/laboratory_user',
+          data: { user_ids: memberIds }
+        });
+      } else {
+        $addMemberModal.modal('hide');
+      }
+    });
+  }
+});
\ No newline at end of file
diff --git a/app/assets/stylesheets/admins/laboratories.scss b/app/assets/stylesheets/admins/laboratories.scss
new file mode 100644
index 000000000..ad5c8c5a8
--- /dev/null
+++ b/app/assets/stylesheets/admins/laboratories.scss
@@ -0,0 +1,99 @@
+.admins-laboratories-index-page {
+  .laboratory-list-table {
+    .member-container {
+      .laboratory-user {
+        display: flex;
+        justify-content: center;
+        flex-wrap: wrap;
+
+        .laboratory-user-item {
+          display: flex;
+          align-items: center;
+          height: 22px;
+          line-height: 22px;
+          padding: 2px 5px;
+          margin: 2px 2px;
+          border: 1px solid #91D5FF;
+          background-color: #E6F7FF;
+          color: #91D5FF;
+          border-radius: 4px;
+        }
+      }
+    }
+  }
+}
+.admins-laboratory-settings-show-page, .admins-laboratory-settings-update-page {
+  .edit-laboratory-setting-container {
+    .logo-item {
+      display: flex;
+
+      &-img {
+        display: block;
+        width: 80px;
+        height: 80px;
+      }
+
+      &-upload {
+        cursor: pointer;
+        position: absolute;
+        top: 0;
+        width: 80px;
+        height: 80px;
+        background: #F5F5F5;
+        border: 1px solid #E5E5E5;
+
+        &::before {
+          content: '';
+          position: absolute;
+          top: 27px;
+          left: 39px;
+          width: 2px;
+          height: 26px;
+          background: #E5E5E5;
+        }
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 39px;
+          left: 27px;
+          width: 26px;
+          height: 2px;
+          background: #E5E5E5;
+        }
+      }
+
+      &-left {
+        position: relative;
+        width: 80px;
+        height: 80px;
+
+        &.has-img {
+          .logo-item-upload {
+            display: none;
+          }
+
+          &:hover {
+            .logo-item-upload {
+              display: block;
+              background: rgba(145, 145, 145, 0.8);
+            }
+          }
+        }
+      }
+
+      &-right {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        color: #777777;
+        font-size: 12px;
+      }
+
+      &-title {
+        color: #23272B;
+        font-size: 14px;
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 1d6f89ec0..fed6ec280 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -183,20 +183,6 @@ class AccountsController < ApplicationController
   end
 
   private
-  def autologin_cookie_name
-    edu_setting('autologin_cookie_name') || 'autologin'
-  end
-
-  def logout_user
-    if User.current.logged?
-      if autologin = cookies.delete(autologin_cookie_name)
-        User.current.delete_autologin_token(autologin)
-      end
-      User.current.delete_session_token(session[:tk])
-      self.logged_user = nil
-    end
-    session[:user_id] = nil
-  end
 
   # type 事件类型 1:用户注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱, 5: 验证手机号是否有效 # 如果有新的继续后面加
   # login_type 1:手机类型 2:邮箱类型
diff --git a/app/controllers/admins/laboratories_controller.rb b/app/controllers/admins/laboratories_controller.rb
new file mode 100644
index 000000000..e393c6677
--- /dev/null
+++ b/app/controllers/admins/laboratories_controller.rb
@@ -0,0 +1,32 @@
+class Admins::LaboratoriesController < Admins::BaseController
+  def index
+    params[:sort_by] = params[:sort_by].presence || 'id'
+    params[:sort_direction] = params[:sort_direction].presence || 'desc'
+
+    laboratories = Admins::LaboratoryQuery.call(params)
+    @laboratories = paginate laboratories.preload(:school, :laboratory_users)
+  end
+
+  def create
+    Admins::CreateLaboratoryService.call(create_params)
+    render_ok
+  rescue Admins::CreateLaboratoryService::Error => ex
+    render_error(ex.message)
+  end
+
+  def destroy
+    current_laboratory.destroy!
+
+    render_delete_success
+  end
+
+  private
+
+  def current_laboratory
+    @_current_laboratory ||= Laboratory.find(params[:id])
+  end
+
+  def create_params
+    params.permit(:school_id)
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/admins/laboratory_settings_controller.rb b/app/controllers/admins/laboratory_settings_controller.rb
new file mode 100644
index 000000000..f9676bfd3
--- /dev/null
+++ b/app/controllers/admins/laboratory_settings_controller.rb
@@ -0,0 +1,20 @@
+class Admins::LaboratorySettingsController < Admins::BaseController
+  def show
+    @laboratory = current_laboratory
+  end
+
+  def update
+    Admins::SaveLaboratorySettingService.call(current_laboratory, form_params)
+    render_ok
+  end
+
+  private
+
+  def current_laboratory
+    @_current_laboratory ||= Laboratory.find(params[:laboratory_id])
+  end
+
+  def form_params
+    params.permit(:identifier, :name, :nav_logo, :login_logo, :tab_logo, :footer, navbar: %i[name link hidden])
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/admins/laboratory_users_controller.rb b/app/controllers/admins/laboratory_users_controller.rb
new file mode 100644
index 000000000..36e389a3e
--- /dev/null
+++ b/app/controllers/admins/laboratory_users_controller.rb
@@ -0,0 +1,19 @@
+class Admins::LaboratoryUsersController < Admins::BaseController
+  helper_method :current_laboratory
+
+  def create
+    Admins::AddLaboratoryUserService.call(current_laboratory, params.permit(user_ids: []))
+    current_laboratory.reload
+  end
+
+  def destroy
+    @laboratory_user = current_laboratory.laboratory_users.find_by(user_id: params[:user_id])
+    @laboratory_user.destroy! if @laboratory_user.present?
+  end
+
+  private
+
+  def current_laboratory
+    @_current_laboratory ||= Laboratory.find(params[:laboratory_id])
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b539a0c68..5c93b08b9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -7,6 +7,8 @@ class ApplicationController < ActionController::Base
 	include ControllerRescueHandler
 	include GitHelper
 	include LoggerHelper
+	include LaboratoryHelper
+	include LoginHelper
 
 	protect_from_forgery prepend: true, unless: -> { request.format.json? }
 
@@ -234,12 +236,6 @@ class ApplicationController < ActionController::Base
 		# end
 	end
 
-	def start_user_session(user)
-		session[:user_id] = user.id
-		session[:ctime] = Time.now.utc.to_i
-		session[:atime] = Time.now.utc.to_i
-	end
-
 	def user_setup
 		# reacct静态资源加载不需要走这一步
 		return if params[:controller] == "main"
@@ -280,17 +276,6 @@ class ApplicationController < ActionController::Base
 		# User.current = User.find 81403
 	end
 
-	# Sets the logged in user
-	def logged_user=(user)
-		reset_session
-		if user && user.is_a?(User)
-			User.current = user
-			start_user_session(user)
-		else
-			User.current = User.anonymous
-		end
-	end
-
 	# Returns the current user or nil if no user is logged in
 	# and starts a session if needed
 	def find_current_user
@@ -306,10 +291,6 @@ class ApplicationController < ActionController::Base
 		end
 	end
 
-	def autologin_cookie_name
-		edu_setting('autologin_cookie_name').presence || 'autologin'
-	end
-
 	def try_to_autologin
 		if cookies[autologin_cookie_name]
 			# auto-login feature starts a new session
@@ -620,22 +601,6 @@ class ApplicationController < ActionController::Base
 		cookies[:fileDownload] = true
 	end
 
-	def set_autologin_cookie(user)
-		token = Token.get_or_create_permanent_login_token(user, "autologin")
-		cookie_options = {
-			:value => token.value,
-			:expires => 1.month.from_now,
-			:path => '/',
-			:secure => false,
-			:httponly => true
-		}
-		if edu_setting('cookie_domain').present?
-			cookie_options = cookie_options.merge(domain: edu_setting('cookie_domain'))
-		end
-		cookies[autologin_cookie_name] = cookie_options
-		logger.info("cookies is #{cookies}")
-	end
-
 	# 149课程的评审用户数据创建(包含创建课堂学生)
 	def open_class_user
 		user = User.find_by(login: "OpenClassUser")
diff --git a/app/controllers/bind_users_controller.rb b/app/controllers/bind_users_controller.rb
new file mode 100644
index 000000000..354b2993b
--- /dev/null
+++ b/app/controllers/bind_users_controller.rb
@@ -0,0 +1,22 @@
+class BindUsersController < ApplicationController
+  before_action :require_login
+
+  def create
+    user = CreateBindUserService.call(current_user, create_params)
+    successful_authentication(user) if user.id != current_user.id
+
+    render_ok
+  rescue ApplicationService::Error => ex
+    render_error(ex.message)
+  end
+
+  def new_user
+    current_user
+  end
+
+  private
+
+  def create_params
+    params.permit(:username, :password, :type, :not_bind)
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/concerns/laboratory_helper.rb b/app/controllers/concerns/laboratory_helper.rb
new file mode 100644
index 000000000..fbb18b36d
--- /dev/null
+++ b/app/controllers/concerns/laboratory_helper.rb
@@ -0,0 +1,15 @@
+module LaboratoryHelper
+  extend ActiveSupport::Concern
+
+  included do
+    helper_method :default_setting
+  end
+
+  def current_laboratory
+    @_current_laboratory ||= (Laboratory.find_by_subdomain(request.subdomain) || Laboratory.find(1))
+  end
+
+  def default_setting
+    @_default_setting ||= LaboratorySetting.find_by(laboratory_id: 1)
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/concerns/login_helper.rb b/app/controllers/concerns/login_helper.rb
new file mode 100644
index 000000000..e94cf8a21
--- /dev/null
+++ b/app/controllers/concerns/login_helper.rb
@@ -0,0 +1,69 @@
+module LoginHelper
+  extend ActiveSupport::Concern
+
+  def edu_setting(name)
+    EduSetting.get(name)
+  end
+
+  def autologin_cookie_name
+    edu_setting('autologin_cookie_name').presence || 'autologin'
+  end
+
+  def set_autologin_cookie(user)
+    token = Token.get_or_create_permanent_login_token(user, "autologin")
+    cookie_options = {
+      :value => token.value,
+      :expires => 1.month.from_now,
+      :path => '/',
+      :secure => false,
+      :httponly => true
+    }
+    if edu_setting('cookie_domain').present?
+      cookie_options = cookie_options.merge(domain: edu_setting('cookie_domain'))
+    end
+    cookies[autologin_cookie_name] = cookie_options
+    Rails.logger.info("cookies is #{cookies}")
+  end
+
+  def successful_authentication(user)
+    Rails.logger.info("id: #{user&.id} Successful authentication start: '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}")
+    # Valid user
+    self.logged_user = user
+
+    # generate a key and set cookie if autologin
+    set_autologin_cookie(user)
+
+    UserAction.create(action_id: user&.id, action_type: 'Login', user_id: user&.id, ip: request.remote_ip)
+    user.update_column(:last_login_on, Time.now)
+    # 注册完成后有一天的试用申请(先去掉)
+    # UserDayCertification.create(user_id: user.id, status: 1)
+  end
+
+  def logout_user
+    if User.current.logged?
+      if autologin = cookies.delete(autologin_cookie_name)
+        User.current.delete_autologin_token(autologin)
+      end
+      User.current.delete_session_token(session[:tk])
+      self.logged_user = nil
+    end
+    session[:user_id] = nil
+  end
+
+  # Sets the logged in user
+  def logged_user=(user)
+    reset_session
+    if user && user.is_a?(User)
+      User.current = user
+      start_user_session(user)
+    else
+      User.current = User.anonymous
+    end
+  end
+
+  def start_user_session(user)
+    session[:user_id] = user.id
+    session[:ctime] = Time.now.utc.to_i
+    session[:atime] = Time.now.utc.to_i
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb
index a3b20c598..0b3e35519 100644
--- a/app/controllers/courses_controller.rb
+++ b/app/controllers/courses_controller.rb
@@ -35,7 +35,7 @@ class CoursesController < ApplicationController
                                          :transfer_to_course_group, :delete_from_course, :export_member_scores_excel,
                                          :search_users, :add_students_by_search, :get_historical_courses, :add_teacher_popup,
                                          :add_teacher, :export_couser_info, :export_member_act_score,
-                                         :update_informs, :new_informs, :delete_informs]
+                                         :update_informs, :new_informs, :delete_informs, :switch_to_student]
   before_action :admin_allowed, only: [:set_invite_code_halt, :set_public_or_private, :change_course_admin,
                                        :set_course_group, :create_group_by_importing_file,
                                        :update_task_position, :tasks_list]
@@ -552,7 +552,7 @@ class CoursesController < ApplicationController
   def change_member_role
     tip_exception("请至少选择一个角色") if params[:roles].blank?
     tip_exception("不能具有老师、助教两种角色") if params[:roles].include?("PROFESSOR") && params[:roles].include?("ASSISTANT_PROFESSOR")
-    tip_exception("管理员不能切换为助教或老师") if @user_course_identity == Course::CREATOR &&
+    tip_exception("管理员不能切换为助教或老师") if params[:user_id].to_i == @course.tea_id &&
       (params[:roles].include?("PROFESSOR") || params[:roles].include?("ASSISTANT_PROFESSOR"))
 
     course_members = @course.course_members.where(user_id: params[:user_id])
@@ -681,13 +681,19 @@ class CoursesController < ApplicationController
         course_member = @course.course_members.find_by!(user_id: current_user.id, is_active: 1)
         tip_exception("切换失败") if course_member.STUDENT?
 
-        course_student = CourseMember.find_by!(user_id: current_user.id, role: %i[STUDENT], course_id: @course.id)
-        course_member.update_attributes(is_active: 0)
-        course_student.update_attributes(is_active: 1)
+        course_student = CourseMember.find_by(user_id: current_user.id, role: %i[STUDENT], course_id: @course.id)
+        course_member.update_attributes!(is_active: 0)
+        if course_student
+          course_student.update_attributes!(is_active: 1)
+        else
+          # 学生身份不存在则创建
+          CourseMember.create!(user_id: current_user.id, role: 4, course_id: @course.id)
+          CourseAddStudentCreateWorksJob.perform_later(@course.id, [current_user.id])
+        end
         normal_status(0, "切换成功")
       rescue => e
         uid_logger_error("switch_to_student error: #{e.message}")
-        tip_exception("切换失败")
+        tip_exception(e.message)
         raise ActiveRecord::Rollback
       end
     end
@@ -1127,7 +1133,7 @@ class CoursesController < ApplicationController
 
   def top_banner
     @user = current_user
-    @is_teacher = @user_course_identity < Course::STUDENT
+    @switch_student = Course::BUSINESS < @user_course_identity && @user_course_identity < Course::STUDENT
     @is_student = @user_course_identity == Course::STUDENT
     @course.increment!(:visits)
   end
diff --git a/app/controllers/exercise_questions_controller.rb b/app/controllers/exercise_questions_controller.rb
index 9eeba6adc..aacef6bc7 100644
--- a/app/controllers/exercise_questions_controller.rb
+++ b/app/controllers/exercise_questions_controller.rb
@@ -619,7 +619,7 @@ class ExerciseQuestionsController < ApplicationController
                 :status => 0
             }
             ExerciseShixunAnswer.create(ex_shixun_option)
-            new_obj_score = @c_score
+            new_obj_score = ex_obj_score + @c_score
           end
           total_scores = new_obj_score + ex_subj_score
           if total_scores < 0.0
diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb
index bc0da8ffc..9979ae48f 100644
--- a/app/controllers/exercises_controller.rb
+++ b/app/controllers/exercises_controller.rb
@@ -694,6 +694,7 @@ class ExercisesController < ApplicationController
 
   #首页批量或单独 立即发布,应是跳出弹窗,设置开始时间和截止时间。
   def publish
+    group_ids = params[:group_ids]&.reject(&:blank?)
     if params[:detail].blank?
       tip_exception("缺少截止时间参数") if params[:end_time].blank?
       tip_exception("截止时间不能早于当前时间") if params[:end_time] <= strf_time(Time.now)
@@ -701,7 +702,6 @@ class ExercisesController < ApplicationController
         @course.end_date.present? && params[:end_time] > strf_time(@course.end_date.end_of_day)
     else
       group_end_times = params[:group_end_times].reject(&:blank?).map{|time| time.to_time}
-      group_ids = params[:group_ids].reject(&:blank?)
       tip_exception("缺少截止时间参数") if group_end_times.blank?
       tip_exception("截止时间和分班参数的个数不一致") if group_end_times.length != group_ids.length
       group_end_times.each do |time|
@@ -978,7 +978,8 @@ class ExercisesController < ApplicationController
               :status => nil,
               :commit_status => 0,
               :objective_score => 0.0,
-              :subjective_score => -1.0
+              :subjective_score => -1.0,
+              :commit_method => 0
           }
           redo_exercise_users = @exercise_users.exercise_commit_users(user_ids)
           redo_exercise_users.update_all(redo_option)
@@ -1102,10 +1103,11 @@ class ExercisesController < ApplicationController
     ActiveRecord::Base.transaction do
       begin
         can_commit_exercise = false
-        Rails.logger.info("######____________params[:commit_method]_________################{params[:commit_method]}")
-        if (@user_course_identity > Course::ASSISTANT_PROFESSOR) && params[:commit_method].present?  #为学生时
+        user_left_time = nil
+        if @user_course_identity > Course::ASSISTANT_PROFESSOR  #为学生时
           if params[:commit_method].to_i == 2   #自动提交时
             user_left_time = get_exercise_left_time(@exercise,current_user)
+            Rails.logger.info("######__________auto_commit_user_left_time_________################{user_left_time}")
             if user_left_time.to_i <= 0
               can_commit_exercise = true
             end
@@ -1130,10 +1132,10 @@ class ExercisesController < ApplicationController
             CommitExercsieNotifyJobJob.perform_later(@exercise.id, current_user.id)
             normal_status(0,"试卷提交成功!")
           else
-            normal_status(-1,"提交失败,请重试!")
+            normal_status(-2,"#{user_left_time.to_i}")
           end
         else
-          normal_status(-1,"提交失败,请重试!")
+          normal_status(-1,"提交失败,当前用户不为课堂学生!")
         end
       rescue Exception => e
         uid_logger_error(e.message)
@@ -1150,7 +1152,7 @@ class ExercisesController < ApplicationController
         # 1 老师权限,0 学生权限
         @is_teacher_or = (@user_course_identity < Course::STUDENT) ? 1 : 0
         @student_status = 2
-        @exercise_questions = @exercise.exercise_questions.includes(:exercise_shixun_challenges,:exercise_standard_answers,:exercise_answers,:exercise_shixun_answers).order("question_number ASC")
+        @exercise_questions = @exercise.exercise_questions.includes(:exercise_shixun_challenges,:exercise_standard_answers,:exercise_answers,:exercise_shixun_answers,:exercise_answer_comments).order("question_number ASC")
         @question_status = []
         get_exercise_status = @exercise.get_exercise_status(current_user)  #当前用户的试卷状态
         @ex_answer_status = @exercise.get_exercise_status(@ex_user&.user)  #当前试卷用户的试卷状态
@@ -1321,7 +1323,7 @@ class ExercisesController < ApplicationController
         end
       rescue Exception => e
         uid_logger_error(e.message)
-        tip_exception("页面调用失败!")
+        tip_exception(e.message)
         raise ActiveRecord::Rollback
       end
     end
@@ -1707,9 +1709,9 @@ class ExercisesController < ApplicationController
         ques_number = q.question_number
       end
       if q.question_type != Exercise::PRACTICAL
-        ques_vote = q.exercise_answers.search_exercise_answer("user_id",user_id)
+        ques_vote = q.exercise_answers.select{|answer| answer.user_id == user_id}
       else
-        ques_vote = q.exercise_shixun_answers.search_shixun_answers("user_id",user_id)
+        ques_vote = q.exercise_shixun_answers.select{|answer| answer.user_id == user_id}
       end
       ques_status = 0
       if ques_vote.present?
diff --git a/app/controllers/homework_commons_controller.rb b/app/controllers/homework_commons_controller.rb
index 9941e1d42..60cf2d6c5 100644
--- a/app/controllers/homework_commons_controller.rb
+++ b/app/controllers/homework_commons_controller.rb
@@ -159,13 +159,20 @@ class HomeworkCommonsController < ApplicationController
         end
 
         # 作品状态 0: 未提交, 1 按时提交, 2 延迟提交
-        unless params[:work_status].blank?
-          @student_works = @student_works.where(work_status: params[:work_status])
+        if params[:work_status].present?
+          params_work_status = request.get? ? params[:work_status].split(",") : params[:work_status]
+          work_status = params_work_status.map{|status| status.to_i}
+          all_student_works = @student_works.left_joins(:myshixun)
+          @student_works = all_student_works.where(work_status: work_status)
+
+          @student_works = @student_works.or(all_student_works.where(work_status: 0)).or(all_student_works.where(myshixuns: {status: 0})) if work_status.include?(3)
+          @student_works = @student_works.or(all_student_works.where(myshixuns: {status: 1})) if work_status.include?(4)
         end
 
         # 分班情况
         unless params[:course_group].blank?
-          group_user_ids = @course.students.where(course_group_id: params[:course_group]).pluck(:user_id)
+          group_ids = request.get? ? params[:course_group].split(",") : params[:course_group]
+          group_user_ids = @course.students.where(course_group_id: group_ids).pluck(:user_id)
           # 有分组只可能是老师身份查看列表
           @student_works = @student_works.where(user_id: group_user_ids)
         end
@@ -477,7 +484,7 @@ class HomeworkCommonsController < ApplicationController
 
               publish_time = setting[:publish_time] == "" ? Time.now : setting[:publish_time]
               # 截止时间为空时取发布时间后一个月
-              end_time = setting[:end_time] == "" ? Time.at(publish_time.to_time.to_i+30*24*3600) : setting[:end_time]
+              end_time = setting[:end_time]
               HomeworkGroupSetting.where(homework_common_id: @homework.id, course_group_id: setting[:group_id]).
                   update_all(publish_time: publish_time, end_time: end_time)
               setting_group_ids << setting[:group_id]
@@ -1044,6 +1051,7 @@ class HomeworkCommonsController < ApplicationController
 
   def publish_homework
     tip_exception("请至少选择一个分班") if params[:group_ids].blank? && @course.course_groups.size != 0
+    group_ids = params[:group_ids]&.reject(&:blank?)
     if params[:detail].blank?
       tip_exception("缺少截止时间参数") if params[:end_time].blank?
       tip_exception("截止时间不能早于当前时间") if params[:end_time] <= strf_time(Time.now)
@@ -1051,7 +1059,6 @@ class HomeworkCommonsController < ApplicationController
         @course.end_date.present? && params[:end_time] > strf_time(@course.end_date.end_of_day)
     else
       group_end_times = params[:group_end_times].reject(&:blank?).map{|time| time.to_time}
-      group_ids = params[:group_ids].reject(&:blank?)
       tip_exception("缺少截止时间参数") if group_end_times.blank?
       tip_exception("截止时间和分班参数的个数不一致") if group_end_times.length != group_ids.length
       group_end_times.each do |time|
@@ -1165,7 +1172,7 @@ class HomeworkCommonsController < ApplicationController
       # 可立即截止的分班:统一设置则是用户管理的所有分班,否则是当前用户管理的分班中已发布且未截止的
       charge_group_ids = @course.charge_group_ids(@current_user)  # 当前用户管理的分班
       group_ids = @homework.unified_setting ? charge_group_ids :
-                      @homework.homework_group_settings.where(course_group_id: charge_group_ids).none_end.pluck(:course_group_id)
+                      @homework.homework_group_settings.where(course_group_id: charge_group_ids).published_no_end.pluck(:course_group_id)
       @course_groups = @course.course_groups.where(id: group_ids)
     else
       tip_exception("没有可截止的分班")
diff --git a/app/controllers/oauth/base_controller.rb b/app/controllers/oauth/base_controller.rb
new file mode 100644
index 000000000..e2eb26a2a
--- /dev/null
+++ b/app/controllers/oauth/base_controller.rb
@@ -0,0 +1,20 @@
+class Oauth::BaseController < ActionController::Base
+  include RenderHelper
+  include LoginHelper
+
+  skip_before_action :verify_authenticity_token
+
+  private
+
+  def session_user_id
+    session[:user_id]
+  end
+
+  def current_user
+    @_current_user ||= User.find_by(id: session_user_id)
+  end
+
+  def auth_hash
+    request.env['omniauth.auth']
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/oauth/qq_controller.rb b/app/controllers/oauth/qq_controller.rb
new file mode 100644
index 000000000..4b9a46443
--- /dev/null
+++ b/app/controllers/oauth/qq_controller.rb
@@ -0,0 +1,9 @@
+class Oauth::QQController < Oauth::BaseController
+  def create
+    user, new_user = Oauth::CreateOrFindQqAccountService.call(current_user, auth_hash)
+
+    successful_authentication(user)
+
+    render_ok(new_user: new_user)
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/oauth/wechat_controller.rb b/app/controllers/oauth/wechat_controller.rb
new file mode 100644
index 000000000..6c0c53eb6
--- /dev/null
+++ b/app/controllers/oauth/wechat_controller.rb
@@ -0,0 +1,11 @@
+class Oauth::WechatController < Oauth::BaseController
+  def create
+    user, new_user = Oauth::CreateOrFindWechatAccountService.call(current_user ,params)
+
+    successful_authentication(user)
+
+    render_ok(new_user: new_user)
+  rescue Oauth::CreateOrFindWechatAccountService::Error => ex
+    render_error(ex.message)
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb
index 2259907e4..da5917e1b 100644
--- a/app/controllers/polls_controller.rb
+++ b/app/controllers/polls_controller.rb
@@ -254,6 +254,7 @@ class PollsController < ApplicationController
 
   #首页批量或单独 立即发布,应是跳出弹窗,设置开始时间和截止时间。
   def publish
+    group_ids = params[:group_ids]&.reject(&:blank?)
     if params[:detail].blank?
       tip_exception("缺少截止时间参数") if params[:end_time].blank?
       tip_exception("截止时间不能早于当前时间") if params[:end_time] <= strf_time(Time.now)
@@ -261,7 +262,6 @@ class PollsController < ApplicationController
         @course.end_date.present? && params[:end_time] > strf_time(@course.end_date.end_of_day)
     else
       group_end_times = params[:group_end_times].reject(&:blank?).map{|time| time.to_time}
-      group_ids = params[:group_ids].reject(&:blank?)
       tip_exception("缺少截止时间参数") if group_end_times.blank?
       tip_exception("截止时间和分班参数的个数不一致") if group_end_times.length != group_ids.length
       group_end_times.each do |time|
diff --git a/app/controllers/question_banks_controller.rb b/app/controllers/question_banks_controller.rb
index 60b9a807c..ddb0f3ce1 100644
--- a/app/controllers/question_banks_controller.rb
+++ b/app/controllers/question_banks_controller.rb
@@ -90,23 +90,45 @@ class QuestionBanksController < ApplicationController
   def send_to_course
     banks = @object_type.classify.constantize.where(id: params[:object_id])
     course = current_user.manage_courses.find_by!(id: params[:course_id])
+    task_ids = []
+    homework_type = ""
+    container_type = ""
     banks.each do |bank|
       case @object_type
       when 'HomeworkBank' # 作业
-        quote_homework_bank bank, course
+        task = quote_homework_bank bank, course
+        homework_type = task.homework_type
       when 'ExerciseBank'
-        if bank.container_type == 'Exercise' # 试卷
-          quote_exercise_bank bank, course
+        container_type = bank.container_type
+        if container_type == 'Exercise' # 试卷
+          task = quote_exercise_bank bank, course
         else # 问卷
-          quote_poll_bank bank, course
+          task = quote_poll_bank bank, course
         end
       when 'GtaskBank'
-        quote_gtask_bank bank, course
+        task = quote_gtask_bank bank, course
       when 'GtopicBank'
-        quote_gtopic_bank bank, course
+        task = quote_gtopic_bank bank, course
       end
+      task_ids << task.id if task
     end
-    normal_status("发送成功")
+
+    case @object_type
+    when 'HomeworkBank' # 作业
+      category_id = course.course_modules.find_by(module_type: homework_type == "normal" ? "common_homework" : "group_homework")&.id
+    when 'ExerciseBank'
+      if container_type == 'Exercise' # 试卷
+        category_id = course.course_modules.find_by(module_type: "exercise")&.id
+      else # 问卷
+        category_id = course.course_modules.find_by(module_type: "poll")&.id
+      end
+    when 'GtaskBank'
+      category_id = course.course_modules.find_by(module_type: "graduation")&.id
+    when 'GtopicBank'
+      category_id = course.course_modules.find_by(module_type: "graduation")&.id
+    end
+
+    render :json => {task_ids: task_ids, category_id: category_id, status: 0, message: "发送成功"}
   end
 
   def destroy
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
new file mode 100644
index 000000000..ce5481147
--- /dev/null
+++ b/app/controllers/settings_controller.rb
@@ -0,0 +1,5 @@
+class SettingsController < ApplicationController
+  def show
+    @laboratory = current_laboratory
+  end
+end
\ No newline at end of file
diff --git a/app/forms/users/update_password_form.rb b/app/forms/users/update_password_form.rb
index 023caa40f..4da341839 100644
--- a/app/forms/users/update_password_form.rb
+++ b/app/forms/users/update_password_form.rb
@@ -4,5 +4,4 @@ class Users::UpdatePasswordForm
   attr_accessor :password, :old_password
 
   validates :password, presence: true
-  validates :old_password, presence: true
 end
\ No newline at end of file
diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb
index fb7bd1a88..9afbdd3af 100644
--- a/app/helpers/courses_helper.rb
+++ b/app/helpers/courses_helper.rb
@@ -62,7 +62,7 @@ module CoursesHelper
       course_board = course.course_board
       "/courses/#{course.id}/boards/#{course_board.id}"
     when "course_group"
-      "/courses/#{course.id}/students"
+      "/courses/#{course.id}/course_groups"
     end
   end
 
diff --git a/app/helpers/exercises_helper.rb b/app/helpers/exercises_helper.rb
index ef9261990..e13f754be 100644
--- a/app/helpers/exercises_helper.rb
+++ b/app/helpers/exercises_helper.rb
@@ -10,9 +10,9 @@ module ExercisesHelper
     exercise_obj_status.each do |q|
       q_type = q.question_type
       if q_type == Exercise::PRACTICAL
-        answers_content = q.exercise_shixun_answers.search_shixun_answers("user_id",user_id)
+        answers_content = q.exercise_shixun_answers.select{|answer| answer.user_id == user_id}
       else
-        answers_content = q.exercise_answers.search_answer_users("user_id",user_id)
+        answers_content = q.exercise_answers.select{|answer| answer.user_id == user_id}
       end
 
       if q_type <= Exercise::JUDGMENT
@@ -40,7 +40,7 @@ module ExercisesHelper
           ques_score = 0.0
         end
       else
-        ques_score = answers_content.score_reviewed.select(:score).pluck(:score).sum
+        ques_score = answers_content.select{|answer| answer.score >= 0.0}.pluck(:score).sum
       end
 
       if ques_score >= q.question_score  #满分作答为正确
@@ -64,7 +64,7 @@ module ExercisesHelper
     exercise_sub_status = exercise_questions.find_by_custom("question_type",Exercise::SUBJECTIVE) #主观题
     @ex_sub_array = []   #主观题的已答/未答
     exercise_sub_status.each do |s|
-      sub_answer = s.exercise_answers.search_answer_users("user_id",user_id)  #主观题只有一个回答
+      sub_answer = s.exercise_answers.select{|answer| answer.user_id == user_id}  #主观题只有一个回答
       if sub_answer.present? && sub_answer.first.score >= 0.0
         if s.question_score <= sub_answer.first.score
           stand_status = 1
@@ -772,12 +772,12 @@ module ExercisesHelper
     question_comment = []
     # user_score_pre = nil
     if ques_type == 5
-      exercise_answers = q.exercise_shixun_answers.search_shixun_answers("user_id",ex_answerer_id)
+      exercise_answers = q.exercise_shixun_answers.select{|answer| answer.user_id == ex_answerer_id}
     else
-      exercise_answers = q.exercise_answers.search_exercise_answer("user_id",ex_answerer_id)  #试卷用户的回答
+      exercise_answers = q.exercise_answers.select{|answer| answer.user_id == ex_answerer_id}  #试卷用户的回答
     end
     if student_status == 2   #当前为老师,或为学生且已提交
-      user_score_pre = exercise_answers.score_reviewed
+      user_score_pre = exercise_answers.select{|answer| answer.score >= 0.0}
       if ques_type == 4  #主观题时,且没有大于0的分数时,为空
         user_score = user_score_pre.present? ? user_score_pre.pluck(:score).sum : nil
       elsif ques_type == 5 || ques_type == 3
@@ -829,7 +829,7 @@ module ExercisesHelper
 
     if ex_type == 4  #填空题/主观题/实训题有评论的
       q_answer_id = exercise_answers.present? ? exercise_answers.first.id : nil
-      question_comment = q.exercise_answer_comments.search_answer_comments("exercise_answer_id",q_answer_id)
+      question_comment = q.exercise_answer_comments.select{|comment| comment.exercise_answer_id == q_answer_id}
     end
     {
         "user_score": (user_score.present? ? user_score.round(1).to_s : nil),
diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb
index 05b1b2f8b..534a78dc1 100644
--- a/app/helpers/export_helper.rb
+++ b/app/helpers/export_helper.rb
@@ -286,10 +286,18 @@ module ExportHelper
     @user_columns = []
     ques_type_boolean = question_types.include?(4)
     if ques_type_boolean  #仅存在主观题或客观题的时候
-      @table_columns = @table_columns + %w(客观题成绩 主观题成绩 最终成绩 开始答题时间 提交时间)
+      @table_columns = @table_columns + %w(客观题成绩 主观题成绩 最终成绩)
     else
-      @table_columns = @table_columns + %w(最终成绩 开始答题时间 提交时间)
+      @table_columns = @table_columns + %w(最终成绩)
     end
+    for i in 1 .. exercise.exercise_questions.size
+      @table_columns = @table_columns + ["第#{i}题"]
+    end
+
+    @table_columns = @table_columns + %w(开始答题时间 提交时间)
+
+    questions = exercise.exercise_questions.includes(:exercise_answers,:exercise_shixun_answers).order("question_number ASC")
+
     export_ex_users.includes(user: :user_extension).each_with_index do |e_user,index|
       user_info = e_user.user
       member = course.students.find_by_user_id(e_user.user_id)
@@ -312,11 +320,36 @@ module ExportHelper
       user_option = [index+1,user_login,user_real_name, user_mail,
                      user_student_id,user_course,user_commit_stu]
       if ques_type_boolean
-        other_user_option = [user_obj_score,user_suj_score,user_score,user_start_time,user_end_time]
+        other_user_option = [user_obj_score,user_suj_score,user_score]
       else
-        other_user_option = [user_score,user_start_time,user_end_time]
+        other_user_option = [user_score]
       end
-      user_option = user_option + other_user_option
+
+      time_option = [user_start_time,user_end_time]
+
+      score_option = []
+      questions.each do |q|
+        q_type = q.question_type
+        if q_type == Exercise::PRACTICAL
+          answers_content = q.exercise_shixun_answers.select{|answer| answer.user_id == e_user.user_id}
+        else
+          answers_content = q.exercise_answers.select{|answer| answer.user_id == e_user.user_id}
+        end
+
+        if q_type <= Exercise::JUDGMENT || q_type == Exercise::SUBJECTIVE
+          if answers_content.present?   #学生有回答时,分数已经全部存到exercise_answer 表,所以可以直接取第一个值
+            ques_score = answers_content.first.score
+            ques_score = ques_score.nil? || ques_score < 0 ? 0.0 : ques_score
+          else
+            ques_score = 0.0
+          end
+        else
+          ques_score = answers_content.select{|answer| answer.score >= 0.0}.pluck(:score).sum
+        end
+        score_option << ques_score
+      end
+
+      user_option = user_option + other_user_option + score_option + time_option
       @user_columns.push(user_option)
     end
   end
@@ -419,7 +452,7 @@ module ExportHelper
       end
     end
 
-    out_file_name = "作品附件_#{homework_common&.course&.name}_#{homework_common.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip"
+    out_file_name = "作品附件_#{homework_common.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip"
     out_file_name.gsub!(" ", "-")
     out_file_name.gsub!("/", "_")
     out_file = find_or_pack(homework_common, homework_common.user_id, digests.sort){
diff --git a/app/helpers/homework_commons_helper.rb b/app/helpers/homework_commons_helper.rb
index cc23d05d6..efc14dc5e 100644
--- a/app/helpers/homework_commons_helper.rb
+++ b/app/helpers/homework_commons_helper.rb
@@ -222,9 +222,17 @@ module HomeworkCommonsHelper
     [{ id: 0 ,name: "未评", count: homework.uncomment_count(user_id)}, {id: 1, name: "已评", count: homework.comment_count(user_id)}]
   end
 
+  # 作品状态
+  def practice_homework_status homework, member
+    [{id: 3, name: "未通关", count: homework.un_complete_count(member)},
+     {id: 4, name: "已通关", count: homework.complete_count(member)},
+     {id: 1, name: "按时完成", count: homework.finished_count(member)},
+     {id: 2, name: "延时完成", count: homework.delay_finished_count(member)}]
+  end
+
   # 作品状态
   def homework_status homework, member
-    [{id: 0, name: "未提交",   count: homework.unfinished_count(member)},
+    [{id: 0, name: "未提交", count: homework.unfinished_count(member)},
      {id: 1, name: "按时提交", count: homework.finished_count(member)},
      {id: 2, name: "延时提交", count: homework.delay_finished_count(member)}]
   end
diff --git a/app/libs/omniauth/strategies/qq.rb b/app/libs/omniauth/strategies/qq.rb
new file mode 100644
index 000000000..513257e3c
--- /dev/null
+++ b/app/libs/omniauth/strategies/qq.rb
@@ -0,0 +1,50 @@
+module OmniAuth
+  module Strategies
+    class QQ < OmniAuth::Strategies::OAuth2
+      option :client_options, {
+        site: 'https://graph.qq.com',
+        authorize_url: '/oauth2.0/authorize',
+        token_url: '/oauth2.0/token'
+      }
+
+      def request_phase
+        super
+      end
+
+      def authorize_params
+        super.tap do |params|
+          %w[scope client_options].each do |v|
+            if request.params[v]
+              params[v.to_sym] = request.params[v]
+            end
+          end
+        end
+      end
+
+      uid { raw_info['openid'].to_s }
+
+      info do
+        {
+          name: user_info['nickname'],
+          nickname: user_info['nickname'],
+          image: user_info['figureurl_qq_1']
+        }
+      end
+
+      extra do
+        { raw_info: user_info }
+      end
+
+      def raw_info
+        access_token.options[:mode] = :query
+        @raw_info ||= access_token.get('/oauth2.0/me').parsed
+      end
+
+      def user_info
+        access_token.options[:mode] = :query
+        params = { oauth_consumer_key: options.client_id, openid: raw_info['openid'], format: 'json' }
+        @user_info ||= access_token.get('/user/get_user_info', params: params)
+      end
+    end
+  end
+end
diff --git a/app/libs/util.rb b/app/libs/util.rb
index 72e728ab9..84f14a6c0 100644
--- a/app/libs/util.rb
+++ b/app/libs/util.rb
@@ -1,3 +1,5 @@
+require 'open-uri'
+
 module Util
   module_function
 
@@ -29,6 +31,16 @@ module Util
     end
   end
 
+  def download_file(url, save_path)
+    data = open(url, &:read)
+    file = File.new(save_path, 'w+')
+    file.binmode
+    file << data
+    file.flush
+    file.close
+    file
+  end
+
   def logger_error(exception)
     Rails.logger.error(exception.message)
     exception.backtrace.each { |message| Rails.logger.error(message) }
diff --git a/app/libs/util/file_manage.rb b/app/libs/util/file_manage.rb
index 822bfca4f..2f87a3e86 100644
--- a/app/libs/util/file_manage.rb
+++ b/app/libs/util/file_manage.rb
@@ -10,31 +10,35 @@ module Util::FileManage
     File.join(Rails.root, "public", "images", relative_path)
   end
 
-  def disk_filename(source_type, source_id,image_file=nil)
-    File.join(storage_path, "#{source_type}", "#{source_id}")
+  def disk_filename(source_type, source_id, suffix=nil)
+    File.join(storage_path, "#{source_type}", "#{source_id}#{suffix}")
   end
 
-  def exist?(source_type, source_id)
-    File.exist?(disk_filename(source_type, source_id))
+  def source_disk_filename(source, suffix=nil)
+    disk_filename(source.class.name, source.id, suffix)
   end
 
-  def exists?(source)
-    File.exist?(disk_filename(source.class, source.id))
+  def exist?(source_type, source_id, suffix=nil)
+    File.exist?(disk_filename(source_type, source_id, suffix))
+  end
+
+  def exists?(source, suffix=nil)
+    File.exist?(disk_filename(source.class, source.id, suffix))
   end
 
   def disk_file_url(source_type, source_id, suffix = nil)
-    t = ctime(source_type, source_id)
+    t = ctime(source_type, source_id, suffix)
     File.join('/images', relative_path, "#{source_type}", "#{source_id}#{suffix}") + "?t=#{t}"
   end
 
-  def source_disk_file_url(source)
-    disk_file_url(source.class, source.id)
+  def source_disk_file_url(source, suffix=nil)
+    disk_file_url(source.class, source.id, suffix)
   end
 
-  def ctime(source_type, source_id)
-    return nil unless exist?(source_type, source_id)
+  def ctime(source_type, source_id, suffix)
+    return nil unless exist?(source_type, source_id, suffix)
 
-    File.ctime(disk_filename(source_type, source_id)).to_i
+    File.ctime(disk_filename(source_type, source_id, suffix)).to_i
   end
 
   def disk_auth_filename(source_type, source_id, type)
diff --git a/app/libs/wechat/app.rb b/app/libs/wechat/weapp.rb
similarity index 100%
rename from app/libs/wechat/app.rb
rename to app/libs/wechat/weapp.rb
diff --git a/app/libs/wechat_oauth.rb b/app/libs/wechat_oauth.rb
new file mode 100644
index 000000000..ba4baee30
--- /dev/null
+++ b/app/libs/wechat_oauth.rb
@@ -0,0 +1,13 @@
+module WechatOauth
+  class << self
+    attr_accessor :appid, :secret, :scope, :base_url
+
+    def logger
+      @_logger ||= STDOUT
+    end
+
+    def logger=(l)
+      @_logger = l
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/libs/wechat_oauth/error.rb b/app/libs/wechat_oauth/error.rb
new file mode 100644
index 000000000..ac7f5fddc
--- /dev/null
+++ b/app/libs/wechat_oauth/error.rb
@@ -0,0 +1,14 @@
+class WechatOauth::Error < StandardError
+  attr_reader :code
+
+  def initialize(code, msg)
+    super(msg)
+    @code = code
+  end
+
+  def message
+    I18n.t("oauth.wechat.#{code}")
+  rescue I18n::MissingTranslationData
+    super
+  end
+end
\ No newline at end of file
diff --git a/app/libs/wechat_oauth/service.rb b/app/libs/wechat_oauth/service.rb
new file mode 100644
index 000000000..35ef8f455
--- /dev/null
+++ b/app/libs/wechat_oauth/service.rb
@@ -0,0 +1,61 @@
+module WechatOauth::Service
+  module_function
+
+  def request(method, url, params)
+    WechatOauth.logger.info("[WechatOauth] [#{method.to_s.upcase}] #{url} || #{params}")
+
+    client = Faraday.new(url: WechatOauth.base_url)
+    response = client.public_send(method, url, params)
+    result = JSON.parse(response.body)
+
+    WechatOauth.logger.info("[WechatOauth] [#{response.status}] #{result}")
+
+    if result['errcode'].present? && result['errcode'].to_s != '0'
+      raise WechatOauth::Error.new(result['errcode'], result['errmsg'])
+    end
+
+    result
+  end
+
+  # https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
+  # response:
+  # {
+  #   "access_token":"ACCESS_TOKEN",
+  #   "expires_in":7200,
+  #   "refresh_token":"REFRESH_TOKEN",
+  #   "openid":"OPENID",
+  #   "scope":"SCOPE",
+  #   "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
+  # }
+  def access_token(code)
+    params = {
+      appid: WechatOauth.appid,
+      secret: WechatOauth.secret,
+      code: code,
+      grant_type: 'authorization_code'
+    }
+
+    request(:get, '/sns/oauth2/access_token', params)
+  end
+
+  # https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html
+  # response:
+  # {
+  #   "openid":"OPENID",
+  #   "nickname":"NICKNAME",
+  #   "sex":1,
+  #   "province":"PROVINCE",
+  #   "city":"CITY",
+  #   "country":"COUNTRY",
+  #   "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
+  #   "privilege":[
+  #     "PRIVILEGE1",
+  #     "PRIVILEGE2"
+  #   ],
+  #   "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
+  #
+  # }
+  def user_info(access_token, openid)
+    request(:get, '/sns/userinfo', access_token: access_token, openid: openid)
+  end
+end
\ No newline at end of file
diff --git a/app/models/homework_common.rb b/app/models/homework_common.rb
index abe254b28..fc2dd3ea4 100644
--- a/app/models/homework_common.rb
+++ b/app/models/homework_common.rb
@@ -240,6 +240,16 @@ class HomeworkCommon < ApplicationRecord
     self.teacher_works(member).delay_finished.count
   end
 
+  # 未通关数
+  def un_complete_count member
+    teacher_works(member).count - complete_count(member)
+  end
+
+  # 通关数
+  def complete_count member
+    Myshixun.where(id: self.teacher_works(member).pluck(:myshixun_id), status: 1).count
+  end
+
   # 分组作业的最大分组id
   def max_group_id
     self.student_works.has_committed.maximum(:group_id).to_i + 1
diff --git a/app/models/homework_group_setting.rb b/app/models/homework_group_setting.rb
index ae9491cb3..7a06d5a7a 100644
--- a/app/models/homework_group_setting.rb
+++ b/app/models/homework_group_setting.rb
@@ -6,6 +6,6 @@ class HomeworkGroupSetting < ApplicationRecord
   scope :none_published, -> {where("homework_group_settings.publish_time IS NULL OR homework_group_settings.publish_time > ?", Time.now)}
   scope :published_no_end, -> {where("homework_group_settings.publish_time IS NOT NULL AND homework_group_settings.publish_time < ?
                                       and homework_group_settings.end_time > ?", Time.now, Time.now)}
-  scope :none_end, -> {where("homework_group_settings.end_time IS NOT NULL AND homework_group_settings.end_time > ?", Time.now)}
+  scope :none_end, -> {where("homework_group_settings.end_time IS NULL or homework_group_settings.end_time > ?", Time.now)}
 
 end
diff --git a/app/models/laboratory.rb b/app/models/laboratory.rb
new file mode 100644
index 000000000..53e66ece0
--- /dev/null
+++ b/app/models/laboratory.rb
@@ -0,0 +1,26 @@
+class Laboratory < ApplicationRecord
+  belongs_to :school, optional: true
+
+  has_many :laboratory_users, dependent: :destroy
+  has_many :users, through: :laboratory_users, source: :user
+
+  has_one :laboratory_setting, dependent: :destroy
+
+  validates :identifier, uniqueness: { case_sensitive: false }, allow_nil: true
+
+  def site
+    rails_env = EduSetting.get('rails_env')
+    suffix = rails_env && rails_env != 'production' ? ".#{rails_env}.educoder.net" : '.educoder.net'
+
+    identifier ? "#{identifier}#{suffix}" : ''
+  end
+
+  def self.find_by_subdomain(subdomain)
+    return if subdomain.blank?
+
+    rails_env = EduSetting.get('rails_env')
+    subdomain = subdomain.slice(0, subdomain.size - rails_env.size - 1) if subdomain.end_with?(rails_env) # winse.dev => winse
+
+    find_by_identifier(subdomain)
+  end
+end
\ No newline at end of file
diff --git a/app/models/laboratory_setting.rb b/app/models/laboratory_setting.rb
new file mode 100644
index 000000000..32848dca2
--- /dev/null
+++ b/app/models/laboratory_setting.rb
@@ -0,0 +1,54 @@
+class LaboratorySetting < ApplicationRecord
+  belongs_to :laboratory
+
+  serialize :config, JSON
+
+  %i[name navbar footer].each do |method_name|
+    define_method method_name do
+      config&.[](method_name.to_s)
+    end
+
+    define_method "#{method_name}=" do |value|
+      self.config ||= {}
+      config.[]=(method_name.to_s, value)
+    end
+  end
+
+  def login_logo_url
+    logo_url('login')
+  end
+
+  def nav_logo_url
+    logo_url('nav')
+  end
+
+  def tab_logo_url
+    logo_url('tab')
+  end
+
+  def default_navbar
+    self.class.default_config[:navbar]
+  end
+
+  private
+
+  def logo_url(type)
+    return nil unless Util::FileManage.exists?(self, type)
+    Util::FileManage.source_disk_file_url(self, type)
+  end
+
+  def self.default_config
+    {
+      name: nil,
+      navbar: [
+        { 'name' => '实践课程', 'link' => '/paths',        'hidden' => false },
+        { 'name' => '翻转课堂', 'link' => '/courses',      'hidden' => false },
+        { 'name' => '实现项目', 'link' => '/shixuns',      'hidden' => false },
+        { 'name' => '在线竞赛', 'link' => '/competitions', 'hidden' => false },
+        { 'name' => '教学案例', 'link' => '/moop_cases',   'hidden' => false },
+        { 'name' => '交流问答', 'link' => '/forums',       'hidden' => false },
+      ],
+      footer: nil
+    }
+  end
+end
\ No newline at end of file
diff --git a/app/models/laboratory_user.rb b/app/models/laboratory_user.rb
new file mode 100644
index 000000000..be6c0c4dd
--- /dev/null
+++ b/app/models/laboratory_user.rb
@@ -0,0 +1,4 @@
+class LaboratoryUser < ApplicationRecord
+  belongs_to :laboratory
+  belongs_to :user
+end
\ No newline at end of file
diff --git a/app/models/open_user.rb b/app/models/open_user.rb
new file mode 100644
index 000000000..91228b976
--- /dev/null
+++ b/app/models/open_user.rb
@@ -0,0 +1,9 @@
+class OpenUser < ApplicationRecord
+  belongs_to :user
+
+  validates :uid, presence: true, uniqueness: { scope: :type }
+
+  def can_bind_cache_key
+    "open_user:#{type}:#{uid}:can_bind"
+  end
+end
\ No newline at end of file
diff --git a/app/models/open_users/qq.rb b/app/models/open_users/qq.rb
new file mode 100644
index 000000000..242693ce5
--- /dev/null
+++ b/app/models/open_users/qq.rb
@@ -0,0 +1,3 @@
+class OpenUsers::QQ < OpenUser
+
+end
\ No newline at end of file
diff --git a/app/models/open_users/wechat.rb b/app/models/open_users/wechat.rb
new file mode 100644
index 000000000..046b3e086
--- /dev/null
+++ b/app/models/open_users/wechat.rb
@@ -0,0 +1,3 @@
+class OpenUsers::Wechat < OpenUser
+
+end
\ No newline at end of file
diff --git a/app/models/user.rb b/app/models/user.rb
index 7bfe9c36f..2f5400051 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,7 +28,12 @@ class User < ApplicationRecord
 
   MIX_PASSWORD_LIMIT = 8
 
+  LOGIN_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
+
   has_one :user_extension, dependent: :destroy
+  has_many :open_users, dependent: :destroy
+  has_one :wechat_open_user, class_name: 'OpenUsers::Wechat'
+  has_one :qq_open_user, class_name: 'OpenUsers::QQ'
   accepts_nested_attributes_for :user_extension, update_only: true
 
   has_many :memos, foreign_key: 'author_id'
@@ -38,7 +43,7 @@ class User < ApplicationRecord
   has_many :myshixuns, :dependent => :destroy
   has_many :study_shixuns, through: :myshixuns, source: :shixun   # 已学习的实训
   has_many :course_messages
-  has_many :courses, dependent: :destroy
+  has_many :courses, foreign_key: 'tea_id', dependent: :destroy
 
   #试卷
   has_many :exercise_banks, :dependent => :destroy
@@ -628,6 +633,23 @@ class User < ApplicationRecord
     admin? || business?
   end
 
+  def self.generate_login(prefix)
+    login = prefix + LOGIN_CHARS.sample(8).join('')
+    while User.exists?(login: login)
+      login = prefix + LOGIN_CHARS.sample(8).join('')
+    end
+
+    login
+  end
+
+  def bind_open_user?(type)
+    case type
+    when 'wechat' then wechat_open_user.present?
+    when 'qq' then qq_open_user.present?
+    else false
+    end
+  end
+
   protected
   def validate_password_length
     # 管理员的初始密码是5位
diff --git a/app/queries/admins/laboratory_query.rb b/app/queries/admins/laboratory_query.rb
new file mode 100644
index 000000000..8667bb8ed
--- /dev/null
+++ b/app/queries/admins/laboratory_query.rb
@@ -0,0 +1,23 @@
+class Admins::LaboratoryQuery < ApplicationQuery
+  include CustomSortable
+
+  attr_reader :params
+
+  sort_columns :id, default_by: :id, default_direction: :desc
+
+  def initialize(params)
+    @params = params
+  end
+
+  def call
+    laboratories = Laboratory.all
+
+    keyword = strip_param(:keyword)
+    if keyword.present?
+      like_sql = 'schools.name LIKE :keyword OR laboratories.identifier LIKE :keyword'
+      laboratories = laboratories.left_joins(:school).where(like_sql, keyword: "%#{keyword}%")
+    end
+
+    custom_sort laboratories, params[:sort_by], params[:sort_direction]
+  end
+end
\ No newline at end of file
diff --git a/app/services/admins/add_laboratory_user_service.rb b/app/services/admins/add_laboratory_user_service.rb
new file mode 100644
index 000000000..16df30880
--- /dev/null
+++ b/app/services/admins/add_laboratory_user_service.rb
@@ -0,0 +1,19 @@
+class Admins::AddLaboratoryUserService < ApplicationService
+  attr_reader :laboratory, :params
+
+  def initialize(laboratory, params)
+    @laboratory = laboratory
+    @params     = params
+  end
+
+  def call
+    columns = %i[]
+    LaboratoryUser.bulk_insert(*columns) do |worker|
+      Array.wrap(params[:user_ids]).compact.each do |user_id|
+        next if laboratory.laboratory_users.exists?(user_id: user_id)
+
+        worker.add(laboratory_id: laboratory.id, user_id: user_id)
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/services/admins/create_laboratory_service.rb b/app/services/admins/create_laboratory_service.rb
new file mode 100644
index 000000000..98300d5af
--- /dev/null
+++ b/app/services/admins/create_laboratory_service.rb
@@ -0,0 +1,20 @@
+class Admins::CreateLaboratoryService < ApplicationService
+  Error = Class.new(StandardError)
+
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def call
+    raise Error, '单位不能为空' if params[:school_id].blank?
+    raise Error, '该单位已存在' if Laboratory.exists?(school_id: params[:school_id])
+
+    ActiveRecord::Base.transaction do
+      laboratory = Laboratory.create!(school_id: params[:school_id])
+
+      laboratory.create_laboratory_setting!
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/services/admins/save_laboratory_setting_service.rb b/app/services/admins/save_laboratory_setting_service.rb
new file mode 100644
index 000000000..00e202cd9
--- /dev/null
+++ b/app/services/admins/save_laboratory_setting_service.rb
@@ -0,0 +1,51 @@
+class Admins::SaveLaboratorySettingService < ApplicationService
+  attr_reader :laboratory, :laboratory_setting, :params
+
+  def initialize(laboratory, params)
+    @params     = params
+    @laboratory = laboratory
+    @laboratory_setting = laboratory.laboratory_setting
+  end
+
+  def call
+    ActiveRecord::Base.transaction do
+      laboratory.identifier = strip params[:identifier]
+      laboratory_setting.name = strip params[:name]
+      laboratory_setting.navbar = navbar_config
+      laboratory_setting.footer = strip params[:footer]
+
+      laboratory.save!
+      laboratory_setting.save!
+
+      deal_logo_file
+    end
+
+    laboratory
+  end
+
+  private
+
+  def navbar_config
+    params[:navbar].map do |nav|
+      hash = {}
+      hash[:name] = strip nav[:name]
+      hash[:link] = strip nav[:link]
+      hash[:hidden] = nav[:hidden].to_s == 0
+      hash
+    end
+  end
+
+  def deal_logo_file
+    save_logo_file(params[:nav_logo], 'nav')
+    save_logo_file(params[:login_logo], 'login')
+    save_logo_file(params[:tab_logo], 'tab')
+  end
+
+  def save_logo_file(file, type)
+    return unless file.present? && file.is_a?(ActionDispatch::Http::UploadedFile)
+
+    file_path = Util::FileManage.source_disk_filename(laboratory_setting, type)
+    File.delete(file_path) if File.exist?(file_path) # 删除之前的文件
+    Util.write_file(file, file_path)
+  end
+end
\ No newline at end of file
diff --git a/app/services/application_service.rb b/app/services/application_service.rb
index c6f66c098..1be6896eb 100644
--- a/app/services/application_service.rb
+++ b/app/services/application_service.rb
@@ -1,3 +1,11 @@
 class ApplicationService
   include Callable
+
+  Error = Class.new(StandardError)
+
+  private
+
+  def strip(str)
+    str.to_s.strip.presence
+  end
 end
\ No newline at end of file
diff --git a/app/services/create_bind_user_service.rb b/app/services/create_bind_user_service.rb
new file mode 100644
index 000000000..93d9d87ca
--- /dev/null
+++ b/app/services/create_bind_user_service.rb
@@ -0,0 +1,53 @@
+class CreateBindUserService < ApplicationService
+  attr_reader :user, :params
+
+  def initialize(user, params)
+    @user   = user
+    @params = params
+  end
+
+  def call
+    raise Error, '系统错误' if open_user.blank?
+    raise Error, '系统错误' unless can_bind_user?
+
+    if params[:not_bind].to_s == 'true'
+      clear_can_bind_user_flag
+      return user
+    end
+
+    bind_user = User.try_to_login(params[:username], params[:password])
+    raise Error, '用户名或者密码错误' if bind_user.blank?
+    raise Error, '该账号已被绑定' if bind_user.bind_open_user?(params[:type].to_s)
+
+    ActiveRecord::Base.transaction do
+      open_user.user_id = bind_user.id
+      open_user.save!
+
+      user.user_extension.delete
+      user.delete
+    end
+
+    clear_can_bind_user_flag
+
+    bind_user
+  end
+
+  private
+
+  def open_user
+    @_open_user ||= begin
+      case params[:type].to_s
+      when 'wechat' then user.wechat_open_user
+      when 'qq' then user.qq_open_user
+      end
+    end
+  end
+
+  def can_bind_user?
+    Rails.cache.read(open_user.can_bind_cache_key).present?
+  end
+
+  def clear_can_bind_user_flag
+    Rails.cache.delete(open_user.can_bind_cache_key)
+  end
+end
\ No newline at end of file
diff --git a/app/services/oauth/create_or_find_qq_account_service.rb b/app/services/oauth/create_or_find_qq_account_service.rb
new file mode 100644
index 000000000..691764ea2
--- /dev/null
+++ b/app/services/oauth/create_or_find_qq_account_service.rb
@@ -0,0 +1,38 @@
+class Oauth::CreateOrFindQqAccountService < ApplicationService
+
+  attr_reader :user, :params
+
+  def initialize(user, params)
+    @user   = user
+    @params = params
+  end
+
+  def call
+    new_user = false
+    # 存在该用户
+    open_user = OpenUsers::QQ.find_by(uid: params['uid'])
+    return [open_user.user, new_user] if open_user.present?
+
+    if user.blank? || !user.logged?
+      new_user = true
+      # 新用户
+      login = User.generate_login('q')
+      @user = User.new(login: login, nickname: params.dig('info', 'nickname'), type: 'User', status: User::STATUS_ACTIVE)
+    end
+
+    ActiveRecord::Base.transaction do
+      if user.new_record?
+        user.save!
+
+        gender = params.dig('extra', 'raw_info', 'gender') == '女' ? 1 : 0
+        user.create_user_extension!(gender: gender)
+      end
+
+      new_open_user = OpenUsers::QQ.create!(user: user, uid: params['uid'])
+
+      Rails.cache.write(new_open_user.can_bind_cache_key, 1, expires_in: 1.hours) if new_user # 方便后面进行账号绑定
+    end
+
+    [user, new_user]
+  end
+end
\ No newline at end of file
diff --git a/app/services/oauth/create_or_find_wechat_account_service.rb b/app/services/oauth/create_or_find_wechat_account_service.rb
new file mode 100644
index 000000000..0313054b7
--- /dev/null
+++ b/app/services/oauth/create_or_find_wechat_account_service.rb
@@ -0,0 +1,57 @@
+class Oauth::CreateOrFindWechatAccountService < ApplicationService
+  Error = Class.new(StandardError)
+
+  attr_reader :user, :params
+
+  def initialize(user, params)
+    @user   = user
+    @params = params
+  end
+
+  def call
+    code = params['code'].to_s.strip
+    raise Error, 'Code不能为空' if code.blank?
+    new_user = false
+
+    result = WechatOauth::Service.access_token(code)
+    result = WechatOauth::Service.user_info(result['access_token'], result['openid'])
+
+    # 存在该用户
+    open_user = OpenUsers::Wechat.find_by(uid: result['unionid'])
+    return [open_user.user, new_user] if open_user.present?
+
+    if user.blank? || !user.logged?
+      new_user = true
+      # 新用户
+      login = User.generate_login('w')
+      @user = User.new(login: login, nickname: result['nickname'], type: 'User', status: User::STATUS_ACTIVE)
+    end
+
+    ActiveRecord::Base.transaction do
+      if new_user
+        user.save!
+
+        gender = result['sex'].to_i == 1 ? 0 : 1
+        user.create_user_extension!(gender: gender)
+
+        # 下载头像
+        avatar_path = Util::FileManage.source_disk_filename(user)
+        Util.download_file(result['headimgurl'], avatar_path)
+      end
+
+      new_open_user= OpenUsers::Wechat.create!(user: user, uid: result['unionid'])
+
+      Rails.cache.write(new_open_user.can_bind_cache_key, 1, expires_in: 1.hours) if new_user # 方便后面进行账号绑定
+    end
+
+    [user, new_user]
+  rescue WechatOauth::Error => ex
+    raise Error, ex.message
+  end
+
+  private
+
+  def code
+    params[:code].to_s.strip
+  end
+end
\ No newline at end of file
diff --git a/app/services/users/update_password_service.rb b/app/services/users/update_password_service.rb
index 0df32eb76..53c6f74c8 100644
--- a/app/services/users/update_password_service.rb
+++ b/app/services/users/update_password_service.rb
@@ -11,7 +11,7 @@ class Users::UpdatePasswordService < ApplicationService
   def call
     Users::UpdatePasswordForm.new(params).validate!
 
-    raise Error, '旧密码不匹配' unless user.check_password?(params[:old_password])
+    raise Error, '旧密码不匹配' unless user.check_password?(params[:old_password]) || user.hashed_password.blank?
 
     ActiveRecord::Base.transaction do
       user.update!(password: params[:password])
diff --git a/app/views/admins/laboratories/index.html.erb b/app/views/admins/laboratories/index.html.erb
new file mode 100644
index 000000000..012eed792
--- /dev/null
+++ b/app/views/admins/laboratories/index.html.erb
@@ -0,0 +1,19 @@
+<% define_admin_breadcrumbs do %>
+  <% add_admin_breadcrumb('云上实验室') %>
+<% end %>
+
+
+  <%= form_tag(admins_laboratories_path(unsafe_params), method: :get, class: 'form-inline search-form flex-1', remote: true) do %>
+    <%= text_field_tag(:keyword, params[:keyword], class: 'form-control col-6 col-md-4 ml-3', placeholder: '学校名称/二级域名前缀检索') %>
+    <%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %>
+  <% end %>
+
+  <%= javascript_void_link '新建', class: 'btn btn-primary', data: { toggle: 'modal', target: '.admin-create-laboratory-modal' } %>
+
+
+
+  <%= render(partial: 'admins/laboratories/shared/list', locals: { laboratories: @laboratories }) %>
+
+
+<%= render 'admins/laboratories/shared/create_laboratory_modal' %>
+<%= render 'admins/laboratories/shared/add_laboratory_user_modal' %>
\ No newline at end of file
diff --git a/app/views/admins/laboratories/index.js.erb b/app/views/admins/laboratories/index.js.erb
new file mode 100644
index 000000000..dc17c6a6d
--- /dev/null
+++ b/app/views/admins/laboratories/index.js.erb
@@ -0,0 +1 @@
+$('.laboratory-list-container').html("<%= j(render partial: 'admins/laboratories/shared/list', locals: { laboratories: @laboratories }) %>");
\ No newline at end of file
diff --git a/app/views/admins/laboratories/shared/_add_laboratory_user_modal.html.erb b/app/views/admins/laboratories/shared/_add_laboratory_user_modal.html.erb
new file mode 100644
index 000000000..a13565cd6
--- /dev/null
+++ b/app/views/admins/laboratories/shared/_add_laboratory_user_modal.html.erb
@@ -0,0 +1,30 @@
+
\ No newline at end of file
diff --git a/app/views/admins/laboratories/shared/_create_laboratory_modal.html.erb b/app/views/admins/laboratories/shared/_create_laboratory_modal.html.erb
new file mode 100644
index 000000000..0a77477d3
--- /dev/null
+++ b/app/views/admins/laboratories/shared/_create_laboratory_modal.html.erb
@@ -0,0 +1,28 @@
+
\ No newline at end of file
diff --git a/app/views/admins/laboratories/shared/_laboratory_item.html.erb b/app/views/admins/laboratories/shared/_laboratory_item.html.erb
new file mode 100644
index 000000000..5dd97b549
--- /dev/null
+++ b/app/views/admins/laboratories/shared/_laboratory_item.html.erb
@@ -0,0 +1,40 @@
+<% school = laboratory.school %>
+<%= school&.name || 'EduCoder主站' %> 
+
+  <% if laboratory.identifier %>
+    <%= link_to laboratory.site, "https://#{laboratory.site}", target: '_blank'  %>
+  <% else %>
+    --
+  <% end %>
+ 
+
+  <% if school && school.identifier.present? %>
+    <%= link_to school.identifier.to_s, statistics_college_path(school.identifier), target: '_blank' %>
+  <% else %>
+    --
+  <% end %>
+ 
+
+  
+    <% laboratory.users.each do |user| %>
+    
+      <%= link_to user.real_name, "/users/#{user.login}", target: '_blank', data: { toggle: 'tooltip', title: '个人主页' } %>
+      <%= link_to(admins_laboratory_laboratory_user_path(laboratory, user_id: user.id),
+                  method: :delete, remote: true, class: 'ml-1 delete-laboratory-user-action',
+                  data: { confirm: '确认删除吗?' }) do %>
+         
+    <% end %>
+  
+ 
+<%= laboratory.created_at.strftime('%Y-%m-%d %H:%M') %> 
+
+  <%= link_to '定制', admins_laboratory_laboratory_setting_path(laboratory) %>
+
+  <% if school.present? && laboratory.id != 1 %>
+    <%= javascript_void_link '添加管理员', class: 'action', data: { laboratory_id: laboratory.id, toggle: 'modal', target: '.admin-add-laboratory-user-modal' } %>
+
+    <%= delete_link '删除', admins_laboratory_path(laboratory, element: ".laboratory-item-#{laboratory.id}"), class: 'delete-laboratory-action' %>
+  <% end %>
+ 
\ No newline at end of file
diff --git a/app/views/admins/laboratories/shared/_list.html.erb b/app/views/admins/laboratories/shared/_list.html.erb
new file mode 100644
index 000000000..33a47eed7
--- /dev/null
+++ b/app/views/admins/laboratories/shared/_list.html.erb
@@ -0,0 +1,25 @@
+
+  
+  
+    单位名称 
+    域名 
+    统计链接 
+    管理员 
+    <%= sort_tag('创建时间', name: 'id', path: admins_laboratories_path) %> 
+    操作 
+   
+   
+  
+  <% if laboratories.present? %>
+    <% laboratories.each do |laboratory| %>
+      
+        <%= render 'admins/laboratories/shared/laboratory_item', laboratory: laboratory %>
+       
+    <% end %>
+  <% else %>
+    <%= render 'admins/shared/no_data_for_table' %>
+  <% end %>
+   
+
+
+<%= render partial: 'admins/shared/paginate', locals: { objects: laboratories } %>
\ No newline at end of file
diff --git a/app/views/admins/laboratory_settings/show.html.erb b/app/views/admins/laboratory_settings/show.html.erb
new file mode 100644
index 000000000..120bba6cb
--- /dev/null
+++ b/app/views/admins/laboratory_settings/show.html.erb
@@ -0,0 +1,131 @@
+<% define_admin_breadcrumbs do %>
+  <% add_admin_breadcrumb('云上实验室', admins_laboratories_path) %>
+  <% add_admin_breadcrumb('单位定制') %>
+<% end %>
+
+
+  <%= simple_form_for(@laboratory, url: admins_laboratory_laboratory_setting_path(@laboratory), method: 'patch', html: { enctype: 'multipart/form-data' }) do |f| %>
+    <% setting = @laboratory.laboratory_setting %>
+
+    
+
+    
+
+    
+
+    
+
+    
+
+    
+
+    
+      <%= javascript_void_link '保存', class: 'btn btn-primary mr-3 px-4 submit-btn' %>
+      <%= link_to '取消', admins_laboratories_path, class: 'btn btn-secondary px-4' %>
+    
+  <% end %>
+
<%= sidebar_item(admins_schools_path, '单位列表', icon: 'university', controller: 'admins-schools') %> 
         <%= sidebar_item(admins_departments_path, '部门列表', icon: 'sitemap', controller: 'admins-departments') %> 
+        <%= sidebar_item(admins_laboratories_path, '云上实验室', icon: 'cloud', controller: 'admins-laboratories') %> 
       <% end %>
     
 
-
-      <%#= sidebar_item_group('#course-submenu', '课堂+', icon: 'mortar-board') do %>
-
-
-
-
-      <%# end %>
-
-
     
       <%= sidebar_item_group('#user-submenu', '用户', icon: 'user') do %>
          <%= sidebar_item(admins_users_path, '用户列表', icon: 'user', controller: 'admins-users') %> 
-
-
       <% end %>
     
 
diff --git a/app/views/courses/students.json.jbuilder b/app/views/courses/students.json.jbuilder
index 0b5d7fe71..5788b44f2 100644
--- a/app/views/courses/students.json.jbuilder
+++ b/app/views/courses/students.json.jbuilder
@@ -1,7 +1,7 @@
 json.students do
   json.array! @students do |student|
     json.user_id student.user_id
-    # json.login student.user.try(:login)
+    json.login student.user.try(:login)
     json.name student.user.try(:real_name)
     json.name_link user_path(student.user)
     json.student_id student.user.try(:student_id)
diff --git a/app/views/courses/top_banner.json.jbuilder b/app/views/courses/top_banner.json.jbuilder
index 877ffcdf1..9a0554585 100644
--- a/app/views/courses/top_banner.json.jbuilder
+++ b/app/views/courses/top_banner.json.jbuilder
@@ -15,7 +15,7 @@ json.is_admin @user_course_identity < Course::PROFESSOR
 json.is_public @course.is_public == 1
 json.code_halt @course.invite_code_halt == 1
 json.invite_code @course.invite_code_halt == 0 ? @course.generate_invite_code : ""
-json.switch_to_student switch_student_role(@is_teacher, @course, @user)
+json.switch_to_student @switch_student
 json.switch_to_teacher switch_teacher_role(@is_student, @course, @user)
 json.switch_to_assistant switch_assistant_role(@is_student, @course, @user)
 #json.join_course !@user.member_of_course?(@course)
diff --git a/app/views/exercises/_user_exercise_info.json.jbuilder b/app/views/exercises/_user_exercise_info.json.jbuilder
index bdac3a985..c351a9b26 100644
--- a/app/views/exercises/_user_exercise_info.json.jbuilder
+++ b/app/views/exercises/_user_exercise_info.json.jbuilder
@@ -65,7 +65,7 @@ json.exercise_questions do
                   shixun_type: user_ques_answers[:shixun_type],
                   ques_position: nil,
                   edit_type:nil
-    if user_ques_comments.count > 0
+    if user_ques_comments.size > 0
       json.question_comments do
         json.partial! "exercises/exercise_comments", question_comment:user_ques_answers[:question_comment].first
       end
diff --git a/app/views/exercises/common_header.json.jbuilder b/app/views/exercises/common_header.json.jbuilder
index 5d33aca66..d43d7c3f8 100644
--- a/app/views/exercises/common_header.json.jbuilder
+++ b/app/views/exercises/common_header.json.jbuilder
@@ -1,6 +1,6 @@
 json.course_is_end @course.is_end # true表示已结束,false表示未结束
 json.extract! @exercise, :id,:exercise_name,:exercise_description,:show_statistic
-json.time @user_left_time
+json.time (@user_left_time.to_i / 60)
 
 json.exercise_status @ex_status
 
diff --git a/app/views/homework_commons/works_list.json.jbuilder b/app/views/homework_commons/works_list.json.jbuilder
index e567ea79a..839b40bfd 100644
--- a/app/views/homework_commons/works_list.json.jbuilder
+++ b/app/views/homework_commons/works_list.json.jbuilder
@@ -23,7 +23,7 @@ if @user_course_identity < Course::STUDENT
   if @homework.homework_type != "practice"
     json.teacher_comment teacher_comment @homework, @current_user.id
   end
-  json.task_status homework_status @homework, @member
+  json.task_status @homework.homework_type != "practice" ? homework_status(@homework, @member) : practice_homework_status(@homework, @member)
   json.course_group_info course_group_info @course, @current_user.id
 
 elsif @user_course_identity == Course::STUDENT
diff --git a/app/views/settings/show.json.jbuilder b/app/views/settings/show.json.jbuilder
new file mode 100644
index 000000000..1fce12b77
--- /dev/null
+++ b/app/views/settings/show.json.jbuilder
@@ -0,0 +1,12 @@
+json.setting do
+  setting = @laboratory.laboratory_setting
+
+  json.name setting.name || default_setting.name
+  json.nav_logo_url (setting.nav_logo_url || default_setting.nav_logo_url)&.[](1..-1)
+  json.login_logo_url (setting.login_logo_url || default_setting.login_logo_url)&.[](1..-1)
+  json.tab_logo_url (setting.tab_logo_url || default_setting.tab_logo_url)&.[](1..-1)
+
+  json.navbar setting.navbar || default_setting.navbar
+
+  json.footer setting.footer || default_setting.footer
+end
\ No newline at end of file
diff --git a/app/views/users/accounts/show.json.jbuilder b/app/views/users/accounts/show.json.jbuilder
index ec81cc6bf..48d69924c 100644
--- a/app/views/users/accounts/show.json.jbuilder
+++ b/app/views/users/accounts/show.json.jbuilder
@@ -25,3 +25,5 @@ json.department_name extension&.department&.name
 
 json.base_info_completed user.profile_completed?
 json.all_certified user.all_certified?
+
+json.has_password user.hashed_password.present?
diff --git a/config/admins/sidebar.yml b/config/admins/sidebar.yml
index 30af794b7..9da34a014 100644
--- a/config/admins/sidebar.yml
+++ b/config/admins/sidebar.yml
@@ -1 +1,2 @@
-admins-mirror_scripts: 'admins-mirror_repositories'
\ No newline at end of file
+admins-mirror_scripts: 'admins-mirror_repositories'
+admins-laboratory_settings: 'admins-laboratories'
\ No newline at end of file
diff --git a/config/configuration.yml.example b/config/configuration.yml.example
index 6feee28d9..612011a7f 100644
--- a/config/configuration.yml.example
+++ b/config/configuration.yml.example
@@ -1,4 +1,13 @@
 defaults: &defaults
+  oauth:
+    qq:
+      appid: 'test'
+      secret: 'test123456'
+    wechat:
+      appid: 'test'
+      secret: 'test'
+      scope: 'snsapi_login'
+      base_url: 'https://api.weixin.qq.com'
   aliyun_vod:
     access_key_id: 'test'
     access_key_secret: 'test'
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index d173fb9fa..a501cb14f 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -14,3 +14,8 @@
 # ActiveSupport::Inflector.inflections(:en) do |inflect|
 #   inflect.acronym 'RESTful'
 # end
+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.acronym 'QQ'
+  inflect.acronym 'OmniAuth'
+end
\ No newline at end of file
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
new file mode 100644
index 000000000..27ade9ed4
--- /dev/null
+++ b/config/initializers/omniauth.rb
@@ -0,0 +1,17 @@
+OmniAuth.config.add_camelization 'qq', 'QQ'
+
+oauth_config = {}
+begin
+  config = Rails.application.config_for(:configuration)
+  oauth_config = config.dig('oauth', 'qq')
+  raise 'oauth qq config missing' if oauth_config.blank?
+rescue => ex
+  raise ex if Rails.env.production?
+
+  puts %Q{\033[33m [warning] qq oauth config or configuration.yml missing,
+           please add it or execute 'cp config/configuration.yml.example config/configuration.yml' \033[0m}
+end
+
+Rails.application.config.middleware.use OmniAuth::Builder do
+  provider :qq, oauth_config['appid'], oauth_config['secret']
+end
diff --git a/config/initializers/wechat_oauth_init.rb b/config/initializers/wechat_oauth_init.rb
new file mode 100644
index 000000000..6c7f849ec
--- /dev/null
+++ b/config/initializers/wechat_oauth_init.rb
@@ -0,0 +1,17 @@
+oauth_config = {}
+begin
+  config = Rails.application.config_for(:configuration)
+  oauth_config = config.dig('oauth', 'wechat')
+  raise 'oauth wechat config missing' if oauth_config.blank?
+rescue => ex
+  raise ex if Rails.env.production?
+
+  puts %Q{\033[33m [warning] wechat oauth config or configuration.yml missing,
+           please add it or execute 'cp config/configuration.yml.example config/configuration.yml' \033[0m}
+end
+
+WechatOauth.appid    = oauth_config['appid']
+WechatOauth.secret   = oauth_config['secret']
+WechatOauth.scope    = oauth_config['scope']
+WechatOauth.base_url = oauth_config['base_url']
+WechatOauth.logger   = Rails.logger
diff --git a/config/locales/laboratories/zh-CN.yml b/config/locales/laboratories/zh-CN.yml
new file mode 100644
index 000000000..42127f0a1
--- /dev/null
+++ b/config/locales/laboratories/zh-CN.yml
@@ -0,0 +1,7 @@
+zh-CN:
+  activerecord:
+    models:
+      laboratory: ''
+    attributes:
+      laboratory:
+        identifier: '二级域名'
\ No newline at end of file
diff --git a/config/locales/oauth/wechat.zh-CN.yml b/config/locales/oauth/wechat.zh-CN.yml
new file mode 100644
index 000000000..12b58c513
--- /dev/null
+++ b/config/locales/oauth/wechat.zh-CN.yml
@@ -0,0 +1,4 @@
+'zh-CN':
+  oauth:
+    wechat:
+      '40029': '授权已失效,请重新授权'
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 50b61c463..32725e8e9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -824,6 +824,11 @@ Rails.application.routes.draw do
       end
     end
     resource :template, only: [:show]
+    resource :setting, only: [:show]
+
+    get '/auth/qq/callback', to: 'oauth/qq#create'
+    get '/auth/wechat/callback', to: 'oauth/wechat#create'
+    resource :bind_user, only: [:create]
   end
 
   namespace :admins do
@@ -953,7 +958,7 @@ Rails.application.routes.draw do
     resources :choose_mirror_repositories, only: [:new, :create]
     resources :schools, only: [:index, :destroy]
     resources :departments, only: [:index, :create, :edit, :update, :destroy] do
-      resource :department_member, only: [:create, :update, :destroy]
+      resource :department_member, only: [:create, :destroy]
       post :merge, on: :collection
     end
     resources :myshixuns, only: [:index]
@@ -971,6 +976,10 @@ Rails.application.routes.draw do
     resources :carousels, only: [:index, :create, :update, :destroy] do
       post :drag, on: :collection
     end
+    resources :laboratories, only: [:index, :create, :destroy] do
+      resource :laboratory_setting, only: [:show, :update]
+      resource :laboratory_user, only: [:create, :destroy]
+    end
   end
 
   resources :colleges, only: [] do
diff --git a/db/migrate/20190821054352_create_open_users.rb b/db/migrate/20190821054352_create_open_users.rb
new file mode 100644
index 000000000..f8e0aba4b
--- /dev/null
+++ b/db/migrate/20190821054352_create_open_users.rb
@@ -0,0 +1,14 @@
+class CreateOpenUsers < ActiveRecord::Migration[5.2]
+  def change
+    create_table :open_users do |t|
+      t.references :user
+
+      t.string :type
+      t.string :uid
+
+      t.timestamps
+
+      t.index [:type, :uid], unique: true
+    end
+  end
+end
diff --git a/db/migrate/20191010011844_create_laboratories.rb b/db/migrate/20191010011844_create_laboratories.rb
new file mode 100644
index 000000000..3dfb442f0
--- /dev/null
+++ b/db/migrate/20191010011844_create_laboratories.rb
@@ -0,0 +1,12 @@
+class CreateLaboratories < ActiveRecord::Migration[5.2]
+  def change
+    create_table :laboratories do |t|
+      t.references :school
+      t.string :identifier
+
+      t.timestamps
+
+      t.index :identifier, unique: true
+    end
+  end
+end
diff --git a/db/migrate/20191010012226_create_laboratory_users.rb b/db/migrate/20191010012226_create_laboratory_users.rb
new file mode 100644
index 000000000..1b7ae762d
--- /dev/null
+++ b/db/migrate/20191010012226_create_laboratory_users.rb
@@ -0,0 +1,8 @@
+class CreateLaboratoryUsers < ActiveRecord::Migration[5.2]
+  def change
+    create_table :laboratory_users do |t|
+      t.references :laboratory
+      t.references :user
+    end
+  end
+end
diff --git a/db/migrate/20191010063403_create_laboratory_settings.rb b/db/migrate/20191010063403_create_laboratory_settings.rb
new file mode 100644
index 000000000..7f0a5f015
--- /dev/null
+++ b/db/migrate/20191010063403_create_laboratory_settings.rb
@@ -0,0 +1,9 @@
+class CreateLaboratorySettings < ActiveRecord::Migration[5.2]
+  def change
+    create_table :laboratory_settings do |t|
+      t.references :laboratory
+
+      t.text :config
+    end
+  end
+end
diff --git a/db/migrate/20191011025619_init_edu_coder_laboratory.rb b/db/migrate/20191011025619_init_edu_coder_laboratory.rb
new file mode 100644
index 000000000..831ca3985
--- /dev/null
+++ b/db/migrate/20191011025619_init_edu_coder_laboratory.rb
@@ -0,0 +1,22 @@
+class InitEduCoderLaboratory < ActiveRecord::Migration[5.2]
+  def change
+    ActiveRecord::Base.transaction do
+      laboratory = Laboratory.create!(id: 1, identifier: 'www')
+      setting = laboratory.build_laboratory_setting
+      footer = %Q{
+
+    }
+      config = setting.class.default_config.merge(name: 'EduCoder', footer: footer)
+      setting.config = config
+      setting.save!
+    end
+  end
+end
diff --git a/public/assets/.sprockets-manifest-4627fa5586ef7fed55ca286af7c028e9.json b/public/assets/.sprockets-manifest-4627fa5586ef7fed55ca286af7c028e9.json
index 52d2a4e9b..b8b49cc9d 100644
--- a/public/assets/.sprockets-manifest-4627fa5586ef7fed55ca286af7c028e9.json
+++ b/public/assets/.sprockets-manifest-4627fa5586ef7fed55ca286af7c028e9.json
@@ -1 +1 @@
-{"files":{"admin-cd9ca8bacc973ce2dbace30c97f6c40bc08e2c2ee44972f668e738e1902c0121.js":{"logical_path":"admin.js","mtime":"2019-09-11T16:20:07+08:00","size":4350881,"digest":"cd9ca8bacc973ce2dbace30c97f6c40bc08e2c2ee44972f668e738e1902c0121","integrity":"sha256-zZyousyXPOLbrOMMl/bEC8COLC7kSXL2aOc44ZAsASE="},"admin-a1b3356efe50ff4717cf22475639b5333c5354ba03fd107c9b7a8d4ae76f47aa.css":{"logical_path":"admin.css","mtime":"2019-09-11T16:20:07+08:00","size":773445,"digest":"a1b3356efe50ff4717cf22475639b5333c5354ba03fd107c9b7a8d4ae76f47aa","integrity":"sha256-obM1bv5Q/0cXzyJHVjm1MzxTVLoD/RB8m3qNSudvR6o="},"font-awesome/fontawesome-webfont-7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979.eot":{"logical_path":"font-awesome/fontawesome-webfont.eot","mtime":"2019-08-14T17:22:43+08:00","size":165742,"digest":"7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979","integrity":"sha256-e/yrbbmdXPvxcFygU23ceFhUMsxfpBu9etDwCQM7KXk="},"font-awesome/fontawesome-webfont-2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe.woff2":{"logical_path":"font-awesome/fontawesome-webfont.woff2","mtime":"2019-08-14T17:22:43+08:00","size":77160,"digest":"2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe","integrity":"sha256-Kt78vAQefRj88tQXh53FoJmXqmTWdbejxLbOM9oT8/4="},"font-awesome/fontawesome-webfont-ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07.woff":{"logical_path":"font-awesome/fontawesome-webfont.woff","mtime":"2019-08-14T17:22:43+08:00","size":98024,"digest":"ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07","integrity":"sha256-ugxZ3rVFD1y0Gz+TYJ7i0NmVQVh33foiPoqKdTNHTwc="},"font-awesome/fontawesome-webfont-aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8.ttf":{"logical_path":"font-awesome/fontawesome-webfont.ttf","mtime":"2019-08-14T17:22:43+08:00","size":165548,"digest":"aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8","integrity":"sha256-qljzPyOaD7AvXHpsRcBD16msmgkzNYBmlOzW1O3A1qg="},"font-awesome/fontawesome-webfont-ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4.svg":{"logical_path":"font-awesome/fontawesome-webfont.svg","mtime":"2019-08-14T17:22:43+08:00","size":444379,"digest":"ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4","integrity":"sha256-rWFXkmwWIrpOHQPUePFUE2hSS/xG9R5C/g2UX37zI+Q="},"college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js":{"logical_path":"college.js","mtime":"2019-09-26T14:40:40+08:00","size":3352744,"digest":"18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287","integrity":"sha256-GPXoQAMxY06JijWswhh4FcCWwl4Kt0q6NBrpFhZs0oc="},"college-944d4273f62c7538368b9017fdd3387b5e3bea31a87873770eb231324546d4d9.css":{"logical_path":"college.css","mtime":"2019-09-11T16:20:07+08:00","size":546841,"digest":"944d4273f62c7538368b9017fdd3387b5e3bea31a87873770eb231324546d4d9","integrity":"sha256-lE1Cc/YsdTg2i5AX/dM4e1476jGoeHN3DrIxMkVG1Nk="},"logo-7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423.png":{"logical_path":"logo.png","mtime":"2019-09-03T08:55:53+08:00","size":2816,"digest":"7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423","integrity":"sha256-f/ESVocJv5f5iY/ockm3qPIA/x9I1TfYWvhyFfGHBCM="},"application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js":{"logical_path":"application.js","mtime":"2019-09-26T14:40:40+08:00","size":600706,"digest":"9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb","integrity":"sha256-nPvD15JZmh0N5ce4QgnhwrLmAzbw8B4Z8FgWY5GHCPs="},"application-5eb87c6e13676d0183317debce17fade27e68c4acee28c419438da15d53c94f2.css":{"logical_path":"application.css","mtime":"2019-09-11T16:20:07+08:00","size":1844002,"digest":"5eb87c6e13676d0183317debce17fade27e68c4acee28c419438da15d53c94f2","integrity":"sha256-Xrh8bhNnbQGDMX3rzhf63ifmjErO4oxBlDjaFdU8lPI="},"admin-c9e5ebe6191548550e27514196ea125cfbb402820ec125a0c9acf99d2d378fe4.js":{"logical_path":"admin.js","mtime":"2019-09-21T15:28:08+08:00","size":4382031,"digest":"c9e5ebe6191548550e27514196ea125cfbb402820ec125a0c9acf99d2d378fe4","integrity":"sha256-yeXr5hkVSFUOJ1FBluoSXPu0AoIOwSWgyaz5nS03j+Q="},"admin-59c59f8cae8bef4a8359286c985458110c9d03ea121516595c988943f4717c38.css":{"logical_path":"admin.css","mtime":"2019-09-21T14:49:04+08:00","size":840093,"digest":"59c59f8cae8bef4a8359286c985458110c9d03ea121516595c988943f4717c38","integrity":"sha256-WcWfjK6L70qDWShsmFRYEQydA+oSFRZZXJiJQ/RxfDg="},"college-38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437.css":{"logical_path":"college.css","mtime":"2019-09-16T13:56:09+08:00","size":579109,"digest":"38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437","integrity":"sha256-OPlT1rpbhdP6tjyzwrvw0FfMxkVNB8+q+sOwbaN7hDc="},"application-646b1158a4e8c1f13e684d6fe9025abc75f8d3ba5256e440802c0398223374f3.css":{"logical_path":"application.css","mtime":"2019-09-21T14:49:04+08:00","size":1988767,"digest":"646b1158a4e8c1f13e684d6fe9025abc75f8d3ba5256e440802c0398223374f3","integrity":"sha256-ZGsRWKTowfE+aE1v6QJavHX407pSVuRAgCwDmCIzdPM="},"admin-a47e37c0ec7cf5f22380249776d1e82d65b6b6aa272ed7389185aa200fa40751.js":{"logical_path":"admin.js","mtime":"2019-09-25T15:33:05+08:00","size":4383107,"digest":"a47e37c0ec7cf5f22380249776d1e82d65b6b6aa272ed7389185aa200fa40751","integrity":"sha256-pH43wOx89fIjgCSXdtHoLWW2tqonLtc4kYWqIA+kB1E="},"admin-432c4eac09b036c57ff1e88d902b8aa7df81164e4b419bac557cf1366c1d3ad9.js":{"logical_path":"admin.js","mtime":"2019-09-25T15:35:20+08:00","size":4383103,"digest":"432c4eac09b036c57ff1e88d902b8aa7df81164e4b419bac557cf1366c1d3ad9","integrity":"sha256-QyxOrAmwNsV/8eiNkCuKp9+BFk5LQZusVXzxNmwdOtk="},"admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js":{"logical_path":"admin.js","mtime":"2019-09-30T14:43:41+08:00","size":4387200,"digest":"978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a","integrity":"sha256-l45c5gf3fCaBShdPSA2nmsJGwiAYaO+EZUqgO7Zye1o="},"admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css":{"logical_path":"admin.css","mtime":"2019-09-30T14:43:41+08:00","size":842269,"digest":"896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7","integrity":"sha256-iWKB9HMXIrDAhNuxryHQ80pbwULViv9Xs5GGSrcd3Kc="},"application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css":{"logical_path":"application.css","mtime":"2019-09-30T14:43:41+08:00","size":1993118,"digest":"97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd","integrity":"sha256-l/MT6bt9JUdmSffXIVlZz0IUgP0KN4XRlWlTv5Sh6L0="}},"assets":{"admin.js":"admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js","admin.css":"admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css","font-awesome/fontawesome-webfont.eot":"font-awesome/fontawesome-webfont-7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979.eot","font-awesome/fontawesome-webfont.woff2":"font-awesome/fontawesome-webfont-2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe.woff2","font-awesome/fontawesome-webfont.woff":"font-awesome/fontawesome-webfont-ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07.woff","font-awesome/fontawesome-webfont.ttf":"font-awesome/fontawesome-webfont-aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8.ttf","font-awesome/fontawesome-webfont.svg":"font-awesome/fontawesome-webfont-ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4.svg","college.js":"college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js","college.css":"college-38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437.css","logo.png":"logo-7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423.png","application.js":"application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js","application.css":"application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css"}}
\ No newline at end of file
+{"files":{"admin-cd9ca8bacc973ce2dbace30c97f6c40bc08e2c2ee44972f668e738e1902c0121.js":{"logical_path":"admin.js","mtime":"2019-09-11T16:20:07+08:00","size":4350881,"digest":"cd9ca8bacc973ce2dbace30c97f6c40bc08e2c2ee44972f668e738e1902c0121","integrity":"sha256-zZyousyXPOLbrOMMl/bEC8COLC7kSXL2aOc44ZAsASE="},"admin-a1b3356efe50ff4717cf22475639b5333c5354ba03fd107c9b7a8d4ae76f47aa.css":{"logical_path":"admin.css","mtime":"2019-09-11T16:20:07+08:00","size":773445,"digest":"a1b3356efe50ff4717cf22475639b5333c5354ba03fd107c9b7a8d4ae76f47aa","integrity":"sha256-obM1bv5Q/0cXzyJHVjm1MzxTVLoD/RB8m3qNSudvR6o="},"font-awesome/fontawesome-webfont-7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979.eot":{"logical_path":"font-awesome/fontawesome-webfont.eot","mtime":"2019-08-14T17:22:43+08:00","size":165742,"digest":"7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979","integrity":"sha256-e/yrbbmdXPvxcFygU23ceFhUMsxfpBu9etDwCQM7KXk="},"font-awesome/fontawesome-webfont-2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe.woff2":{"logical_path":"font-awesome/fontawesome-webfont.woff2","mtime":"2019-08-14T17:22:43+08:00","size":77160,"digest":"2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe","integrity":"sha256-Kt78vAQefRj88tQXh53FoJmXqmTWdbejxLbOM9oT8/4="},"font-awesome/fontawesome-webfont-ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07.woff":{"logical_path":"font-awesome/fontawesome-webfont.woff","mtime":"2019-08-14T17:22:43+08:00","size":98024,"digest":"ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07","integrity":"sha256-ugxZ3rVFD1y0Gz+TYJ7i0NmVQVh33foiPoqKdTNHTwc="},"font-awesome/fontawesome-webfont-aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8.ttf":{"logical_path":"font-awesome/fontawesome-webfont.ttf","mtime":"2019-08-14T17:22:43+08:00","size":165548,"digest":"aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8","integrity":"sha256-qljzPyOaD7AvXHpsRcBD16msmgkzNYBmlOzW1O3A1qg="},"font-awesome/fontawesome-webfont-ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4.svg":{"logical_path":"font-awesome/fontawesome-webfont.svg","mtime":"2019-08-14T17:22:43+08:00","size":444379,"digest":"ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4","integrity":"sha256-rWFXkmwWIrpOHQPUePFUE2hSS/xG9R5C/g2UX37zI+Q="},"college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js":{"logical_path":"college.js","mtime":"2019-09-26T14:40:40+08:00","size":3352744,"digest":"18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287","integrity":"sha256-GPXoQAMxY06JijWswhh4FcCWwl4Kt0q6NBrpFhZs0oc="},"college-944d4273f62c7538368b9017fdd3387b5e3bea31a87873770eb231324546d4d9.css":{"logical_path":"college.css","mtime":"2019-09-11T16:20:07+08:00","size":546841,"digest":"944d4273f62c7538368b9017fdd3387b5e3bea31a87873770eb231324546d4d9","integrity":"sha256-lE1Cc/YsdTg2i5AX/dM4e1476jGoeHN3DrIxMkVG1Nk="},"logo-7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423.png":{"logical_path":"logo.png","mtime":"2019-09-03T08:55:53+08:00","size":2816,"digest":"7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423","integrity":"sha256-f/ESVocJv5f5iY/ockm3qPIA/x9I1TfYWvhyFfGHBCM="},"application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js":{"logical_path":"application.js","mtime":"2019-09-26T14:40:40+08:00","size":600706,"digest":"9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb","integrity":"sha256-nPvD15JZmh0N5ce4QgnhwrLmAzbw8B4Z8FgWY5GHCPs="},"application-5eb87c6e13676d0183317debce17fade27e68c4acee28c419438da15d53c94f2.css":{"logical_path":"application.css","mtime":"2019-09-11T16:20:07+08:00","size":1844002,"digest":"5eb87c6e13676d0183317debce17fade27e68c4acee28c419438da15d53c94f2","integrity":"sha256-Xrh8bhNnbQGDMX3rzhf63ifmjErO4oxBlDjaFdU8lPI="},"admin-c9e5ebe6191548550e27514196ea125cfbb402820ec125a0c9acf99d2d378fe4.js":{"logical_path":"admin.js","mtime":"2019-09-21T15:28:08+08:00","size":4382031,"digest":"c9e5ebe6191548550e27514196ea125cfbb402820ec125a0c9acf99d2d378fe4","integrity":"sha256-yeXr5hkVSFUOJ1FBluoSXPu0AoIOwSWgyaz5nS03j+Q="},"admin-59c59f8cae8bef4a8359286c985458110c9d03ea121516595c988943f4717c38.css":{"logical_path":"admin.css","mtime":"2019-09-21T14:49:04+08:00","size":840093,"digest":"59c59f8cae8bef4a8359286c985458110c9d03ea121516595c988943f4717c38","integrity":"sha256-WcWfjK6L70qDWShsmFRYEQydA+oSFRZZXJiJQ/RxfDg="},"college-38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437.css":{"logical_path":"college.css","mtime":"2019-09-16T13:56:09+08:00","size":579109,"digest":"38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437","integrity":"sha256-OPlT1rpbhdP6tjyzwrvw0FfMxkVNB8+q+sOwbaN7hDc="},"application-646b1158a4e8c1f13e684d6fe9025abc75f8d3ba5256e440802c0398223374f3.css":{"logical_path":"application.css","mtime":"2019-09-21T14:49:04+08:00","size":1988767,"digest":"646b1158a4e8c1f13e684d6fe9025abc75f8d3ba5256e440802c0398223374f3","integrity":"sha256-ZGsRWKTowfE+aE1v6QJavHX407pSVuRAgCwDmCIzdPM="},"admin-a47e37c0ec7cf5f22380249776d1e82d65b6b6aa272ed7389185aa200fa40751.js":{"logical_path":"admin.js","mtime":"2019-09-25T15:33:05+08:00","size":4383107,"digest":"a47e37c0ec7cf5f22380249776d1e82d65b6b6aa272ed7389185aa200fa40751","integrity":"sha256-pH43wOx89fIjgCSXdtHoLWW2tqonLtc4kYWqIA+kB1E="},"admin-432c4eac09b036c57ff1e88d902b8aa7df81164e4b419bac557cf1366c1d3ad9.js":{"logical_path":"admin.js","mtime":"2019-09-25T15:35:20+08:00","size":4383103,"digest":"432c4eac09b036c57ff1e88d902b8aa7df81164e4b419bac557cf1366c1d3ad9","integrity":"sha256-QyxOrAmwNsV/8eiNkCuKp9+BFk5LQZusVXzxNmwdOtk="},"admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js":{"logical_path":"admin.js","mtime":"2019-09-30T14:43:41+08:00","size":4387200,"digest":"978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a","integrity":"sha256-l45c5gf3fCaBShdPSA2nmsJGwiAYaO+EZUqgO7Zye1o="},"admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css":{"logical_path":"admin.css","mtime":"2019-09-30T14:43:41+08:00","size":842269,"digest":"896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7","integrity":"sha256-iWKB9HMXIrDAhNuxryHQ80pbwULViv9Xs5GGSrcd3Kc="},"application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css":{"logical_path":"application.css","mtime":"2019-09-30T14:43:41+08:00","size":1993118,"digest":"97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd","integrity":"sha256-l/MT6bt9JUdmSffXIVlZz0IUgP0KN4XRlWlTv5Sh6L0="},"admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js":{"logical_path":"admin.js","mtime":"2019-10-11T14:38:33+08:00","size":4394616,"digest":"2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888","integrity":"sha256-LNsjRC+nNQJThbiPKQDfBP7zi2FTAEGm2+N17w8K6Ig="},"admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css":{"logical_path":"admin.css","mtime":"2019-10-10T17:12:05+08:00","size":846514,"digest":"2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc","integrity":"sha256-LChUuaAhWN7VqAmq9xRKhjCxA1SrTlb+zE3/zHE3lsw="},"application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css":{"logical_path":"application.css","mtime":"2019-10-10T17:12:05+08:00","size":2001607,"digest":"50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af","integrity":"sha256-UAWa6SmGYEO0cBUShwL8+6U9MqLfFI5k4dlhwQZRxq8="}},"assets":{"admin.js":"admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js","admin.css":"admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css","font-awesome/fontawesome-webfont.eot":"font-awesome/fontawesome-webfont-7bfcab6db99d5cfbf1705ca0536ddc78585432cc5fa41bbd7ad0f009033b2979.eot","font-awesome/fontawesome-webfont.woff2":"font-awesome/fontawesome-webfont-2adefcbc041e7d18fcf2d417879dc5a09997aa64d675b7a3c4b6ce33da13f3fe.woff2","font-awesome/fontawesome-webfont.woff":"font-awesome/fontawesome-webfont-ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07.woff","font-awesome/fontawesome-webfont.ttf":"font-awesome/fontawesome-webfont-aa58f33f239a0fb02f5c7a6c45c043d7a9ac9a093335806694ecd6d4edc0d6a8.ttf","font-awesome/fontawesome-webfont.svg":"font-awesome/fontawesome-webfont-ad6157926c1622ba4e1d03d478f1541368524bfc46f51e42fe0d945f7ef323e4.svg","college.js":"college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js","college.css":"college-38f953d6ba5b85d3fab63cb3c2bbf0d057ccc6454d07cfaafac3b06da37b8437.css","logo.png":"logo-7ff112568709bf97f9898fe87249b7a8f200ff1f48d537d85af87215f1870423.png","application.js":"application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js","application.css":"application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css"}}
\ No newline at end of file
diff --git a/public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css b/public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css
similarity index 99%
rename from public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css
rename to public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css
index fe1e75888..5b6bef77a 100644
--- a/public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css
+++ b/public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css
@@ -25274,6 +25274,114 @@ input.form-control {
   color: #6c757d;
 }
 
+/* line 4, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-pack: center;
+          justify-content: center;
+  flex-wrap: wrap;
+}
+
+/* line 9, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user .laboratory-user-item {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-align: center;
+          align-items: center;
+  height: 22px;
+  line-height: 22px;
+  padding: 2px 5px;
+  margin: 2px 2px;
+  border: 1px solid #91D5FF;
+  background-color: #E6F7FF;
+  color: #91D5FF;
+  border-radius: 4px;
+}
+
+/* line 27, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item {
+  display: -webkit-box;
+  display: flex;
+}
+
+/* line 30, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-img, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-img {
+  display: block;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 36, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  width: 80px;
+  height: 80px;
+  background: #F5F5F5;
+  border: 1px solid #E5E5E5;
+}
+
+/* line 45, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::before, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::before {
+  content: '';
+  position: absolute;
+  top: 27px;
+  left: 39px;
+  width: 2px;
+  height: 26px;
+  background: #E5E5E5;
+}
+
+/* line 55, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::after, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::after {
+  content: '';
+  position: absolute;
+  top: 39px;
+  left: 27px;
+  width: 26px;
+  height: 2px;
+  background: #E5E5E5;
+}
+
+/* line 66, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left {
+  position: relative;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 72, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload {
+  display: none;
+}
+
+/* line 77, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload {
+  display: block;
+  background: rgba(145, 145, 145, 0.8);
+}
+
+/* line 85, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-right, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-right {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+          flex-direction: column;
+  -webkit-box-pack: justify;
+          justify-content: space-between;
+  color: #777777;
+  font-size: 12px;
+}
+
+/* line 93, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-title, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-title {
+  color: #23272B;
+  font-size: 14px;
+}
+
 /* line 4, app/assets/stylesheets/admins/library_applies.scss */
 .admins-library-applies-index-page .library-applies-list-container span.apply-status-agreed {
   color: #28a745;
diff --git a/public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css.gz b/public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css.gz
similarity index 78%
rename from public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css.gz
rename to public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css.gz
index 581305db5..bfac93da9 100644
Binary files a/public/assets/admin-896281f4731722b0c084dbb1af21d0f34a5bc142d58aff57b391864ab71ddca7.css.gz and b/public/assets/admin-2c2854b9a02158ded5a809aaf7144a8630b10354ab4e56fecc4dffcc713796cc.css.gz differ
diff --git a/public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js b/public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js
similarity index 99%
rename from public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js
rename to public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js
index 49e1025c7..6203a3ce8 100644
--- a/public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js
+++ b/public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js
@@ -134528,6 +134528,256 @@ $(document).on('turbolinks:load', function() {
   }
 })
 ;
+$(document).on('turbolinks:load', function() {
+  if ($('body.admins-laboratory-settings-show-page, body.admins-laboratory-settings-update-page').length > 0) {
+    var $container = $('.edit-laboratory-setting-container');
+    var $form = $container.find('.edit_laboratory');
+
+    $('.logo-item-left').on("change", 'input[type="file"]', function () {
+      var $fileInput = $(this);
+      var file = this.files[0];
+      var imageType = /image.*/;
+      if (file && file.type.match(imageType)) {
+        var reader = new FileReader();
+        reader.onload = function () {
+          var $box = $fileInput.parent();
+          $box.find('img').attr('src', reader.result).css('display', 'block');
+          $box.addClass('has-img');
+        };
+        reader.readAsDataURL(file);
+      } else {
+      }
+    });
+
+    createMDEditor('laboratory-footer-editor', { height: 200, placeholder: '请输入备案信息' });
+
+    $form.validate({
+      errorElement: 'span',
+      errorClass: 'danger text-danger',
+      errorPlacement:function(error,element){
+        if(element.parent().hasClass("input-group")){
+          element.parent().after(error);
+        }else{
+          element.after(error)
+        }
+      },
+      rules: {
+        identifier: {
+          required: true,
+          checkSite: true
+        },
+        name: {
+          required: true
+        }
+      }
+    });
+    $.validator.addMethod("checkSite",function(value,element,params){
+      var checkSite = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
+      return this.optional(element)||(checkSite.test(value + '.educoder.com'));
+    },"域名不合法!");
+
+    $form.on('click', '.submit-btn', function(){
+      $form.find('.submit-btn').attr('disabled', 'disabled');
+      $form.find('.error').html('');
+      var valid = $form.valid();
+
+      $('input[name="navbar[][name]"]').each(function(_, e){
+        var $ele = $(e);
+        if($ele.val() === undefined || $ele.val().length === 0){
+          $ele.addClass('danger text-danger');
+          valid = false;
+        } else {
+          $ele.removeClass('danger text-danger');
+        }
+      });
+
+      if(!valid) return;
+      $.ajax({
+        method: 'PATCH',
+        dataType: 'json',
+        url: $form.attr('action'),
+        data: new FormData($form[0]),
+        processData: false,
+        contentType: false,
+        success: function(data){
+          $.notify({ message: '保存成功' });
+          window.location.reload();
+        },
+        error: function(res){
+          var data = res.responseJSON;
+          $form.find('.error').html(data.message);
+        },
+        complete: function(){
+          $form.find('.submit-btn').attr('disabled', false);
+        }
+      });
+    })
+  }
+});
+$(document).on('turbolinks:load', function() {
+  if ($('body.admins-laboratories-index-page').length > 0) {
+    var $searchContainer = $('.laboratory-list-form');
+    var $searchForm = $searchContainer.find('form.search-form');
+    var $list = $('.laboratory-list-container');
+
+    // ============== 新建 ===============
+    var $modal = $('.modal.admin-create-laboratory-modal');
+    var $form = $modal.find('form.admin-create-laboratory-form');
+    var $schoolSelect = $modal.find('.school-select');
+
+    $form.validate({
+      errorElement: 'span',
+      errorClass: 'danger text-danger',
+      rules: {
+        school_id: {
+          required: true
+        }
+      },
+      messages: {
+        school_id: {
+          required: '请选择所属单位'
+        }
+      }
+    });
+
+    // modal ready fire
+    $modal.on('show.bs.modal', function () {
+      $schoolSelect.select2('val', ' ');
+    });
+
+    // ************** 学校选择 *************
+    var matcherFunc = function(params, data){
+      if ($.trim(params.term) === '') {
+        return data;
+      }
+      if (typeof data.text === 'undefined') {
+        return null;
+      }
+
+      if (data.name && data.name.indexOf(params.term) > -1) {
+        var modifiedData = $.extend({}, data, true);
+        return modifiedData;
+      }
+
+      // Return `null` if the term should not be displayed
+      return null;
+    };
+
+    var defineSchoolSelect = function(schools) {
+      $schoolSelect.select2({
+        theme: 'bootstrap4',
+        placeholder: '请选择单位',
+        minimumInputLength: 1,
+        data: schools,
+        templateResult: function (item) {
+          if(!item.id || item.id === '') return item.text;
+          return item.name;
+        },
+        templateSelection: function(item){
+          if (item.id) {
+            $('#school_id').val(item.id);
+          }
+          return item.name || item.text;
+        },
+        matcher: matcherFunc
+      });
+    }
+
+    $.ajax({
+      url: '/api/schools/for_option.json',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        defineSchoolSelect(data.schools);
+      }
+    });
+
+    $modal.on('click', '.submit-btn', function(){
+      $form.find('.error').html('');
+
+      if ($form.valid()) {
+        var url = $form.data('url');
+
+        $.ajax({
+          method: 'POST',
+          dataType: 'json',
+          url: url,
+          data: $form.serialize(),
+          success: function(){
+            $.notify({ message: '创建成功' });
+            $modal.modal('hide');
+
+            setTimeout(function(){
+              window.location.reload();
+            }, 500);
+          },
+          error: function(res){
+            var data = res.responseJSON;
+            $form.find('.error').html(data.message);
+          }
+        });
+      }
+    });
+
+    // ============= 添加管理员 ==============
+    var $addMemberModal = $('.admin-add-laboratory-user-modal');
+    var $addMemberForm = $addMemberModal.find('.admin-add-laboratory-user-form');
+    var $memberSelect = $addMemberModal.find('.laboratory-user-select');
+    var $laboratoryIdInput = $addMemberForm.find('input[name="laboratory_id"]')
+
+    $addMemberModal.on('show.bs.modal', function(event){
+      var $link = $(event.relatedTarget);
+      var laboratoryId = $link.data('laboratory-id');
+      $laboratoryIdInput.val(laboratoryId);
+
+      $memberSelect.select2('val', ' ');
+    });
+
+    $memberSelect.select2({
+      theme: 'bootstrap4',
+      placeholder: '请输入要添加的管理员姓名',
+      multiple: true,
+      minimumInputLength: 1,
+      ajax: {
+        delay: 500,
+        url: '/admins/users',
+        dataType: 'json',
+        data: function(params){
+          return { name: params.term };
+        },
+        processResults: function(data){
+          return { results: data.users }
+        }
+      },
+      templateResult: function (item) {
+        if(!item.id || item.id === '') return item.text;
+        return item.real_name;
+      },
+      templateSelection: function(item){
+        if (item.id) {
+        }
+        return item.real_name || item.text;
+      }
+    });
+
+    $addMemberModal.on('click', '.submit-btn', function(){
+      $addMemberForm.find('.error').html('');
+
+      var laboratoryId = $laboratoryIdInput.val();
+      var memberIds = $memberSelect.val();
+      if (laboratoryId && memberIds && memberIds.length > 0) {
+        $.ajax({
+          method: 'POST',
+          dataType: 'script',
+          url: '/admins/laboratories/' + laboratoryId + '/laboratory_user',
+          data: { user_ids: memberIds }
+        });
+      } else {
+        $addMemberModal.modal('hide');
+      }
+    });
+  }
+});
 $(document).on('turbolinks:load', function() {
   if ($('body.admins-library-applies-index-page').length > 0) {
     var $searchFrom = $('.library-applies-list-form');
diff --git a/public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js.gz b/public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js.gz
similarity index 98%
rename from public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js.gz
rename to public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js.gz
index 13e36b4f6..492769a78 100644
Binary files a/public/assets/admin-978e5ce607f77c26814a174f480da79ac246c2201868ef84654aa03bb6727b5a.js.gz and b/public/assets/admin-2cdb23442fa735025385b88f2900df04fef38b61530041a6dbe375ef0f0ae888.js.gz differ
diff --git a/public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css b/public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css
similarity index 99%
rename from public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css
rename to public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css
index c1d8ae955..f62f2f56d 100644
--- a/public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css
+++ b/public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css
@@ -25274,6 +25274,114 @@ input.form-control {
   color: #6c757d;
 }
 
+/* line 4, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-pack: center;
+          justify-content: center;
+  flex-wrap: wrap;
+}
+
+/* line 9, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user .laboratory-user-item {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-align: center;
+          align-items: center;
+  height: 22px;
+  line-height: 22px;
+  padding: 2px 5px;
+  margin: 2px 2px;
+  border: 1px solid #91D5FF;
+  background-color: #E6F7FF;
+  color: #91D5FF;
+  border-radius: 4px;
+}
+
+/* line 27, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item {
+  display: -webkit-box;
+  display: flex;
+}
+
+/* line 30, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-img, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-img {
+  display: block;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 36, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  width: 80px;
+  height: 80px;
+  background: #F5F5F5;
+  border: 1px solid #E5E5E5;
+}
+
+/* line 45, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::before, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::before {
+  content: '';
+  position: absolute;
+  top: 27px;
+  left: 39px;
+  width: 2px;
+  height: 26px;
+  background: #E5E5E5;
+}
+
+/* line 55, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::after, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::after {
+  content: '';
+  position: absolute;
+  top: 39px;
+  left: 27px;
+  width: 26px;
+  height: 2px;
+  background: #E5E5E5;
+}
+
+/* line 66, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left {
+  position: relative;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 72, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload {
+  display: none;
+}
+
+/* line 77, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload {
+  display: block;
+  background: rgba(145, 145, 145, 0.8);
+}
+
+/* line 85, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-right, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-right {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+          flex-direction: column;
+  -webkit-box-pack: justify;
+          justify-content: space-between;
+  color: #777777;
+  font-size: 12px;
+}
+
+/* line 93, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-title, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-title {
+  color: #23272B;
+  font-size: 14px;
+}
+
 /* line 4, app/assets/stylesheets/admins/library_applies.scss */
 .admins-library-applies-index-page .library-applies-list-container span.apply-status-agreed {
   color: #28a745;
@@ -26246,6 +26354,113 @@ input.form-control {
 .admins-identity-authentications-index-page .identity-authentication-list-container span.apply-status-3 {
   color: #6c757d;
 }
+/* line 4, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-pack: center;
+          justify-content: center;
+  flex-wrap: wrap;
+}
+
+/* line 9, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratories-index-page .laboratory-list-table .member-container .laboratory-user .laboratory-user-item {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-align: center;
+          align-items: center;
+  height: 22px;
+  line-height: 22px;
+  padding: 2px 5px;
+  margin: 2px 2px;
+  border: 1px solid #91D5FF;
+  background-color: #E6F7FF;
+  color: #91D5FF;
+  border-radius: 4px;
+}
+
+/* line 27, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item {
+  display: -webkit-box;
+  display: flex;
+}
+
+/* line 30, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-img, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-img {
+  display: block;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 36, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  width: 80px;
+  height: 80px;
+  background: #F5F5F5;
+  border: 1px solid #E5E5E5;
+}
+
+/* line 45, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::before, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::before {
+  content: '';
+  position: absolute;
+  top: 27px;
+  left: 39px;
+  width: 2px;
+  height: 26px;
+  background: #E5E5E5;
+}
+
+/* line 55, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-upload::after, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-upload::after {
+  content: '';
+  position: absolute;
+  top: 39px;
+  left: 27px;
+  width: 26px;
+  height: 2px;
+  background: #E5E5E5;
+}
+
+/* line 66, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left {
+  position: relative;
+  width: 80px;
+  height: 80px;
+}
+
+/* line 72, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img .logo-item-upload {
+  display: none;
+}
+
+/* line 77, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-left.has-img:hover .logo-item-upload {
+  display: block;
+  background: rgba(145, 145, 145, 0.8);
+}
+
+/* line 85, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-right, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-right {
+  display: -webkit-box;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+          flex-direction: column;
+  -webkit-box-pack: justify;
+          justify-content: space-between;
+  color: #777777;
+  font-size: 12px;
+}
+
+/* line 93, app/assets/stylesheets/admins/laboratories.scss */
+.admins-laboratory-settings-show-page .edit-laboratory-setting-container .logo-item-title, .admins-laboratory-settings-update-page .edit-laboratory-setting-container .logo-item-title {
+  color: #23272B;
+  font-size: 14px;
+}
 /* line 4, app/assets/stylesheets/admins/library_applies.scss */
 .admins-library-applies-index-page .library-applies-list-container span.apply-status-agreed {
   color: #28a745;
diff --git a/public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css.gz b/public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css.gz
new file mode 100644
index 000000000..f5162f95a
Binary files /dev/null and b/public/assets/application-50059ae929866043b47015128702fcfba53d32a2df148e64e1d961c10651c6af.css.gz differ
diff --git a/public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css.gz b/public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css.gz
deleted file mode 100644
index 60ba5db0b..000000000
Binary files a/public/assets/application-97f313e9bb7d25476649f7d7215959cf421480fd0a3785d1956953bf94a1e8bd.css.gz and /dev/null differ
diff --git a/public/assets/application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js.gz b/public/assets/application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js.gz
index af63ccbfc..d17b8f444 100644
Binary files a/public/assets/application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js.gz and b/public/assets/application-9cfbc3d792599a1d0de5c7b84209e1c2b2e60336f0f01e19f0581663918708fb.js.gz differ
diff --git a/public/assets/college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js.gz b/public/assets/college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js.gz
index 3a4c01edc..d4b8b22dc 100644
Binary files a/public/assets/college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js.gz and b/public/assets/college-18f5e8400331634e898a35acc2187815c096c25e0ab74aba341ae916166cd287.js.gz differ
diff --git a/public/react/config/webpack.config.dev.js b/public/react/config/webpack.config.dev.js
index fe525154f..ef38a18f8 100644
--- a/public/react/config/webpack.config.dev.js
+++ b/public/react/config/webpack.config.dev.js
@@ -32,7 +32,7 @@ module.exports = {
   // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.s
 	// devtool: "cheap-module-eval-source-map",
   // 开启调试
-  devtool: "source-map",  // 开启调试
+  //devtool: "source-map",  // 开启调试
   // These are the "entry points" to our application.
   // This means they will be the "root" imports that are included in JS bundle.
   // The first two entry points enable "hot" CSS and auto-refreshes for JS.
diff --git a/public/react/public/index.html b/public/react/public/index.html
index 321fbe0e5..f6eef196a 100755
--- a/public/react/public/index.html
+++ b/public/react/public/index.html
@@ -8,7 +8,9 @@
       
     
      {
diff --git a/public/react/src/common/course/WordsBtn.js b/public/react/src/common/course/WordsBtn.js
index 85a85cfb6..68b278507 100644
--- a/public/react/src/common/course/WordsBtn.js
+++ b/public/react/src/common/course/WordsBtn.js
@@ -8,20 +8,20 @@ class WordsBtn extends Component {
   }
 
   render() {
-    let{to, href,targets, style2 }=this.props
+    let{to, href,targets, style2, style, className, ...others }=this.props
     return(
       
         {
           to==undefined&&targets==undefined ?
           {this.props.children} :
 						targets!=undefined? {this.props.children} 
           :
            
diff --git a/public/react/src/modules/courses/ListPageIndex.js b/public/react/src/modules/courses/ListPageIndex.js
index 730a1bdce..8c5652838 100644
--- a/public/react/src/modules/courses/ListPageIndex.js
+++ b/public/react/src/modules/courses/ListPageIndex.js
@@ -33,6 +33,12 @@ const StudentsList= Loadable({
     loader: () => import('./members/studentsList'),
     loading: Loading,
 });
+//分班列表
+const CourseGroupList= Loadable({
+    loader: () => import('./members/CourseGroupList'),
+    loading: Loading,
+});
+
 const Eduinforms= Loadable({
   loader: () => import('./gradinforms/Eduinforms.js'),
   loading: Loading,
@@ -234,7 +240,7 @@ class ListPageIndex extends Component{
                                                 > 
                                                  ( 
 
diff --git a/public/react/src/modules/courses/Resource/Fileslistitem.js b/public/react/src/modules/courses/Resource/Fileslistitem.js
index 418c201b5..d364f7ada 100644
--- a/public/react/src/modules/courses/Resource/Fileslistitem.js
+++ b/public/react/src/modules/courses/Resource/Fileslistitem.js
@@ -275,25 +275,26 @@ class Fileslistitem extends Component{
                       `
                     }
                   
-                  {discussMessage.course_groups.length===0?"":
-                  
-                    {discussMessage.course_groups.map((item,key)=>{
-                    return(
-                      
-                        {item.course_group_name} 
-                        将发布于 { moment(item.course_group_publish_time).format('YYYY-MM-DD HH:mm')} 
-                      
-                    )
-                    })}
-
-                  }
+									{/*资源分班*/}
+                  {/*{discussMessage.course_groups.length===0?"":*/}
+                  {/**/}
+                    {/*{discussMessage.course_groups.map((item,key)=>{*/}
+                    {/*return(*/}
+                      {/*
*/}
+                        {/*{item.course_group_name} */}
+                        {/*将发布于 { moment(item.course_group_publish_time).format('YYYY-MM-DD HH:mm')} */}
+                      {/*
*/}
+                    {/*)*/}
+                    {/*})}*/}
+
+                  {/*}*/}
 
                     
                       
                         {discussMessage.author.name} 
                         大小 {discussMessage.filesize} 
                         下载 {discussMessage.downloads_count} 
-                        引用 {discussMessage.quotes} 
+                        {/*引用 {discussMessage.quotes} */}
                         
                             {/*{moment(discussMessage.publish_time).format('YYYY-MM-DD HH:mm:ss')}*/}
                             {/*{moment(discussMessage.publish_time).fromNow()}*/}
diff --git a/public/react/src/modules/courses/Resource/index.js b/public/react/src/modules/courses/Resource/index.js
index 1fea29f84..3423d87f0 100644
--- a/public/react/src/modules/courses/Resource/index.js
+++ b/public/react/src/modules/courses/Resource/index.js
@@ -51,8 +51,14 @@ class Fileslists extends Component{
     })
     if(this.props.match.params.main_id){
       this.seactall();
+      this.setState({
+         child:false,
+			})
     }else if(this.props.match.params.Id){
       this.seactall(parseInt(this.props.match.params.Id),1)
+			this.setState({
+				child:true,
+			})
     }
     this.updadatalist();
 		on('updateNavSuccess', this.updateNavSuccess)
@@ -74,9 +80,12 @@ class Fileslists extends Component{
 			this.setState({
 				isSpin:true,
 				checkBoxValues:[],
-				checkAllValue:false
+				checkAllValue:false,
 			})
       if(this.props.match.params.main_id!=undefined){
+				this.setState({
+					child:false,
+				})
         this.seactall();
       }
     }
@@ -84,31 +93,15 @@ class Fileslists extends Component{
 			this.setState({
 				isSpin:true,
 				checkBoxValues:[],
-				checkAllValue:false
+				checkAllValue:false,
 			})
       if(this.props.match.params.Id!=undefined){
+				this.setState({
+					child:true,
+				})
         this.seactall(parseInt(this.props.match.params.Id),1)
       }
     }
-    // if ( prevProps.match.params.Id != this.props.match.params.Id ||prevProps.isaloadtype!= this.props.isaloadtype) {
-    //   let lists=this.props.course_modules;
-    //   if(lists!=undefined){
-    //     debugger
-    //     let url=this.props.location.pathname;
-    //     lists.forEach((item,index)=>{
-    //       if(url===item.category_url){
-    //         this.seactall();
-    //       }
-    //       if(item.second_category!=undefined&&item.second_category.length!=0){
-    //         item.second_category.forEach((iem,key)=>{
-    //           if(url===iem.second_category_url){
-    //             this.seactall(parseInt(this.props.match.params.Id),2);
-    //           }
-    //         })
-    //       }
-    //     })
-    //   }
-    // }
   }
 
   updadatalist=(id)=>{
@@ -188,23 +181,27 @@ class Fileslists extends Component{
         course_second_category_id:id
       }
     }).then((result)=>{
-      // console.log(result)
-
-      if(result.status===200){
-        if(result.data.status===0){
-          let list=result.data.data;
-          this.setState({
-            total_count:list.total_count,
-						publish_count:list.publish_count,
-						unpublish_count:list.unpublish_count,
-            files:list.files,
-            filesId:list.id,
-            name:list.name,
-            course_is_public:result.data.data.course_is_public,
-            isSpin:false
-          })
-        }
-      }
+     if(result!=undefined){
+			 if(result.status===200){
+				 if(result.data.status===0){
+					 let list=result.data.data;
+					 this.setState({
+						 total_count:list.total_count,
+						 publish_count:list.publish_count,
+						 unpublish_count:list.unpublish_count,
+						 files:list.files,
+						 filesId:list.id,
+						 name:list.name,
+						 course_is_public:result.data.data.course_is_public,
+						 isSpin:false
+					 })
+				 }
+			 }
+		 }else{
+     	this.setState({
+				isSpin:false
+			})
+		 }
     }).catch((error)=>{
       console.log(error)
       this.setState({
@@ -399,11 +396,16 @@ class Fileslists extends Component{
   }
 
   addDir = () => {
-    let {filesId}=this.state;
+    let {filesId,course_modules}=this.state;
     this.setState({
 			checkBoxValues:[]
 		})
-    trigger('attachmentAddlog', parseInt(filesId))
+		if(parseInt(this.props.match.params.main_id)!=parseInt(this.props.coursesids)){
+			trigger('attachmentAddlog', parseInt(		course_modules&&course_modules.course_modules[0].id))
+		}else{
+			trigger('attachmentAddlog', parseInt(filesId))
+		}
+
   }
 
   editDir = (name) => {
@@ -677,11 +679,13 @@ class Fileslists extends Component{
       course_modules,
       shixunmodal,
       course_is_public,
-			filesId
+			filesId,
+			child
     } = this.state;
     let category_id= this.props.match.params.category_id;
 
 
+
     return(
         
 
@@ -766,7 +770,7 @@ class Fileslists extends Component{
               has_course_groups={this.state.has_course_groups}
               attachmentId={this.state.coursesecondcategoryid}
           />:""}
-
+ 					{/*设置资源*/}
           {Settingtype&&Settingtype===true?
-                    {this.props.isAdmin()?parseInt(this.props.match.params.main_id)===parseInt(this.props.coursesids)?this.addDir()} className={"mr30 font-16"}>添加目录 :"":""}
+                    {/*{this.props.isAdmin()?parseInt(this.props.match.params.main_id)===parseInt(this.props.coursesids)?this.addDir()} className={"mr30 font-16"}>新建目录 :"":""}*/}
+										{this.props.isAdmin()?this.addDir()} className={"mr30 font-16"}>新建目录 :""}
                     {this.props.isAdmin()?parseInt(this.props.match.params.main_id)!=parseInt(this.props.coursesids)?this.editDir(name)} className={"mr30 font-16"}>目录重命名 :"":""}
 
                     {this.props.isAdmin()||this.props.isStudent() ?  this.addResource()}>选用资源 :""}
@@ -869,7 +874,7 @@ class Fileslists extends Component{
                       {/*})}*/}
                       {this.props.isAdmin()?parseInt(this.props.match.params.main_id)===filesId&&filesId?
                         
-                          this.addDir()}>添加目录 
+                          this.addDir()}>新建目录 
                         
                         :"":""}
                     
@@ -885,7 +890,7 @@ class Fileslists extends Component{
                       {/*className={sorttype === 'created_on'?"none":""}  className={sorttype === 'quotes'?"none":""}  className={sorttype === 'downloads'?"none":""} */}
                        this.onSortTypeChange('created_on')}>更新时间排序 
                        this.onSortTypeChange('downloads')}>下载次数排序 
-                       this.onSortTypeChange('quotes')}>引用次数排序 
+                      {/* this.onSortTypeChange('quotes')}>引用次数排序 */}
                     
                   :""}
                 
diff --git a/public/react/src/modules/courses/boards/BoardsNew.js b/public/react/src/modules/courses/boards/BoardsNew.js
index 0222907d3..9ae74699f 100644
--- a/public/react/src/modules/courses/boards/BoardsNew.js
+++ b/public/react/src/modules/courses/boards/BoardsNew.js
@@ -287,11 +287,12 @@ class BoardsNew extends Component{
     const isAdmin = this.props.isAdmin()
     const courseId=this.props.match.params.coursesId;
     const boardId = this.props.match.params.boardId
-    const isCourseEnd = this.props.isCourseEnd()
+    const isCourseEnd = this.props.isCourseEnd();
+		document.title=this.props.coursedata&&this.props.coursedata.name;
     return(
         
           
                                this.refs['addDirModal'].open()}>
-                                
                             
                           }
diff --git a/public/react/src/modules/courses/boards/TopicDetail.js b/public/react/src/modules/courses/boards/TopicDetail.js
index 8ecc85565..d42b68c17 100644
--- a/public/react/src/modules/courses/boards/TopicDetail.js
+++ b/public/react/src/modules/courses/boards/TopicDetail.js
@@ -526,8 +526,10 @@ class TopicDetail extends Component {
       const isAdmin = this.props.isAdmin()
       // TODO 图片上传地址
       const courseId=this.props.match.params.coursesId;
-      const boardId = this.props.match.params.boardId
-      const isCourseEnd = this.props.isCourseEnd()
+      const boardId = this.props.match.params.boardId;
+      const isCourseEnd = this.props.isCourseEnd();
+
+			document.title=this.props.coursedata&&this.props.coursedata.name;
 	    return (
          {/* fl with100 */}
           :""
+				}
+				{
+					this.props.OneSelftype===true? 
+						
+
+
+							{ this.props.usingCheckBeforePost ?
+								
+									
+										发布设置均可修改, 
+										
+                点击修改
+                 
+									
+									
+										此设置将对所有分班生效
+									
+								  :
+								
+									
+										{this.props.Topval}
+										{this.props.Topvalright} 
+									
+									
+										{this.props.Botvalleft===undefined?"":"{this.props.Botvalleft}" }
+										{this.props.Botval}
+									
+								   }
+
+
+							{this.props.starttime===undefined||
+							this.props.starttime===""?""
+								: 
+									
+											发布时间: 
+										{this.props.starttime} 
+									{this.props.modaltype===undefined||this.props.modaltype===2?	
+									{/*{this.props.endtime}*/}
+								  截止时间: 
+									{this.state.endtimetypevalue}
:""}
+                 :""}
+								
}
+							{/* usingCheckBeforePost 为true的时候 全选所有分班 */}
+
+							
+							{this.props.modaltype===undefined||this.props.modaltype===2
+							|| this.props.usingCheckBeforePost ?"":
+								
+									分班名称 
+
+									截止时间 
+								 
+							}
+							{this.props.modaltype===undefined||this.props.modaltype===2
+							|| this.props.usingCheckBeforePost ?"":
+							}
+
+							
+
+						
 :""}
+			
{
-		let {course_groups}=this.state;
-		let newgroup_publish=course_groups;
-		for(var i=0; i{
-		let newlist=this.state.course_groups;
-		newlist.splice(key,1);
-		this.setState({
-			course_groups:newlist
-		})
-	}
-
-	addgrouppublish=()=>{
-		let newlist=this.state.course_groups;
-		newlist.push(  {
-			course_group_id : undefined,
-			publish_time :""
-			// moment(new Date()).format('YYYY-MM-DD HH:mm')
-		})
-		this.setState({
-			course_groups:newlist
-		})
-	}
 	render(){
-		let {is_public,unified_setting,course_groups,datatime,description,datalist,course_group_publish_timestype}=this.state;
+		let {datatime,description,datalist}=this.state;
 
 		const uploadProps = {
 			width: 600,
-			// https://github.com/ant-design/ant-design/issues/15505
-			// showUploadList={false},然后外部拿到 fileList 数组自行渲染列表。
-			// showUploadList: false,
 			action: `${getUrl()}/api/attachments.json`,
 			onChange: this.handleChange,
 			onRemove: this.onAttachmentRemove,
@@ -401,9 +262,11 @@ class Selectsetting extends Component{
 				return isLt150M;
 			},
 		};
-
-
-		// console.log(this.props.has_course_groups)
+		const radioStyle = {
+			display: 'block',
+			height: '30px',
+			lineHeight: '30px',
+		};
 		return(
 			
 				*/}
-							{/*{this.state.fileList.length===0?"":this.state.fileList.map((item,key)=>{*/}
-							{/*return(*/}
-							{/*
*/}
-							{/**/}
-							{/* */}
-							{/**/}
-							{/*{item.name}*/}
-							{/* */}
-							{/**/}
-							{/*{item.response===undefined?"":isNaN(bytesToSize(item.filesize))?"123":bytesToSize(item.filesize)}*/}
-							{/* */}
-							{/*this.onAttachmentRemove(item.response===undefined?"":item.response.id&&item.response.id)}> */}
-							{/*
*/}
-							{/*)*/}
-							{/*})}*/}
-
 							{this.state.newfileListtypes===true?
请先上传资源
:""}
 
 							
@@ -648,19 +480,14 @@ class Selectsetting extends Component{
                 }
               `}
 
-								{/*
*/}
-									{/**/}
-										{/*勾选后所有用户可见,否则仅课堂成员可见 */}
-									{/* */}
-								{/*
*/}
-								{/*{this.props.has_course_groups&&this.props.has_course_groups===true?:""}*/}
-								{this.state.course_groupss&&this.state.course_groupss.length>0?
-								统一设置 (选中则所有分班使用相同的发布设置,否则各个单独设置) 
-								 :""}
+								{this.props.course_is_public===true?
+									公开: 
+									选中,所有用户可见,否则课堂成员可见 
+								 
+								
:""}
+
+
+
 
 								
 
-
-								{/*this.props.has_course_groups&&this.props.has_course_groups===true?:""*/}
-								
-									{unified_setting===false?
-										this.state.course_groups&&this.state.course_groups.map((item,key)=>{
-											return(
-												
-
-
-													this.selectassigngroups(e,index,key)}
-													>
-														{	this.state.course_groupss&&this.state.course_groupss.map((item,key)=>{
-															return(
-																{item.name} 
-															)
-														})}
-													 
-
-
-													this.onChangeTimepublishs(e,index,key)}
-														// onChange={ this.onChangeTimepublish }
-														disabledTime={disabledDateTime}
-														disabledDate={disabledDate}
-													/>
-
-													{key!=0?this.deletegrouppublish(key)}> :""}
-													{key===course_groups.length-1? 
-											)
-										}):""}
-								
-
 							
 							
-							{unified_setting===true?
-								
-			              
-			                  
-								
:""}
-							{/*{this.state.course_group_idtypes===true?
请选择分班
:""}*/}
-
-							{/*{course_group_publish_timestype===true?
请填写完整
:""}*/}
+
+							
+								发布设置: 
+								
+									
+										立即发布
+									 
+									
+										延迟发布 
+										 
+									(按照设置的时间定时发布) 
+								 
+							
+