Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_AntdVue/zh/modules/index.md
T
2026-10-08 21:09:24 +08:00

7.8 KiB
Raw Blame History

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 更新个人信息

核心代码(来自项目实际代码)

获取用户信息

// 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 返回前端 :::

获取菜单(动态路由数据源)

/**
 * 获取当前用户菜单(树形)
 *
 * - 超管(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 菜单管理。 :::

修改密码

/**
 * 流程:校验旧密码 → 加密新密码并保存 → 吊销该用户全部令牌。
 *
 * @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)。 :::

更新个人信息

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(校验旧密码 + 全端下线)。 :::

前端调用

// 获取当前用户信息(登录后 / 刷新页面时)
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({ password: '123456', newPassword: '654321', confirmPassword: '654321' });

// 更新个人信息
await updateProfile({ realname: '张三', mobile: '13800138000' });

相关页面