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

233 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。