# 6.13 租户管理 :::tip 概述 租户管理是多租户体系的核心,支持租户的 CRUD、创建租户账号、查看租户用户。`TenantService` 继承 `BaseService`(租户表本身不做租户隔离),并通过悲观锁生成租户编码。 ::: ## 模块特点 | 特性 | 说明 | |------|------| | 基类 | `BaseService`(系统共享,租户表不隔离) | | Trait 组合 | `HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps, HasUniqueCheck` | | 文件字段 | `logo`(租户 Logo,保存到 tenant 目录) | | 唯一校验 | `code` 全局唯一 | | 枚举映射 | `status` → tenant_status 字典 | | 特殊接口 | `account`(创建租户账号)、`users/{tenantId}`(查看租户用户) | | 编码生成 | `beforeAdd` 悲观锁生成租户编码(T+4位序号,如 T0001) | | 编码保护 | `beforeUpdate` 禁止修改编码 | | 删除校验 | `beforeDelete` 检查租户下是否存在用户 | | 列表补全 | `afterPageList` 批量补充 `userCount`(避免 N+1) | ## API 接口 | 方法 | 路径 | 权限码 | 说明 | |------|------|--------|------| | GET | `/api/tenant/page` | sys:tenant:page | 分页列表(含 userCount) | | GET | `/api/tenant/list` | sys:tenant:list | 全量列表 | | GET | `/api/tenant/detail/{id}` | sys:tenant:detail | 详情 | | GET | `/api/tenant/users/{tenantId}` | — | 租户用户列表 | | POST | `/api/tenant/add` | sys:tenant:add | 新增租户 | | POST | `/api/tenant/account` | sys:tenant:add | 创建租户账号 | | PUT | `/api/tenant/update` | sys:tenant:update | 修改租户 | | DELETE | `/api/tenant/delete/{id}` | sys:tenant:delete | 删除租户 | | DELETE | `/api/tenant/batchDelete` | sys:tenant:batchDelete | 批量删除 | ## Service 配置与钩子(来自项目实际代码) ```php class TenantService extends BaseService { use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps, HasUniqueCheck; protected string $modelClass = Tenant::class; protected array $fileFields = ['logo']; protected string $fileSaveDir = 'tenant'; protected array $pageLikeFields = ['name', 'code', 'contact']; protected array $pageEqFields = ['status']; protected array $uniqueFields = ['code']; protected array $serializeMaps = [ 'status' => 'tenant_status', ]; } ``` ### 编码生成(beforeAdd 钩子) ```php /** * 新增前钩子:自动生成租户编码 * * 租户编码格式:T + 4位序号左补零(如 T0001、T0002)。 * 通过悲观锁获取当前最大编码并递增,避免并发重号。 * 编码在创建前生成,一次 SQL 即可完成,无需回写。 */ protected function beforeAdd(array $data): array { // 使用悲观锁防止并发生成相同编码 $lastCode = Tenant::withoutGlobalScopes() ->lockForUpdate() ->orderBy('id', 'desc') ->value('code'); $nextSeq = 1; if ($lastCode && preg_match('/^T(\d+)$/', $lastCode, $m)) { $nextSeq = (int) $m[1] + 1; } $data['code'] = 'T'.str_pad((string) $nextSeq, 4, '0', STR_PAD_LEFT); return $data; } ``` :::tip 悲观锁防重号 多管理员同时创建租户时,`lockForUpdate()` 在事务内锁定最大编码读取,保证序号递增不重号——生成发生在创建前,无需像"回写编码"方案那样二次 UPDATE。 ::: ### 编码保护(beforeUpdate 钩子) ```php protected function beforeUpdate(int $id, array $data): array { unset($data['code']); // 租户编码由系统自动生成,不允许手动修改 return $data; } ``` ### 删除保护(beforeDelete 钩子) ```php protected function beforeDelete(int $id): void { // 存在关联用户时不允许删除 $hasUsers = User::where('tenant_id', $id)->exists(); if ($hasUsers) { throw new BizException('该租户下存在用户,不可删除'); } } ``` :::tip 默认租户保护 默认租户(id=1)下必然存在用户(平台数据归属),因此用户数校验天然阻止删除默认租户。超级管理员(uid=1)不受租户隔离限制,创建租户隔离数据时自动代入默认租户(见 [5.8 多租户隔离](/zh/core/tenant.html))。 ::: ### 列表补全 userCount(afterPageList 钩子) ```php protected function afterPageList(array &$records, array $params): void { $tenantIds = array_column($records, 'id'); if (empty($tenantIds)) { return; } // 批量获取租户用户数:一次查询按 tenant_id 分组统计 $userCounts = User::whereIn('tenant_id', $tenantIds) ->where('is_delete', 0) ->selectRaw('tenant_id, count(*) as cnt') ->groupBy('tenant_id') ->pluck('cnt', 'tenant_id') ->toArray(); foreach ($records as &$item) { $item['userCount'] = $userCounts[$item['id']] ?? 0; } } ``` `/api/tenant/page` 和 `/api/tenant/list` 返回的每条记录自动包含 `userCount` 字段。 ## 租户用户接口 | 方法/服务 | 说明 | |------|------| | `GET /api/tenant/users/{tenantId}` | 租户用户列表(`getUsers()`:仅返回 id/username/realname/mobile/status/create_time 展示字段,不带敏感信息) | | `getUserCount($tenantId)` | 租户用户数(供统计使用) | ## 创建租户账号(POST /api/tenant/account) ```php /** * 创建租户管理员账号 * * 流程:校验租户 → 用户数上限校验 → 用户名全局唯一 → 组装角色 → 事务内委托 * UserService::add() 完成标准创建。编码生成、密码加密、城市拆分、 * 头像迁移与角色关联由其生命周期钩子完成。 * 未传 password 时使用默认密码。 */ public function createAccount(array $data): int { /* ... */ } ``` **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `tenantId` | int | 是 | 所属租户 | | `username` | string | 是 | 登录用户名 | | `realname` | string | 是 | 姓名 | | `password` | string | 否 | 密码(缺省用默认密码) | | `mobile` / `email` | string | 否 | 手机号 / 邮箱 | | `roles` | array | 否 | 角色ID数组(表单多选,推荐) | | `roleId` | int | 否 | 绑定角色(单角色兼容传参) | | `gender` | int | 否 | 性别:1=男 2=女 3=保密 | | `avatar` | string | 否 | 头像地址(正式路径,入库时去域名) | | `deptId` / `levelId` / `positionId` | int | 否 | 部门 / 职级 / 岗位 ID | | `city` | array | 是 | 省/市/区/街道四级编码数组 | | `address` | string | 是 | 详细地址 | | `status` | int | 否 | 状态:1=正常 2=禁用(默认 1) | | `sort` | int | 否 | 排序值 | | `intro` / `note` | string | 否 | 简介 / 备注 | :::tip 编码规则与用户模块一致 租户账号编码按 `租户编码-6位序号` 规则生成(如 `T0001-000001`),由标准创建流程的 `beforeAdd` 钩子统一生成——所有创建路径(直接添加、租户账号、导入)编码格式相同。 ::: ## 套餐化角色预置 系统内置三套租户角色模板,分配不同的菜单权限,用于按套餐等级快速授权: | 角色名称 | 定位 | 菜单范围 | |----------|------|----------| | 租户-标准版 | 基础功能 | 核心业务模块 | | 租户-专业版 | 进阶功能 | 核心 + 高级模块 | | 租户-旗舰版 | 全功能 | 全部菜单(最全) | **使用方式:** 创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限;创建租户管理员账号(`tenant/account`)时选择对应套餐角色即可完成授权。 :::tip 设计意图 套餐角色解决了"每个租户都要手动配一遍菜单"的重复工作。管理员创建租户时只需选择套餐,角色权限即刻生效。 ::: ## 演示数据 系统内置一组演示数据,可直接登录体验多租户效果: ### 平台级 | 账号 | 密码 | 角色 | 说明 | |------|------|------|------| | admin | 123456 | 超级管理员 | 平台管理,可管理所有租户 | ### 租户级 | 账号 | 密码 | 所属租户 | 角色 | 说明 | |------|------|----------|------|------| | tenant001 | 123456 | 客户租户A | 租户管理员 | 可体验租户视角的功能与数据隔离 | :::warning 演示环境注意 以上账号仅用于开发和演示环境,生产环境部署后请立即修改密码或删除演示数据。 ::: ## 租户状态 | 字段 | 说明 | |------|------| | `status` | 0=禁用 1=启用 | | `expire_time` | 过期时间,过期后 `ResolveTenant` 返回 403「租户已过期」 | | `max_user_num` | 最大用户数限制(创建账号时校验) | 租户状态与有效期由 `ResolveTenant` 中间件在每个请求上校验,详见 [5.8 多租户隔离](/zh/core/tenant.html)。