如何在Rails范围型多租户应用中实现跨公司管理的Agent模型
针对你的需求,我会一步步拆解如何让Agent切换管理的公司并查看对应报告,先从模型关系优化开始,再到切换逻辑和权限控制:
一、优化模型关系(可选但更合理)
你当前设计的Agent has_many :companies是一对多,但实际场景中可能一家公司也需要多个Agent管理,所以更推荐用多对多关联,通过中间表agent_companies来实现:
# agent.rb class Agent < ApplicationRecord has_many :agent_companies has_many :companies, through: :agent_companies end # agent_company.rb(生成模型:rails generate model AgentCompany agent:references company:references) class AgentCompany < ApplicationRecord belongs_to :agent belongs_to :company end # company.rb 保持原有,新增关联 class Company < ApplicationRecord has_many :agent_companies has_many :agents, through: :agent_companies # 原有关联 has_many :reports has_many :users end
如果坚持用一对多(Agent拥有多家公司),原模型关系可以保留,但多对多的扩展性更强,更适配实际业务场景。
二、实现公司切换逻辑
核心是在会话(session)中存储Agent当前选中的company_id,这样后续所有请求都能基于这个ID自动过滤资源。
1. 添加切换公司的控制器动作
可以单独创建一个控制器处理切换逻辑,或者挂载到Agent控制器中:
# app/controllers/company_switch_controller.rb class CompanySwitchController < ApplicationController before_action :authenticate_agent! # 假设你已实现Agent的登录验证 def update # 验证当前Agent是否有权访问该公司 company = current_agent.companies.find(params[:company_id]) session[:current_company_id] = company.id redirect_to request.referer || agent_reports_path, notice: "已切换到公司:#{company.name}" rescue ActiveRecord::RecordNotFound redirect_to request.referer, alert: "无权限访问该公司" end end
2. 配置路由
在routes.rb中添加切换路由:
post '/switch-company', to: 'company_switch#update', as: :switch_company
3. 视图层添加切换UI
在Agent专属的布局页面(比如导航栏)添加下拉选择框,让Agent快速切换公司:
<!-- app/views/layouts/agent_layout.html.erb --> <div class="company-switch-widget"> <%= form_tag switch_company_path, method: :post do %> <%= select_tag :company_id, options_from_collection_for_select(current_agent.companies, :id, :name, session[:current_company_id]), prompt: "选择管理的公司" %> <%= submit_tag "切换", class: "btn btn-sm" %> <% end %> </div>
三、全局资源范围限定
接下来要确保Agent访问的所有资源(Reports、Users等)都自动过滤到当前选中的公司,避免手动重复写查询条件。
1. 在ApplicationController中添加全局方法
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base # 针对Agent的控制器,添加前置动作 def set_current_company return unless current_agent # 仅Agent登录时生效 # 优先从会话取已选中的公司 if session[:current_company_id] @current_company = current_agent.companies.find_by(id: session[:current_company_id]) end # 如果会话中没有有效公司,默认选第一个有权限的公司 @current_company ||= current_agent.companies.first # 同步更新会话ID session[:current_company_id] = @current_company.id if @current_company end end
2. 在Agent相关控制器中应用过滤
比如创建Agent专属的ReportsController,和普通用户的控制器区分开:
# app/controllers/agent/reports_controller.rb class Agent::ReportsController < ApplicationController before_action :authenticate_agent! before_action :set_current_company def index # 自动过滤当前公司的所有报告,关联用户信息提升查询效率 @reports = @current_company.reports.includes(:user).order(created_at: :desc) end def show # 确保只能查看当前公司的报告 @report = @current_company.reports.find(params[:id]) end # edit、update等动作同理,都基于@current_company做范围限定 end
3. 模型层添加安全作用域(可选)
为了避免在控制器之外误获取到其他公司的资源,可以在模型中添加作用域:
# report.rb class Report < ApplicationRecord belongs_to :user belongs_to :company scope :for_company, ->(company_id) { where(company_id: company_id) } end
之后在控制器中可以用@current_company.reports或者Report.for_company(@current_company.id),效果一致,但作用域能让代码更具语义化。
四、权限验证强化
除了切换时的权限检查,还可以在控制器中添加全局权限验证,防止恶意请求:
# 在Agent的控制器中添加 def authorize_company_access unless current_agent.companies.exists?(@current_company.id) redirect_to agent_root_path, alert: "无权限访问该资源" end end
然后在需要的控制器动作前添加before_action :authorize_company_access,进一步加固权限。
这样一套流程下来,Agent就能顺利切换公司,并且只能访问自己负责的公司的报告和用户资源了。
内容的提问来源于stack exchange,提问作者aymorgan

