# 6.1 用户管理 :::tip 概述 用户管理是系统核心模块,继承 BaseTenantLogic 实现租户隔离,支持 CRUD、重置密码、Excel 导入导出。 ::: ## 模块特点 | 特性 | 说明 | |------|------| | 租户隔离 | 继承 BaseTenantLogic,自动按 tenant_id 过滤 | | 数据权限 | 配置 dataScopeDeptField='dept_id',支持按部门过滤 | | 编码生成 | beforeAdd 自动生成用户编码(租户编码-6位序号,如 T0001-000020) | | 文件字段 | avatar(头像),保存到 user/avatar 目录 | | 唯一校验 | username、mobile 全局唯一 | | 枚举映射 | gender→gender 字典,status→user_status 字典 | | 密码加密 | bcrypt 双重加盐,新增时自动加密 | | 角色关联 | 通过 UserRole 中间表关联,支持多角色 | ## 文件清单 | 文件 | 说明 | |------|------| | `app/controller/UserController.php` | 控制器(9 个接口) | | `app/logic/UserLogic.php` | 逻辑层(含 import/export/resetPassword) | | `app/model/User.php` | 模型 | | `app/model/UserRole.php` | 用户角色关联模型 | | `app/validate/UserValidate.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 | 导出 | ## 特殊逻辑 ### 重置密码 ```php // app/controller/UserController.php public function resetPwd(): Json { $data = $this->getJsonBody(); $id = $data['userId'] ?? 0; if (empty($id)) { return $this->fail('缺少id参数'); } $this->logic->resetPassword($id); return $this->success(null, '密码重置成功'); } ``` **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `userId` | int | 是 | 用户ID | :::tip 默认密码 重置密码接口无需传递密码参数,系统自动使用 `PasswordService::getDefaultPasswordPlain()` 作为默认密码。 ::: ### 新增用户(beforeAdd 钩子) ```php // app/logic/UserLogic.php /** * 新增前处理:生成编码、处理城市、加密密码 * * @param array $data 待新增的数据 * @return array 处理后的数据 */ protected function beforeAdd(array $data): array { // 自动生成用户编码:租户编码 + 租户内序号(如 T0001-000020) $tenantId = $data['tenantId'] ?? request()->tenantId ?? 0; if ($tenantId == 0) { $tenantId = config('tenant.default_tenant_id', 1); } if ($tenantId > 0) { $tenant = Tenant::find($tenantId); $currentUserCount = User::where('tenant_id', $tenantId)->where('is_delete', 0)->count(); $nextSeq = $currentUserCount + 1; $data['code'] = $tenant->code . '-' . str_pad((string)$nextSeq, 6, '0', STR_PAD_LEFT); } // 处理城市字段 if (isset($data['city']) && is_array($data['city'])) { $data['province_code'] = $data['city'][0] ?? ''; $data['city_code'] = $data['city'][1] ?? ''; $data['district_code'] = $data['city'][2] ?? ''; $data['street_code'] = $data['city'][3] ?? ''; $data['city_info'] = City::resolveCityNames($data['city']); unset($data['city']); } // 密码加密 $password = $data['password'] ?? PasswordService::getDefaultPasswordPlain(); $encrypted = PasswordService::encrypt($password); $data['password'] = $encrypted['password']; $data['salt'] = $encrypted['salt']; return $data; } ``` :::tip 编码规则 - 用户编码格式:`租户编码-6位序号`,如 `T0001-000020` - 序号 = 当前租户下未删除用户数 + 1 - 超级管理员(tenantId=0)自动代入默认租户生成编码 - 编码由系统自动生成,前端不传,修改时不可变更 ::: ### 新增后保存角色(afterAdd 钩子) ```php /** * 新增后处理:保存用户角色关联 * * @param int $id 新增用户的主键 ID * @param array $data 新增时提交的原始数据 * @return void */ protected function afterAdd(int $id, array $data): void { $roleIds = $data['roles'] ?? []; foreach ($roleIds as $roleId) { UserRole::create([ 'user_id' => $id, 'role_id' => $roleId, 'create_user' => request()->userInfo->username ?? '', ]); } } ``` :::info 多角色支持 前端传 `roles: [1, 2, 3]` 数组,`afterAdd` 遍历写入 `UserRole` 中间表。`afterUpdate` 同理,先删后插实现全量覆盖。 ::: ### 详情补全(afterDetail 钩子) ```php /** * 详情查询后处理:补充关联数据与格式化输出 * * @param int $id 用户 ID * @param array &$data 详情数据(引用传递) * @return void */ protected function afterDetail(int $id, array &$data): void { // 补充角色信息 $roleIds = UserRole::where('user_id', $id)->column('role_id'); $data['roles'] = $roleIds; // 补充关联名称 $data['levelName'] = Level::getNameMap()[$data['level_id']] ?? ''; $data['positionName'] = Position::getNameMap()[$data['position_id']] ?? ''; $data['deptName'] = Dept::getNameMap()[$data['dept_id']] ?? ''; // 组装城市数组 $data['city'] = [ $data['province_code'] ?? '', $data['city_code'] ?? '', $data['district_code'] ?? '', $data['street_code'] ?? '', ]; // 移除敏感字段 unset($data['password'], $data['salt']); } ``` ### 导出(export) 导出为分批导出(chunkExport),每批 500 条,避免内存溢出。 | 导出列 | 字段来源 | |--------|----------| | 用户编码 | code(系统自动生成) | | 姓名 | realname | | 性别 | genderText(DictService 自动转换) | | 手机号 | mobile | | 邮箱 | email | | 部门 | deptName(Dept::getNameMap 映射) | | 职级 | levelName(Level::getNameMap 映射) | | 岗位 | positionName(Position::getNameMap 映射) | | 城市 | cityInfo(4个城市编码拼接为名称字符串) | | 地址 | address | | 用户名 | username | | 简介 | intro | | 状态 | statusText(DictService 自动转换) | | 备注 | note | ### 导入(import) 导入时需处理以下反向映射: | 导入列 | 处理逻辑 | |--------|----------| | 性别/状态 | 非数字时 DictService::getValue 反查 | | 部门/职级/岗位 | 名称通过 array_flip(getNameMap) 反查 ID | | 城市 | cityInfo 通过 City::resolveCityCodes 拆为4个编码 | | 用户编码 | 忽略(由 beforeAdd 自动生成) | :::warning 导入不处理 code 导入时即使 Excel 中有「用户编码」列也会被忽略,编码由 `beforeAdd` 自动生成。 :::