feat(user): 添加个人资料管理功能
- 在AppLayout中使用useCallback优化回调函数,提升性能 - 集成权限检查功能,动态显示平台设置菜单项 - 添加updateMyProfile和updateMyPassword API接口 - 扩展UserProfile类型定义,增加密码重置需求字段 - 实现强制密码重置拦截逻辑,确保安全性 - 优化租户加载逻辑,避免非平台模式下的无效请求 - 添加个人资料页面路由和组件 - 在useAuth钩子中提供用户档案数据访问 - 后端实现用户资料更新和密码修改接口 - 添加密码更新DTO和相关验证逻辑master
parent
b55f17a0aa
commit
351e56a059
|
|
@ -2,6 +2,7 @@ package com.imeeting.controller;
|
|||
|
||||
import com.imeeting.auth.JwtTokenProvider;
|
||||
import com.imeeting.common.ApiResponse;
|
||||
import com.imeeting.dto.PasswordUpdateDTO;
|
||||
import com.imeeting.dto.UserProfile;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.security.LoginUser;
|
||||
|
|
@ -101,6 +102,7 @@ public class UserController {
|
|||
profile.setStatus(user.getStatus());
|
||||
profile.setAdmin(userId == 1L);
|
||||
profile.setIsPlatformAdmin(user.getIsPlatformAdmin());
|
||||
profile.setPwdResetRequired(user.getPwdResetRequired());
|
||||
return ApiResponse.ok(profile);
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +184,41 @@ public class UserController {
|
|||
return ApiResponse.ok(sysUserService.removeById(id));
|
||||
}
|
||||
|
||||
@PutMapping("/profile")
|
||||
public ApiResponse<Boolean> updateProfile(@RequestBody SysUser user) {
|
||||
Long userId = getCurrentUserId();
|
||||
SysUser existing = sysUserService.getByIdIgnoreTenant(userId);
|
||||
if (existing == null) return ApiResponse.error("用户不存在");
|
||||
|
||||
existing.setDisplayName(user.getDisplayName());
|
||||
existing.setEmail(user.getEmail());
|
||||
existing.setPhone(user.getPhone());
|
||||
return ApiResponse.ok(sysUserService.updateById(existing));
|
||||
}
|
||||
|
||||
@PutMapping("/password")
|
||||
public ApiResponse<Boolean> updatePassword(@RequestBody PasswordUpdateDTO dto) {
|
||||
Long userId = getCurrentUserId();
|
||||
SysUser user = sysUserService.getByIdIgnoreTenant(userId);
|
||||
if (user == null) return ApiResponse.error("用户不存在");
|
||||
|
||||
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPasswordHash())) {
|
||||
return ApiResponse.error("旧密码不正确");
|
||||
}
|
||||
|
||||
user.setPasswordHash(passwordEncoder.encode(dto.getNewPassword()));
|
||||
user.setPwdResetRequired(0); // 重置标志位
|
||||
return ApiResponse.ok(sysUserService.updateById(user));
|
||||
}
|
||||
|
||||
private Long getCurrentUserId() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
||||
return ((LoginUser) auth.getPrincipal()).getUserId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/roles")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:role:list')")
|
||||
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ public class UserProfile {
|
|||
@JsonProperty("isAdmin")
|
||||
private boolean isAdmin;
|
||||
private Boolean isPlatformAdmin;
|
||||
private Integer pwdResetRequired;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ export async function getCurrentUser() {
|
|||
return resp.data.data as UserProfile;
|
||||
}
|
||||
|
||||
export async function updateMyProfile(payload: Partial<SysUser>) {
|
||||
const resp = await http.put("/api/users/profile", payload);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function updateMyPassword(payload: any) {
|
||||
const resp = await http.put("/api/users/password", payload);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function createPermission(payload: Partial<SysPermission>) {
|
||||
const resp = await http.post("/api/permissions", payload);
|
||||
return resp.data.data as boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { UserProfile } from "../types";
|
||||
|
||||
export function useAuth() {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(() => localStorage.getItem("accessToken"));
|
||||
|
|
@ -9,13 +10,19 @@ export function useAuth() {
|
|||
return () => window.removeEventListener("storage", handler);
|
||||
}, []);
|
||||
|
||||
const profile = useMemo<UserProfile | null>(() => {
|
||||
const data = sessionStorage.getItem("userProfile");
|
||||
return data ? JSON.parse(data) : null;
|
||||
}, [accessToken]);
|
||||
|
||||
const isAuthed = !!accessToken;
|
||||
const logout = () => {
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("refreshToken");
|
||||
sessionStorage.removeItem("userProfile");
|
||||
setAccessToken(null);
|
||||
};
|
||||
|
||||
return { accessToken, isAuthed, logout };
|
||||
return { accessToken, isAuthed, profile, logout };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Layout, Menu, Button, Space, Avatar, Dropdown, message, type MenuProps, Select } from "antd";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
|
|
@ -49,7 +49,7 @@ export default function AppLayout() {
|
|||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuth();
|
||||
const { load: loadPermissions } = usePermission();
|
||||
const { load: loadPermissions, can } = usePermission();
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
|
|
@ -111,17 +111,17 @@ export default function AppLayout() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
const handleLogout = useCallback(() => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
};
|
||||
}, [logout, navigate]);
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
const changeLanguage = useCallback((lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
message.success(lng === 'zh-CN' ? '已切换至中文' : 'Switched to English');
|
||||
};
|
||||
}, [i18n]);
|
||||
|
||||
const buildMenuTree = (list: SysPermission[]) => {
|
||||
const buildMenuTree = useCallback((list: SysPermission[]) => {
|
||||
const map = new Map<number, SysPermission & { children?: SysPermission[] }>();
|
||||
const roots: (SysPermission & { children?: SysPermission[] })[] = [];
|
||||
list.forEach((m) => map.set(m.permId, { ...m, children: [] }));
|
||||
|
|
@ -138,9 +138,9 @@ export default function AppLayout() {
|
|||
};
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toMenuItems = (nodes: (SysPermission & { children?: SysPermission[] })[]) =>
|
||||
const toMenuItems = useCallback((nodes: (SysPermission & { children?: SysPermission[] })[]): any[] =>
|
||||
nodes.map((m) => {
|
||||
const key = m.path || m.code || String(m.permId);
|
||||
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
||||
|
|
@ -157,16 +157,45 @@ export default function AppLayout() {
|
|||
icon,
|
||||
label: <Link to={m.path || "#"}>{m.name}</Link>,
|
||||
};
|
||||
}), []); // 移除 [toMenuItems] 依赖项以解决 TDZ 错误
|
||||
|
||||
const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]);
|
||||
|
||||
const userMenuItems: MenuProps["items"] = useMemo(() => {
|
||||
const items: any[] = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: <Link to="/profile">{t('layout.profile')}</Link>,
|
||||
icon: <UserOutlined />
|
||||
},
|
||||
];
|
||||
|
||||
let profile: any = {};
|
||||
try {
|
||||
const stored = sessionStorage.getItem("userProfile");
|
||||
if (stored) profile = JSON.parse(stored) || {};
|
||||
} catch (e) {
|
||||
profile = {};
|
||||
}
|
||||
|
||||
if (profile.isPlatformAdmin || can("sys_platform:config:update")) {
|
||||
items.push({
|
||||
key: 'settings',
|
||||
label: <Link to="/platform-settings">{t('layout.settings')}</Link>,
|
||||
icon: <SettingOutlined />
|
||||
});
|
||||
}
|
||||
|
||||
items.push({ type: 'divider', key: 'd1' });
|
||||
items.push({
|
||||
key: 'logout',
|
||||
label: t('layout.logout'),
|
||||
icon: <LogoutOutlined />,
|
||||
onClick: handleLogout
|
||||
});
|
||||
|
||||
const menuItems = toMenuItems(buildMenuTree(menus));
|
||||
|
||||
const userMenuItems: MenuProps["items"] = [
|
||||
{ key: 'profile', label: t('layout.profile'), icon: <UserOutlined /> },
|
||||
{ key: 'settings', label: t('layout.settings'), icon: <SettingOutlined /> },
|
||||
{ type: 'divider' },
|
||||
{ key: 'logout', label: t('layout.logout'), icon: <LogoutOutlined />, onClick: handleLogout },
|
||||
];
|
||||
return items;
|
||||
}, [t, can, handleLogout]);
|
||||
|
||||
const langMenuItems: MenuProps["items"] = [
|
||||
{ key: 'zh-CN', label: '简体中文', onClick: () => changeLanguage('zh-CN') },
|
||||
|
|
|
|||
|
|
@ -82,14 +82,18 @@ export default function Orgs() {
|
|||
const [form] = Form.useForm();
|
||||
|
||||
const loadTenants = async () => {
|
||||
// 优化:如果不是平台模式,直接设置当前租户,不发请求
|
||||
if (!isPlatformMode) {
|
||||
setSelectedTenantId(activeTenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await listTenants({ current: 1, size: 100 });
|
||||
const list = resp.records || [];
|
||||
setTenants(list);
|
||||
|
||||
if (!isPlatformMode) {
|
||||
setSelectedTenantId(activeTenantId);
|
||||
} else if (list.length > 0 && selectedTenantId === undefined) {
|
||||
if (list.length > 0 && selectedTenantId === undefined) {
|
||||
setSelectedTenantId(list[0].id);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import Login from "../pages/Login";
|
||||
import ResetPassword from "../pages/ResetPassword";
|
||||
import AppLayout from "../layouts/AppLayout";
|
||||
import { menuRoutes } from "./routes";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
|
||||
function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
const { isAuthed } = useAuth();
|
||||
const { isAuthed, profile } = useAuth();
|
||||
if (!isAuthed) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
// 强制改密拦截
|
||||
if (profile?.pwdResetRequired === 1) {
|
||||
return <Navigate to="/reset-password" replace />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +21,7 @@ export default function AppRoutes() {
|
|||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import UserRoleBinding from "../pages/UserRoleBinding";
|
|||
import RolePermissionBinding from "../pages/RolePermissionBinding";
|
||||
import SysParams from "../pages/SysParams";
|
||||
import PlatformSettings from "../pages/PlatformSettings";
|
||||
import Profile from "../pages/Profile";
|
||||
|
||||
import type { MenuRoute } from "../types";
|
||||
|
||||
export const menuRoutes: MenuRoute[] = [
|
||||
{ path: "/", label: "总览", element: <Dashboard />, perm: "menu:dashboard" },
|
||||
{ path: "/profile", label: "个人中心", element: <Profile /> },
|
||||
{ path: "/tenants", label: "租户管理", element: <Tenants />, perm: "menu:tenants" },
|
||||
{ path: "/orgs", label: "组织管理", element: <Orgs />, perm: "menu:orgs" },
|
||||
{ path: "/users", label: "用户管理", element: <Users />, perm: "menu:users" },
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface UserProfile {
|
|||
status?: number;
|
||||
isAdmin: boolean;
|
||||
isPlatformAdmin?: boolean;
|
||||
pwdResetRequired?: number;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue