Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/modules/tenant.md
T
2026-10-08 17:50:29 +08:00

8.6 KiB
Raw Blame History

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 配置与钩子(来自项目实际代码)

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 钩子)

/**
 * 新增前钩子:自动生成租户编码
 *
 * 租户编码格式: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 钩子)

protected function beforeUpdate(int $id, array $data): array
{
    unset($data['code']);   // 租户编码由系统自动生成,不允许手动修改
    return $data;
}

删除保护(beforeDelete 钩子)

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 多租户隔离)。 :::

列表补全 userCount(afterPageList 钩子)

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)

/**
 * 创建租户管理员账号
 *
 * 流程:校验租户 → 用户数上限校验 → 用户名全局唯一 → 组装角色 → 事务内委托
 * 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 多租户隔离。