签入版本
This commit is contained in:
@@ -1,172 +1,188 @@
|
||||
# 6.13 租户管理
|
||||
|
||||
:::tip 概述
|
||||
租户管理是多租户体系的核心,支持租户的 CRUD、创建租户账号、查看租户用户。继承 BaseLogic(系统共享,租户表本身不隔离)。
|
||||
租户管理是多租户体系的核心,支持租户的 CRUD、创建租户账号、查看租户用户。`TenantService` 继承 `BaseService`(租户表本身不做租户隔离),并通过悲观锁生成租户编码。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,租户表不隔离) |
|
||||
| 特殊接口 | account(创建租户账号)、users(查看租户用户) |
|
||||
| 删除校验 | beforeDelete 检查用户数 + 保护默认租户 |
|
||||
| 编码生成 | afterAdd 自动生成租户编码(T+4位ID) |
|
||||
| 状态校验 | TenantMiddleware 校验租户状态和过期时间 |
|
||||
| 基类 | `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 | 分页列表 |
|
||||
| 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` | - | 租户用户列表 |
|
||||
| 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/delete/{id}` | sys:tenant:delete | 删除租户 |
|
||||
| DELETE | `/api/tenant/batchDelete` | sys:tenant:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
## Service 配置与钩子(来自项目实际代码)
|
||||
|
||||
```php
|
||||
// app/logic/TenantLogic.php
|
||||
|
||||
class TenantLogic extends BaseLogic
|
||||
class TenantService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
|
||||
|
||||
protected string $modelClass = Tenant::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code', 'contact'];
|
||||
protected array $fileFields = ['logo'];
|
||||
protected string $fileSaveDir = 'tenant';
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code', 'contact'];
|
||||
protected array $pageEqFields = ['status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'id', 'type' => 'desc'];
|
||||
protected array $uniqueFields = ['code'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = ['status' => 'tenant_status'];
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'tenant_status',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
/**
|
||||
* 修改前处理:禁止修改编码
|
||||
*
|
||||
* @param int $id 租户 ID
|
||||
* @param array $data 待更新的数据
|
||||
* @return array 过滤后的数据
|
||||
*/
|
||||
protected function beforeUpdate(int $id, array $data): array
|
||||
{
|
||||
// 租户编码由系统自动生成(T+4位ID),不允许手动修改
|
||||
unset($data['code']);
|
||||
return $data;
|
||||
### 编码生成(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);
|
||||
|
||||
/**
|
||||
* 新增后处理:自动生成租户编码
|
||||
*
|
||||
* @param int $id 新增的租户 ID
|
||||
* @param array $data 新增时提交的原始数据
|
||||
* @return void
|
||||
*/
|
||||
protected function afterAdd(int $id, array $data): void
|
||||
{
|
||||
// 编码格式:T + 4位ID左补零,如 T0001、T0002;因为依赖自增ID,所以在新增完成后回写
|
||||
$code = 'T' . str_pad((string)$id, 4, '0', STR_PAD_LEFT);
|
||||
Tenant::where('id', $id)->update(['code' => $code]);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
/**
|
||||
* 删除前处理:保护默认租户 + 检查用户数
|
||||
*
|
||||
* @param int $id 待删除的租户 ID
|
||||
* @return void
|
||||
* @throws \Exception 当为默认租户或存在用户时抛出
|
||||
*/
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
// 默认租户不可删除
|
||||
if ($id == 1) {
|
||||
throw new \Exception('默认租户不可删除');
|
||||
}
|
||||
$userCount = User::where('tenant_id', $id)->where('is_delete', 0)->count();
|
||||
if ($userCount > 0) {
|
||||
throw new \Exception('该租户下存在用户,不可删除');
|
||||
}
|
||||
}
|
||||
:::tip 悲观锁防重号
|
||||
多管理员同时创建租户时,`lockForUpdate()` 在事务内锁定最大编码读取,保证序号递增不重号——生成发生在创建前,无需像"回写编码"方案那样二次 UPDATE。
|
||||
:::
|
||||
|
||||
/**
|
||||
* 列表查询后处理:批量补充租户用户数
|
||||
*
|
||||
* @param array &$records 记录列表(引用传递)
|
||||
* @param array $params 查询参数
|
||||
* @return void
|
||||
*/
|
||||
protected function afterPageList(array &$records, array $params): void
|
||||
{
|
||||
if (empty($records)) return;
|
||||
### 编码保护(beforeUpdate 钩子)
|
||||
|
||||
// 一次性查询当前页所有租户的用户数量,避免 N+1
|
||||
$tenantIds = array_column($records, 'id');
|
||||
$counts = User::where('is_delete', 0)
|
||||
->whereIn('tenant_id', $tenantIds)
|
||||
->group('tenant_id')
|
||||
->column('count(*)', 'tenant_id');
|
||||
```php
|
||||
protected function beforeUpdate(int $id, array $data): array
|
||||
{
|
||||
unset($data['code']); // 租户编码由系统自动生成,不允许手动修改
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
foreach ($records as &$record) {
|
||||
$record['userCount'] = $counts[$record['id']] ?? 0;
|
||||
}
|
||||
### 删除保护(beforeDelete 钩子)
|
||||
|
||||
```php
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
// 存在关联用户时不允许删除
|
||||
$hasUsers = User::where('tenant_id', $id)->exists();
|
||||
if ($hasUsers) {
|
||||
throw new BizException('该租户下存在用户,不可删除');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 列表返回 userCount
|
||||
|
||||
`/api/tenant/page` 和 `/api/tenant/list` 接口返回的每条记录中自动包含 `userCount` 字段,表示该租户下的用户数量。
|
||||
|
||||
:::tip 实现方式
|
||||
通过 `BaseLogic::afterPageList` 钩子实现,使用 `whereIn` + `group` 批量查询,避免 N+1 问题。
|
||||
:::tip 默认租户保护
|
||||
默认租户(id=1)下必然存在用户(平台数据归属),因此用户数校验天然阻止删除默认租户。超级管理员(uid=1)不受租户隔离限制,创建租户隔离数据时自动代入默认租户(见 [5.8 多租户隔离](/zh/core/tenant.html))。
|
||||
:::
|
||||
|
||||
### 创建租户账号
|
||||
### 列表补全 userCount(afterPageList 钩子)
|
||||
|
||||
```php
|
||||
// TenantLogic::createAccount()
|
||||
// 处理流程:
|
||||
// 1. 校验租户存在 + 用户数限制
|
||||
// 2. 注入租户ID,调用 UserLogic::add() 完成标准创建
|
||||
// (含编码自动生成、密码加密、头像处理、角色关联)
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 用户编码由 UserLogic 统一生成
|
||||
创建租户账号时不再由 TenantLogic 直接生成用户编码,
|
||||
而是通过 UserLogic::beforeAdd 统一生成,确保所有用户创建路径(直接添加、租户账号、导入)编码规则一致。
|
||||
:::
|
||||
`/api/tenant/page` 和 `/api/tenant/list` 返回的每条记录自动包含 `userCount` 字段。
|
||||
|
||||
:::warning 默认租户保护
|
||||
默认租户(id=1)不可删除,编码不可修改。超级管理员(tenant_id=0)不受租户隔离限制,创建租户隔离数据时自动代入默认租户(tenant_id=1)。详见 [多租户隔离](/zh/core/tenant.html)。
|
||||
## 租户用户接口
|
||||
|
||||
| 方法/服务 | 说明 |
|
||||
|------|------|
|
||||
| `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` 钩子统一生成——所有创建路径(直接添加、租户账号、导入)编码格式相同。
|
||||
:::
|
||||
|
||||
## 套餐化角色预置
|
||||
@@ -179,7 +195,7 @@ class TenantLogic extends BaseLogic
|
||||
| 租户-专业版 | 进阶功能 | 核心 + 高级模块 |
|
||||
| 租户-旗舰版 | 全功能 | 全部菜单(最全) |
|
||||
|
||||
**使用方式:** 创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限。创建租户管理员账号时,选择对应套餐角色即可完成授权。
|
||||
**使用方式:** 创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限;创建租户管理员账号(`tenant/account`)时选择对应套餐角色即可完成授权。
|
||||
|
||||
:::tip 设计意图
|
||||
套餐角色解决了"每个租户都要手动配一遍菜单"的重复工作。管理员创建租户时只需选择套餐,角色权限即刻生效。
|
||||
@@ -210,5 +226,7 @@ class TenantLogic extends BaseLogic
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `status` | 0=禁用 1=启用 |
|
||||
| `expire_time` | 过期时间,过期后 TenantMiddleware 返回 403 |
|
||||
| `max_user_num` | 最大用户数限制 |
|
||||
| `expire_time` | 过期时间,过期后 `ResolveTenant` 返回 403「租户已过期」 |
|
||||
| `max_user_num` | 最大用户数限制(创建账号时校验) |
|
||||
|
||||
租户状态与有效期由 `ResolveTenant` 中间件在每个请求上校验,详见 [5.8 多租户隔离](/zh/core/tenant.html)。
|
||||
|
||||
Reference in New Issue
Block a user