From 2534723842a07af43a0ee252ba6d999c99a566b3 Mon Sep 17 00:00:00 2001 From: andyesfly <35127884@qq.com> Date: Thu, 28 May 2026 16:25:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=AE=A1=E7=90=86=E4=B8=8E=E7=99=BB=E5=BD=95=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=BB=86=E8=8A=82=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 调整仪表盘图标从DeviceOutlined为MonitorOutlined 2. 新增403无权访问页面与路由配置 3. 完善权限存储与路由守卫逻辑 4. 修复用户信息获取与token持久化问题 5. 补充普通管理员权限初始化与补丁脚本 6. 优化后端用户服务与认证控制器逻辑 7. 调整登录表单交互与请求流程 --- client/src/router/index.ts | 23 ++++++- client/src/stores/permission.ts | 23 ++++++- client/src/views/403.vue | 81 ++++++++++++++++++++++++ client/src/views/dashboard/index.vue | 6 +- client/src/views/login/index.vue | 48 +++++++++----- database/fix-admin-permissions.sql | 28 ++++++++ database/init.sql | 13 ++++ server/src/controller/auth.controller.ts | 22 ++++++- server/src/service/user.service.ts | 65 ++++++++++++------- 9 files changed, 259 insertions(+), 50 deletions(-) create mode 100644 client/src/views/403.vue create mode 100644 database/fix-admin-permissions.sql diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 4bd4432..3b59812 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -9,6 +9,12 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/login/index.vue"), meta: { requiresAuth: false, title: "登录" } }, + { + path: "/403", + name: "Forbidden", + component: () => import("@/views/403.vue"), + meta: { requiresAuth: false, title: "无权访问" } + }, { path: "/", component: () => import("@/layouts/index.vue"), @@ -112,9 +118,20 @@ router.beforeEach(async (to, _from, next) => { if (!permissionStore.userInfo) { try { - await permissionStore.fetchUserInfo(); + const success = await permissionStore.fetchUserInfo(); + if (!success) { + next({ + path: "/login", + query: { redirect: to.fullPath } + }); + return; + } } catch (error) { - permissionStore.logout(); + console.error('获取用户信息失败:', error); + next({ + path: "/login", + query: { redirect: to.fullPath } + }); return; } } @@ -129,4 +146,4 @@ router.beforeEach(async (to, _from, next) => { next(); }); -export default router; +export default router; \ No newline at end of file diff --git a/client/src/stores/permission.ts b/client/src/stores/permission.ts index 682c4d3..29dd2f7 100644 --- a/client/src/stores/permission.ts +++ b/client/src/stores/permission.ts @@ -14,7 +14,7 @@ interface UserInfo { export const usePermissionStore = defineStore('permission', () => { const userInfo = ref(null); - const token = ref(''); + const token = ref(localStorage.getItem('access_token') || ''); const permissions = ref([]); const isLoggedIn = computed(() => !!token.value); @@ -64,12 +64,28 @@ export const usePermissionStore = defineStore('permission', () => { if (res.code === 200) { userInfo.value = res.data; permissions.value = res.data.permissions || []; + return true; } + return false; } catch (error) { console.error('获取用户信息失败:', error); + return false; } } + function setToken(newToken: string) { + token.value = newToken; + localStorage.setItem('access_token', newToken); + } + + function setUserInfo(info: UserInfo | null) { + userInfo.value = info; + } + + function setPermissions(perms: string[]) { + permissions.value = perms; + } + return { userInfo, token, @@ -80,5 +96,8 @@ export const usePermissionStore = defineStore('permission', () => { login, logout, fetchUserInfo, + setToken, + setUserInfo, + setPermissions, }; -}); +}); \ No newline at end of file diff --git a/client/src/views/403.vue b/client/src/views/403.vue new file mode 100644 index 0000000..8a0a7ef --- /dev/null +++ b/client/src/views/403.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/client/src/views/dashboard/index.vue b/client/src/views/dashboard/index.vue index 0898e79..62ae8c2 100644 --- a/client/src/views/dashboard/index.vue +++ b/client/src/views/dashboard/index.vue @@ -121,7 +121,7 @@ import { message, } from 'ant-design-vue'; import { - DeviceOutlined, + MonitorOutlined, CheckCircleOutlined, AlertOutlined, TeamOutlined, @@ -149,7 +149,7 @@ const statCards = [ { key: 'deviceTotal', title: '设备总数', - icon: markRaw(DeviceOutlined), + icon: markRaw(MonitorOutlined), color: '#1890ff', iconBg: '#e6f7ff', }, @@ -603,4 +603,4 @@ function getActivityColor(module: string): string { } } } - + \ No newline at end of file diff --git a/client/src/views/login/index.vue b/client/src/views/login/index.vue index eb51791..04cd159 100644 --- a/client/src/views/login/index.vue +++ b/client/src/views/login/index.vue @@ -20,7 +20,7 @@ + :disabled="loading" @pressEnter="handlePasswordEnter"> @@ -29,7 +29,7 @@ + :disabled="loading" @pressEnter="handleLogin"> @@ -68,6 +68,7 @@ import { reactive, ref, onMounted, nextTick } from 'vue' import { useRouter, useRoute } from 'vue-router' import { message } from 'ant-design-vue' +import axios from 'axios' import { UserOutlined, LockOutlined, @@ -112,33 +113,46 @@ onMounted(() => { }) }) -function focusPassword() { +function handlePasswordEnter() { passwordInputRef.value?.focus() } -function submitForm() { - formRef.value?.submit() -} - async function handleLogin() { try { loading.value = true - await permissionStore.login(formState.username, formState.password) + const response = await axios.post('/api/auth/login', { + username: formState.username, + password: formState.password + }) - if (formState.rememberMe) { - localStorage.setItem('remembered_username', formState.username) - } else { - localStorage.removeItem('remembered_username') - } + const res = response.data - message.success('登录成功!') + if (res.code === 200) { + const { accessToken, refreshToken, userInfo } = res.data + + localStorage.setItem('access_token', accessToken) + localStorage.setItem('refresh_token', refreshToken) + + permissionStore.setToken(accessToken) + permissionStore.setUserInfo(userInfo) + permissionStore.setPermissions(userInfo.permissions || []) + + if (formState.rememberMe) { + localStorage.setItem('remembered_username', formState.username) + } else { + localStorage.removeItem('remembered_username') + } + + message.success('登录成功!') - setTimeout(() => { const redirect = (route.query.redirect as string) || '/' router.push(redirect) - }, 500) + } else { + throw new Error(res.message || '登录失败') + } } catch (error: any) { + console.error('登录失败:', error) message.error(error.message || '登录失败,请检查用户名和密码') formState.password = '' nextTick(() => { @@ -344,4 +358,4 @@ async function handleLogin() { color: rgba(255, 255, 255, 0.5); } } - + \ No newline at end of file diff --git a/database/fix-admin-permissions.sql b/database/fix-admin-permissions.sql new file mode 100644 index 0000000..42a58aa --- /dev/null +++ b/database/fix-admin-permissions.sql @@ -0,0 +1,28 @@ +-- ============================================================ +-- 普通管理员权限补丁 (用于已有数据库) +-- 执行方式: 在MySQL中执行此SQL即可 +-- ============================================================ +USE smart_grid_iot; + +-- 给普通管理员角色(role_id=2, code='admin')分配基础管理权限 +-- 先删除已存在的(避免重复), 再插入 +DELETE FROM `role_permissions` WHERE `role_id` = 2; + +INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES +(2, (SELECT id FROM permissions WHERE code = 'dashboard')), +(2, (SELECT id FROM permissions WHERE code = 'system')), +(2, (SELECT id FROM permissions WHERE code = 'settings')), +(2, (SELECT id FROM permissions WHERE code = 'user:list')), +(2, (SELECT id FROM permissions WHERE code = 'user:create')), +(2, (SELECT id FROM permissions WHERE code = 'user:update')), +(2, (SELECT id FROM permissions WHERE code = 'role:list')), +(2, (SELECT id FROM permissions WHERE code = 'system:basicSetting')), +(2, (SELECT id FROM permissions WHERE code = 'system:securitySetting')), +(2, (SELECT id FROM permissions WHERE code = 'log:operation')); + +-- 验证结果 +SELECT r.name AS 角色名称, r.code AS 角色编码, COUNT(rp.permission_id) AS 权限数量 +FROM roles r +LEFT JOIN role_permissions rp ON r.id = rp.role_id +WHERE r.id = 2 +GROUP BY r.id; diff --git a/database/init.sql b/database/init.sql index cb7626a..ba62b17 100644 --- a/database/init.sql +++ b/database/init.sql @@ -195,6 +195,19 @@ INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES (1, 1); INSERT INTO `role_permissions` (`role_id`, `permission_id`) SELECT 1, id FROM permissions; +-- 给普通管理员角色(role_id=2)分配基础管理权限(不含删除/重置密码/角色管理等高危操作) +INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES +(2, 1), -- dashboard 仪表盘 +(2, 2), -- system 系统管理(父菜单) +(2, 3), -- settings 系统设置(父菜单) +(2, 4), -- user:list 用户管理 +(2, 6), -- user:create 新增用户 +(2, 7), -- user:update 编辑用户 +(2, 5), -- role:list 角色管理(仅查看) +(2, 14), -- system:basicSetting 基本设置 +(2, 15), -- system:securitySetting 安全设置 +(2, 16); -- log:operation 操作日志 + -- ============================================================ -- 初始化完成提示 diff --git a/server/src/controller/auth.controller.ts b/server/src/controller/auth.controller.ts index 151f87d..4722be0 100644 --- a/server/src/controller/auth.controller.ts +++ b/server/src/controller/auth.controller.ts @@ -5,6 +5,8 @@ import { TokenUtil } from "../util/token"; import { PermissionService } from "../service/permission.service"; import { InjectRepository } from "@midwayjs/sequelize"; import User from "../entity/user.entity"; +import UserRole from "../entity/userRole.entity"; +import Role from "../entity/role.entity"; @Controller("/api/auth") export class AuthController { @@ -23,6 +25,12 @@ export class AuthController { @InjectRepository(User) userModel!: typeof User; + @InjectRepository(UserRole) + userRoleModel!: typeof UserRole; + + @InjectRepository(Role) + roleModel!: typeof Role; + @Post("/login") async login(@Body() body: { username: string; password: string }) { try { @@ -102,7 +110,14 @@ export class AuthController { const decoded = this.tokenUtil.verifyToken(token); const userId = decoded.userId; - const user = await this.userModel.findByPk(userId); + const user = await this.userModel.findByPk(userId, { + include: [ + { + model: this.userRoleModel, + include: [this.roleModel] + } + ] + }); if (!user) { return { code: 404, @@ -111,6 +126,7 @@ export class AuthController { } const permissions = await this.permissionService.getUserPermissions(userId); + const roles = (user as any).userRoles?.map((ur: any) => ur.role?.code).filter(Boolean) || []; return { code: 200, @@ -120,7 +136,7 @@ export class AuthController { username: user.username, nickname: user.nickname || user.username, avatar: user.avatar, - roles: [], + roles, permissions } }; @@ -131,4 +147,4 @@ export class AuthController { }; } } -} \ No newline at end of file +} diff --git a/server/src/service/user.service.ts b/server/src/service/user.service.ts index 3fd6b11..62e6f19 100644 --- a/server/src/service/user.service.ts +++ b/server/src/service/user.service.ts @@ -1,5 +1,6 @@ import { Provide, Inject } from "@midwayjs/core"; import { InjectRepository } from "@midwayjs/sequelize"; +import { Repository } from "sequelize-typescript"; import User from "../entity/user.entity"; import UserRole from "../entity/userRole.entity"; import Role from "../entity/role.entity"; @@ -9,17 +10,17 @@ import { hashPassword } from "../util/crypto"; @Provide() export class UserService { @InjectRepository(User) - userModel!: typeof User; + userModel!: Repository; @InjectRepository(UserRole) - userRoleModel!: typeof UserRole; + userRoleModel!: Repository; @InjectRepository(Role) - roleModel!: typeof Role; + roleModel!: Repository; - async list(query: { page?: number; pageSize?: number; keyword?: string; status?: number; roleId?: number }): Promise { - const page = query.page || 1; - const pageSize = query.pageSize || 10; + async list(query: { page?: any; pageSize?: any; keyword?: string; status?: number; roleId?: number }): Promise { + const page = Number(query.page) || 1; + const pageSize = Number(query.pageSize) || 10; const where: any = {}; if (query.keyword) { @@ -32,11 +33,11 @@ export class UserService { const includeOptions: any = [ { - model: this.userRoleModel, + model: UserRole, required: !!query.roleId, include: [ { - model: this.roleModel, + model: Role, where: query.roleId ? { id: query.roleId } : {}, required: !!query.roleId } @@ -50,14 +51,21 @@ export class UserService { limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]], - distinct: true + distinct: true, + raw: false + }); + + const list = rows.map((user) => { + const u = user.toJSON(); + delete u.password; + return u; }); return { code: 200, message: "获取用户列表成功", data: { - list: rows, + list, total: count, page: Number(page), pageSize: Number(pageSize) @@ -69,8 +77,8 @@ export class UserService { const user = await this.userModel.findByPk(id, { include: [ { - model: this.userRoleModel, - include: [this.roleModel] + model: UserRole, + include: [Role] } ] }); @@ -83,10 +91,13 @@ export class UserService { }; } + const userData = user.toJSON(); + delete userData.password; + return { code: 200, message: "获取用户详情成功", - data: user + data: userData }; } @@ -123,16 +134,19 @@ export class UserService { const createdUser = await this.userModel.findByPk(user.id, { include: [ { - model: this.userRoleModel, - include: [this.roleModel] + model: UserRole, + include: [Role] } ] }); + const userData = createdUser!.toJSON(); + delete userData.password; + return { code: 200, message: "创建用户成功", - data: createdUser + data: userData }; } @@ -168,16 +182,19 @@ export class UserService { const updatedUser = await this.userModel.findByPk(id, { include: [ { - model: this.userRoleModel, - include: [this.roleModel] + model: UserRole, + include: [Role] } ] }); + const userData = updatedUser!.toJSON(); + delete userData.password; + return { code: 200, message: "更新用户成功", - data: updatedUser + data: userData }; } @@ -242,10 +259,13 @@ export class UserService { await user.update({ status }); + const userData = user.toJSON(); + delete userData.password; + return { code: 200, message: "状态更新成功", - data: user + data: userData }; } @@ -255,7 +275,8 @@ export class UserService { order: [ ["sort_order", "ASC"], ["created_at", "DESC"] - ] + ], + raw: true }); return { @@ -277,4 +298,4 @@ export class UserService { await this.userRoleModel.bulkCreate(userRoles); } -} +} \ No newline at end of file