diff --git a/backend/design/project_design.md b/backend/design/project_design.md index 815c98b..eed34e0 100644 --- a/backend/design/project_design.md +++ b/backend/design/project_design.md @@ -2,7 +2,7 @@ ## 1. 项目概述 本项目为“智能会议语音识别与总结系统”的管理后台,提供用户、角色、权限、设备与任务等管理能力。 -后端为 Java 服务,前端为后台管理 Web。 +后端为 Java 服务,前端为后台管理 Web。 后续所有内容变更都必须同步更新该文档 ## 2. 技术栈 ### 后端 diff --git a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java index 31bd821..4d2c667 100644 --- a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java @@ -116,6 +116,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } + } catch (io.jsonwebtoken.ExpiredJwtException e) { + SecurityContextHolder.clearContext(); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token expired\"}"); + return; + } catch (io.jsonwebtoken.JwtException e) { + SecurityContextHolder.clearContext(); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":\"401\",\"msg\":\"Invalid token\"}"); + return; } catch (Exception ignored) { SecurityContextHolder.clearContext(); } diff --git a/backend/src/main/java/com/imeeting/controller/RoleController.java b/backend/src/main/java/com/imeeting/controller/RoleController.java index 8e791c7..04cdc95 100644 --- a/backend/src/main/java/com/imeeting/controller/RoleController.java +++ b/backend/src/main/java/com/imeeting/controller/RoleController.java @@ -6,7 +6,9 @@ import com.imeeting.common.annotation.Log; import com.imeeting.entity.SysRole; import com.imeeting.entity.SysRolePermission; import com.imeeting.entity.SysUser; +import com.imeeting.entity.SysUserRole; import com.imeeting.mapper.SysRolePermissionMapper; +import com.imeeting.mapper.SysUserRoleMapper; import com.imeeting.service.SysRoleService; import com.imeeting.service.SysUserService; import org.springframework.security.access.prepost.PreAuthorize; @@ -21,11 +23,13 @@ public class RoleController { private final SysRoleService sysRoleService; private final SysUserService sysUserService; private final SysRolePermissionMapper sysRolePermissionMapper; + private final SysUserRoleMapper sysUserRoleMapper; - public RoleController(SysRoleService sysRoleService, SysUserService sysUserService, SysRolePermissionMapper sysRolePermissionMapper) { + public RoleController(SysRoleService sysRoleService, SysUserService sysUserService, SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper) { this.sysRoleService = sysRoleService; this.sysUserService = sysUserService; this.sysRolePermissionMapper = sysRolePermissionMapper; + this.sysUserRoleMapper = sysUserRoleMapper; } @GetMapping @@ -103,6 +107,42 @@ public class RoleController { return ApiResponse.ok(true); } + @PostMapping("/{id}/users") + @PreAuthorize("@ss.hasPermi('sys_role:update')") + @Log(value = "角色关联用户", type = "角色管理") + public ApiResponse bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) { + if (payload == null || payload.getUserIds() == null) { + return ApiResponse.ok(true); + } + for (Long userId : payload.getUserIds()) { + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("role_id", id).eq("user_id", userId); + if (sysUserRoleMapper.selectCount(qw) == 0) { + SysUserRole ur = new SysUserRole(); + ur.setRoleId(id); + ur.setUserId(userId); + sysUserRoleMapper.insert(ur); + } + } + return ApiResponse.ok(true); + } + + @DeleteMapping("/{id}/users/{userId}") + @PreAuthorize("@ss.hasPermi('sys_role:update')") + @Log(value = "角色取消关联用户", type = "角色管理") + public ApiResponse unbindUser(@PathVariable Long id, @PathVariable Long userId) { + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("role_id", id).eq("user_id", userId); + sysUserRoleMapper.delete(qw); + return ApiResponse.ok(true); + } + + public static class UserBindingPayload { + private List userIds; + public List getUserIds() { return userIds; } + public void setUserIds(List userIds) { this.userIds = userIds; } + } + public static class PermissionBindingPayload { private List permIds; diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts index 4844c05..188148f 100644 --- a/frontend/src/api/http.ts +++ b/frontend/src/api/http.ts @@ -23,13 +23,13 @@ http.interceptors.response.use( return resp; }, (error) => { - if (error.response && error.response.status === 401) { + if (error.response && (error.response.status === 401 || error.response.status === 403)) { // Clear session/local storage localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); sessionStorage.removeItem("userProfile"); - // Force redirect to login - window.location.href = "/login"; + // Force redirect to login with timeout flag + window.location.href = "/login?timeout=1"; } return Promise.reject(error); } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 8bc35cb..e797092 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -127,6 +127,16 @@ export async function fetchUsersByRoleId(roleId: number) { return resp.data.data as SysUser[]; } +export async function bindUsersToRole(roleId: number, userIds: number[]) { + const resp = await http.post(`/api/roles/${roleId}/users`, { userIds }); + return resp.data.data as boolean; +} + +export async function unbindUserFromRole(roleId: number, userId: number) { + const resp = await http.delete(`/api/roles/${roleId}/users/${userId}`); + return resp.data.data as boolean; +} + export async function fetchLogs(params: any) { const resp = await http.get("/api/logs", { params }); return resp.data.data; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c90761d..f9c3327 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -44,6 +44,15 @@ export default function Login() { init(); }, [loadCaptcha]); + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + if (searchParams.get("timeout") === "1") { + message.warning(t('login.loginTimeout')); + // Clean up the URL to avoid repeated messages on refresh + window.history.replaceState({}, document.title, window.location.pathname); + } + }, [t]); + const onFinish = async (values: any) => { setLoading(true); try { diff --git a/frontend/src/pages/Roles.tsx b/frontend/src/pages/Roles.tsx index 98ce6f4..26e8796 100644 --- a/frontend/src/pages/Roles.tsx +++ b/frontend/src/pages/Roles.tsx @@ -15,7 +15,8 @@ import { Col, Tabs, Empty, - Select + Select, + Modal } from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; @@ -28,7 +29,10 @@ import { saveRolePermissions, updateRole, deleteRole, - fetchUsersByRoleId + fetchUsersByRoleId, + bindUsersToRole, + unbindUserFromRole, + listUsers } from "../api"; import type { SysPermission, SysRole, SysUser } from "../types"; import { usePermission } from "../hooks/usePermission"; @@ -40,7 +44,8 @@ import { DeleteOutlined, KeyOutlined, UserOutlined, - SaveOutlined + SaveOutlined, + UserAddOutlined } from "@ant-design/icons"; import "./Roles.css"; @@ -107,6 +112,12 @@ export default function Roles() { const [roleUsers, setRoleUsers] = useState([]); const [loadingUsers, setLoadingUsers] = useState(false); + // User selection states + const [allUsers, setAllUsers] = useState([]); + const [userModalOpen, setUserModalOpen] = useState(false); + const [selectedUserKeys, setSelectedUserKeys] = useState([]); + const [userSearchText, setUserSearchText] = useState(""); + // Search const [searchText, setSearchText] = useState(""); @@ -122,6 +133,53 @@ export default function Roles() { [permissions, t] ); + const loadAllUsers = async () => { + try { + const list = await listUsers(); + setAllUsers(list || []); + } catch (e) { + console.error(e); + } + }; + + const openUserModal = () => { + loadAllUsers(); + setSelectedUserKeys([]); + setUserModalOpen(true); + }; + + const handleAddUsers = async () => { + if (!selectedRole || selectedUserKeys.length === 0) return; + try { + await bindUsersToRole(selectedRole.roleId, selectedUserKeys); + message.success(t('common.success')); + setUserModalOpen(false); + selectRole(selectedRole); + } catch (e) { + message.error(t('common.error')); + } + }; + + const handleUnbindUser = async (userId: number) => { + if (!selectedRole) return; + try { + await unbindUserFromRole(selectedRole.roleId, userId); + message.success(t('common.success')); + selectRole(selectedRole); + } catch (e) { + message.error(t('common.error')); + } + }; + + const filteredModalUsers = useMemo(() => { + const existingIds = new Set(roleUsers.map(u => u.userId)); + return allUsers.filter(u => + !existingIds.has(u.userId) && + (u.username.toLowerCase().includes(userSearchText.toLowerCase()) || + u.displayName.toLowerCase().includes(userSearchText.toLowerCase())) + ); + }, [allUsers, roleUsers, userSearchText]); + const loadPermissions = async () => { try { const list = await listPermissions(); @@ -400,6 +458,16 @@ export default function Roles() { tab={} key="users" > +
+ +
{s === 1 ? '正常' : '禁用'} + }, + { + title: t('common.action'), + key: 'action', + width: 100, + render: (_, record) => ( + handleUnbindUser(record.userId)} + disabled={!can("sys_role:update")} + > +
setSelectedUserKeys(keys as number[]) + }} + columns={[ + { title: t('users.displayName'), dataIndex: 'displayName' }, + { title: t('users.username'), dataIndex: 'username' }, + { title: t('users.org'), dataIndex: 'orgId', render: () => '-' } // Simplification + ]} + /> + + {/* Basic Info Drawer */}