必须在模型/查询构建阶段显式控制租户ID注入,推荐继承Model封装TenantModel并重写newQuery统一绑定where('tenant_id', $this->getTenantId()),关联查询、事务、CLI及队列场景需单独处理租户上下文。

ThinkPHP 6 的 Db 查询如何自动注入租户 ID 条件
核心结论:不能依赖全局事件或中间件“统一加 where”,必须在模型/查询构建阶段显式控制,否则会漏掉原生 SQL、子查询、关联预加载等场景。
常见错误是把 tenant_id 塞进中间件的 Request 对象里,然后在模型的 boot 方法里用 where 绑定——这只能覆盖 find/select 等主表查询,一旦调用 Db::table('user')->where(...)->update() 或执行 with(['profile']),租户条件就丢了。
- 推荐做法:封装一个继承自
Model的基类TenantModel,重写newQuery方法,在返回的查询实例上统一绑定where('tenant_id', $this->getTenantId()) -
$this->getTenantId()应从请求上下文(如think\Request)获取,而非 Session 或配置文件——避免 CLI 或队列场景失效 - 注意:
newQuery不影响原生Db::name()调用,这类操作需单独封装tenantDb()工具函数
多租户下 belongsTo 关联为何查不到数据?
因为 ThinkPHP 默认关联不携带租户条件,即使主模型加了 tenant_id,关联模型仍按无租户逻辑查表。
例如:用户属于某个部门,部门表也需隔离租户,但 User::with('department') 默认只加 department_id 条件,不会加 tenant_id。
立即学习“PHP免费学习笔记(深入)”;
- 必须在关联定义里显式传入闭包条件:
return $this->belongsTo(Department::class, 'dept_id')->where('tenant_id', $this->tenant_id) - 更稳妥的方式是让
Department本身也继承TenantModel,并在其newQuery中注入条件——这样所有对 Department 的查询(包括关联、手动 newQuery)都自动带租户 - 注意:关联的
save和delete操作不受newQuery影响,需在关联模型的beforeWrite钩子里补全tenant_id
Db::transaction 中租户上下文丢失怎么办
事务内如果跨模型操作,且部分模型没走 TenantModel,或用了原生 Db::execute(),租户隔离就彻底失效。
典型现象:A 模型更新成功,B 模型却误改了其他租户的数据。
- 事务开始前,务必确认当前上下文租户 ID 已确定(比如从 JWT token 解析,而不是从 Cookie 读取)
- 禁止在事务中混用
Db::name()和Model::create()——统一走租户模型,或全部用封装好的tenantDb() - 如果必须执行原生 SQL,记得手动拼接
AND tenant_id = ?,并用参数绑定,别直接字符串拼接 - 测试时重点验证:事务回滚后,是否仍有脏数据残留?租户字段是否被意外清空?
CLI 命令和定时任务怎么带租户上下文
命令行没有 HTTP 请求,Request 对象为空,tenant_id 无法自动推导——这是最容易被忽略的隔离缺口。
表现是:后台导出、日志清理、缓存预热等任务,一跑就扫全库。
- 强制要求所有租户相关命令必须带
--tenant-id=xxx参数,启动时校验非空 - 避免使用
php think command直接调用,应封装一层入口脚本,先初始化租户上下文再执行业务逻辑 - 队列任务(如 Redis 队列)序列化时,必须显式把
tenant_id写入 job payload,消费时重新 set 到上下文,不能依赖闭包捕获



















