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

240 lines
8.4 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.1 用户管理
:::tip 概述
用户管理是系统核心模块,`UserService` 继承 `BaseTenantService` 实现租户隔离,支持 CRUD、重置密码、Excel 导入导出,并通过钩子完成编码生成、密码加密、角色关联等业务。
:::
## 模块特点
| 特性 | 说明 |
|------|------|
| 租户隔离 | 继承 `BaseTenantService`,自动按 `tenant_id` 过滤与填充 |
| 数据权限 | `dataScopeDeptField='dept_id'`,支持按部门过滤 |
| 编码生成 | `beforeAdd` 自动生成用户编码(租户编码-6位序号,如 `T0001-000020`) |
| 唯一校验 | `username` 全局唯一;`mobile` **按租户分组唯一**(`['mobile','tenant_id']`) |
| 枚举映射 | `gender`→gender 字典,`status`→user_status 字典 |
| 密码加密 | bcrypt 双重加盐,新增/重置时自动加密 |
| 角色关联 | 通过 `UserRole` 中间表关联,支持多角色(`roles` 数组) |
| 导入导出 | Excel 分批导出 + 表头映射导入(字典/部门/城市反查) |
## 文件清单
| 文件 | 说明 |
|------|------|
| `app/Http/Controllers/Api/UserController.php` | 控制器(9 个接口) |
| `app/Services/UserService.php` | 业务服务(含 import/export/resetPassword + 6 个钩子) |
| `app/Models/User.php` | 模型 |
| `app/Models/UserRole.php` | 用户角色关联模型 |
| `app/Http/Requests/UserRequest.php` | 表单请求验证 |
## API 接口
| 方法 | 路径 | 权限码 | 说明 |
|------|------|--------|------|
| GET | `/api/user/page` | sys:user:page | 分页列表 |
| GET | `/api/user/detail/{id}` | sys:user:detail | 详情 |
| POST | `/api/user/add` | sys:user:add | 新增 |
| PUT | `/api/user/update` | sys:user:update | 修改 |
| PUT | `/api/user/resetPwd` | sys:user:resetPwd | 重置密码 |
| DELETE | `/api/user/delete/{id}` | sys:user:delete | 删除 |
| DELETE | `/api/user/batchDelete` | sys:user:batchDelete | 批量删除 |
| POST | `/api/user/import` | sys:user:import | 导入 |
| GET | `/api/user/export` | sys:user:export | 导出 |
## Service 属性配置
```php
// app/Services/UserService.php(来自项目实际代码)
class UserService extends BaseTenantService
{
// HasDataScope(数据权限 + 租户隔离)由 BaseTenantService 继承提供
// HasFileProcessing:avatar 头像字段的保存前迁移与读取后补域名
use HasCrudOperations, HasFileProcessing, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
protected string $modelClass = User::class;
// 单文件字段:保存时迁移临时文件,读取时补全域名
protected array $fileFields = ['avatar'];
// 文件保存子目录
protected string $fileSaveDir = 'user';
// LIKE 模糊匹配字段
protected array $pageLikeFields = ['username', 'realname', 'mobile'];
// 精确匹配字段
protected array $pageEqFields = ['status', 'dept_id', 'gender', 'tenant_id'];
// 默认排序:先按租户分组,再按 id 倒序
protected array $pageOrderBy = [
['field' => 'tenant_id', 'type' => 'asc'],
['field' => 'id', 'type' => 'desc'],
];
// 唯一性校验:username 全局唯一;mobile 在同一租户内唯一
protected array $uniqueFields = ['username', ['mobile', 'tenant_id']];
// 字典翻译
protected array $serializeMaps = [
'gender' => 'gender',
'status' => 'user_status',
];
// 数据权限:按部门过滤
protected string $dataScopeDeptField = 'dept_id';
}
```
## 特殊逻辑
### 重置密码
```php
// app/Http/Controllers/Api/UserController.php(来自项目实际代码)
public function resetPwd(): JsonResponse
{
$data = $this->getJsonBody();
$id = $data['userId'] ?? 0;
if (empty($id)) {
return $this->fail('缺少id参数');
}
try {
$this->userService->resetPassword($id);
return $this->success(null, '密码重置成功');
} catch (BizException $e) {
return $this->fail($e->getMessage(), $e->getCode());
} catch (\Exception $e) {
logger()->error('操作失败', ['error' => $e->getMessage()]);
return $this->fail('操作失败');
}
}
```
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `userId` | int | 是 | 用户ID |
:::tip 默认密码与全端下线
重置密码接口无需传递密码参数,系统自动使用默认密码(系统参数 `DEFAULT_PASSWORD`,默认 `123456`)加密写入。`UserService::resetPassword()` 成功后会自动调用 `JwtService::revokeAllTokens($id)` 吊销该用户全部令牌(见 [5.6 密码加密服务](/zh/core/password.html))。
:::
### 新增用户(beforeAdd 钩子)
```php
// app/Services/UserService.php(行为说明)
protected function beforeAdd(array $data): array
{
// 1. 自动生成用户编码:租户编码 + 租户内序号(如 T0001-000020)
// 序号 = 当前租户下未删除用户数 + 1
// 平台用户(tenantId=0)自动代入默认租户生成编码
// 2. 处理城市字段:city 数组拆为 province_code / city_code /
// district_code / street_code 四个编码 + city_info 名称拼接
// 3. 密码加密:PasswordService::encrypt()(未传密码时用默认密码)
// → $data['password'] = 哈希; $data['salt'] = 外部盐
return $data;
}
```
:::tip 编码规则
- 用户编码格式:`租户编码-6位序号`,如 `T0001-000020`
- 序号 = 当前租户下未删除用户数 + 1
- 编码由系统自动生成,前端不传,修改时不可变更
:::
### 角色关联(afterAdd / afterUpdate 钩子)
```php
// afterAdd:新增后保存用户角色关联
protected function afterAdd(int $id, array $data): void
{
$roleIds = $data['roles'] ?? [];
foreach ($roleIds as $roleId) {
UserRole::create(['user_id' => $id, 'role_id' => $roleId]);
}
}
// afterUpdate:先删后插实现全量覆盖
```
:::info 多角色支持
新增/修改时前端传 `roles: [1, 2, 3]` ID 数组,`afterAdd` 遍历写入 `UserRole` 中间表,`afterUpdate` 先删除旧关联再重建;详情接口返回 ID 数组供表单回显,分页列表返回 `[{id, name}]` 对象数组,表格可直接取 `name` 展示。
:::
### 详情补全(afterDetail 钩子)
```php
// 详情查询后处理(引用传递)
protected function afterDetail(int $id, array &$data): void
{
// 补充角色 ID 数组
$data['roles'] = UserRole::where('user_id', $id)->pluck('role_id');
// 补充关联名称(部门/职级/岗位显示名)
$data['deptName'] = ...;
$data['levelName'] = ...;
$data['positionName'] = ...;
// 组装城市编码数组(供前端级联回显)
$data['city'] = [
$data['province_code'] ?? '', $data['city_code'] ?? '',
$data['district_code'] ?? '', $data['street_code'] ?? '',
];
// 移除敏感字段:password / salt 绝不返回前端
unset($data['password'], $data['salt']);
}
```
:::warning 敏感字段
`password` 与 `salt` 在详情/列表输出时必须剔除——这也是日志安全(操作日志 param 排除 password/token)之外的第二道防线。
:::
### 列表补全(afterPageList 钩子)
分页列表批量补充角色名、部门/职级/岗位名称与字典翻译(避免 N+1,见 [3.5 Service 层开发](/zh/guide/logic.html))。
### 导出(export)
导出为分批导出(`ExcelService`),每批 500 条,避免内存溢出。
| 导出列 | 字段来源 |
|--------|----------|
| 用户编码 | code(系统自动生成) |
| 姓名 | realname |
| 性别 | genderText(字典自动转换) |
| 手机号 / 邮箱 | mobile / email |
| 部门 / 职级 / 岗位 | deptName / levelName / positionName |
| 城市 | cityInfo(四个城市编码拼接为名称) |
| 地址 | address |
| 用户名 | username |
| 状态 | statusText(字典自动转换) |
| 创建时间 | create_time |
### 导入(import)
导入时需处理以下反向映射:
| 导入列 | 处理逻辑 |
|--------|----------|
| 性别/状态 | 非数字时 `DictCacheService::getValue` 反查 |
| 部门/职级/岗位 | 名称通过名称映射反查 ID |
| 城市 | cityInfo 通过城市工具拆为 4 个编码 |
| 用户编码 | 忽略(由 `beforeAdd` 自动生成) |
单条失败不中断整体,失败明细(行号 + 原因)汇总返回。
:::warning 导入不处理 code
导入时即使 Excel 中有「用户编码」列也会被忽略,编码由 `beforeAdd` 自动生成。
:::