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

219 lines
7.7 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.15 首页与个人中心
:::tip 概述
首页与个人中心模块负责当前登录用户的信息获取、菜单加载、密码修改和个人信息更新。逻辑由专用的 `IndexService` 承载(不走 BaseService CRUD),接口位于认证组内但**无需配置 `#[Permission]`**——已登录即可访问。
:::
## 模块特点
| 特性 | 说明 |
|------|------|
| Service | `IndexService`(专用逻辑,非 CRUD 服务) |
| 权限 | 无需 `#[Permission]`,已登录即可访问 |
| 权限缓存复用 | `getUser` 的权限列表复用 `JwtService::getUserPermissions`(Redis 缓存),与 `Authenticate` 中间件保持一致 |
| 敏感字段 | 返回前移除 `password` / `salt` |
| 修改密码 | 改密成功后**吊销该用户全部令牌**(全端下线) |
## API 接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/index/getUser` | 获取当前登录用户信息(含角色和权限) |
| GET | `/api/index/getMenus` | 获取当前用户菜单(树形,动态路由数据源) |
| PUT | `/api/index/updatePassword` | 修改密码(改密后全端下线) |
| PUT | `/api/index/updateProfile` | 更新个人信息 |
## 核心代码(来自项目实际代码)
### 获取用户信息
```php
// app/Services/IndexService.php(节选)
public function getUser(int $userId): array
{
$user = User::with(['dept'])->find($userId);
if (! $user) {
throw new BizException('用户不存在');
}
$data = $user->toArray();
// 头像补全域名
if (! empty($data['avatar'])) {
$data['avatar'] = get_file_url($data['avatar']);
}
// 获取角色列表(完整角色对象)
$roleIds = UserRole::where('user_id', $userId)->pluck('role_id')->toArray();
if (! empty($roleIds)) {
$data['roles'] = Role::whereIn('id', $roleIds)->get()->toArray();
} else {
$data['roles'] = [];
}
// 获取权限列表:复用 JwtService 的 Redis 缓存,与 Authenticate 保持一致
$data['permissions'] = app(JwtService::class)->getUserPermissions($userId);
// 组装城市编码数组,供前端级联选择回显
$data['city'] = [
$data['province_code'] ?? '',
$data['city_code'] ?? '',
$data['district_code'] ?? '',
$data['street_code'] ?? '',
];
// 隐藏敏感字段
unset($data['password'], $data['salt']);
return $data;
}
```
:::info 返回结构说明
- `roles`:完整角色对象数组(含 id、name、code 等),非仅 ID
- `permissions`:权限码数组,超级管理员为 `['*:*:*']`
- `city`:4 层城市编码数组 `[省, 市, 区, 街道]`
- `avatar`:自动补全域名前缀
- 字段名由 `Result::success()` 自动转为 camelCase 返回前端
:::
### 获取菜单(动态路由数据源)
```php
/**
* 获取当前用户菜单(树形)
*
* - 超管(userId=1):返回全部启用的目录/菜单;
* - 普通用户:按角色关联的菜单ID取菜单,再组装为树形;
* - 无角色或无关联菜单时返回空数组。
*
* 字段说明:
* - hide:0=可见,1=不可见(取 hide=0);
* - type:0=菜单/目录,1=节点/权限按钮(取 0);
* - status:0=正常,1=停用(取 0)。
*/
public function getMenus(int $userId): array
{
// 超级管理员获取所有菜单
if ($userId === 1) {
$menus = Menu::where('status', 0)
->where('hide', 0)
->where('type', 0)
->where('is_delete', 0)
->orderBy('sort', 'asc')
->get()
->toArray();
return $this->buildTree($menus);
}
// 获取用户角色 → 角色关联菜单ID(去重)→ 过滤后组装树形
// (同样附加 status=0、hide=0、type=0、is_delete=0 四个条件)
}
```
:::info 菜单查询四条件
获取可导航菜单时保证四个条件:`type=0`(菜单/目录)、`status=0`(启用)、`hide=0`(未隐藏)、`is_delete=0`(未删除)。`type=1` 的权限节点不返回给前端菜单树(仅供后端鉴权)。详见 [6.3 菜单管理](/zh/modules/menu.html)。
:::
### 修改密码
```php
/**
* 流程:校验旧密码 → 加密新密码并保存 → 吊销该用户全部令牌。
*
* @throws BizException 用户不存在或旧密码错误时抛出
*/
public function updatePassword(int $userId, string $oldPassword, string $newPassword): bool
{
$user = User::find($userId);
// 验证旧密码(bcrypt 双重加盐)
if (! PasswordService::verify($oldPassword, $user->password, $user->salt)) {
throw new BizException('旧密码错误');
}
// 加密新密码(外部盐 + bcrypt)
$encrypted = PasswordService::encrypt($newPassword);
$user->password = $encrypted['password'];
$user->salt = $encrypted['salt'];
$saved = $user->save();
if ($saved) {
// 改密后全端下线:吊销该用户全部令牌 + 权限缓存
app(JwtService::class)->revokeAllTokens($userId);
}
return $saved !== false;
}
```
:::warning 改密安全闭环
修改密码成功后立即调用 `revokeAllTokens($userId)`——删除 Redis 中该用户**全部** refresh_token 与权限缓存,所有已登录设备(含攻击者持有的旧会话)下一次请求即失效。见 [5.1 用户认证(JWT)](/zh/core/jwt.html)。
:::
### 更新个人信息
```php
public function updateProfile(int $userId, array $data): bool
{
$user = User::find($userId);
if (! $user) {
throw new BizException('用户不存在');
}
// 字段白名单:仅允许更新以下字段
$allowFields = ['realname', 'gender', 'avatar', 'mobile', 'email', 'intro', 'note', 'address'];
$updateData = [];
foreach ($allowFields as $field) {
if (isset($data[$field])) {
$updateData[$field] = $data[$field];
}
}
// 处理城市字段:city 数组拆分到 4 个独立字段,并生成 city_info
if (isset($data['city']) && is_array($data['city'])) {
$updateData['province_code'] = $data['city'][0] ?? '';
$updateData['city_code'] = $data['city'][1] ?? '';
$updateData['district_code'] = $data['city'][2] ?? '';
$updateData['street_code'] = $data['city'][3] ?? '';
$updateData['city_info'] = City::resolveCityNames($data['city']);
}
$user->update($updateData);
return true;
}
```
:::warning 越权防护
`updateProfile` 采用字段白名单(`realname / gender / avatar / mobile / email / intro / note / address`),`city` 数组拆分写入四级编码;`password`、`salt`、`role_id`、`tenant_id`、`id` 等不在名单内的字段一律忽略,防止用户通过个人信息接口越权修改自身密码、角色或租户归属——改密码必须走 `updatePassword`(校验旧密码 + 全端下线)。
:::
## 前端调用
```typescript
// 获取当前用户信息(登录后 / 刷新页面时)
const userInfo = await getUserInfo();
// userInfo.permissions = ['sys:user:page', 'sys:article:add', ...]
// userInfo.roles = [{id:1, name:'超级管理员', ...}]
// userInfo.city = [省编码, 市编码, 区编码, 街道编码]
// 获取菜单(用于动态路由生成)
const menus = await getMenuList();
// menus = [{id:1, name:'控制面板', path:'/dashboard', children:[...]}]
// 修改密码(成功后前端需清除令牌重新登录 —— 后端已全端吊销)
await updatePassword({ oldPassword: '123456', newPassword: '654321' });
// 更新个人信息
await updateProfile({ realname: '张三', mobile: '13800138000' });
```
## 相关页面
- [4.2 请求处理链路](/zh/architecture/request-flow.html) — 认证与菜单加载在链路中的位置
- [5.4 权限管理(RBAC)](/zh/core/rbac.html) — 权限码与角色体系
- [9.5 路由与动态菜单](/zh/frontend/router.html) — 前端如何用 getMenus 生成路由