后台管理系统八股

发布于:2025-06-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

项⽬地址:https://github.com/Xiaodie-888/Frontend.git 前端

                  https://github.com/Xiaodie-888/backend.git 后端
技术栈:Vue3+Vite+Typrscript+Pinia+Element-plus+Vue-Router+Express.js+MySQL

核⼼⼯作与技术:

  1. 基础组件封装:基于 Element Plus 封装了 SearchForm 和 DataTable 两个核⼼业务组件,使⽤ TypeScript 定义类型系统,并将分⻚等通⽤逻辑提取到 useTable Hook 中,使组件在三个核⼼业务模块中复⽤,提升开发效率。
  2. 权限控制实现:实现了基于 JWT 的登录认证流程,开发了动态路由加载和按钮级权限控制,封装统⼀的 axios 请求拦截器处理 token,有效解决了系统权限管理混乱的问题。
  3. 性能优化:通过路由懒加载和 keep-alive 缓存优化⾸屏加载,针对产品列表实现分⻚加载和表格组件更新优化,显著提升了系统响应速度。

🔐 基于JWT的完整权限认证系统实现

循序渐进的系统构建过程

🔐 第一步:搭建JWT认证基础

先在后端建立JWT认证机制,使用jsonwebtoken库生成Token。用户登录时,系统验证账号密码后,将用户信息(排除密码等敏感数据)加密生成7小时有效期的JWT Token,以Bearer格式返回给前端。同时返回用户的department(部门)和position(职位)信息,为后续权限控制奠定基础。

🌐 第二步:封装axios请求拦截器

再在前端封装统一的HTTP请求处理机制。在请求拦截器中自动从localStorage获取Token并添加到请求头的Authorization字段;在响应拦截器中统一处理各种状态码,特别是401状态码时自动清除本地存储并跳转登录页,实现Token失效的自动处理。这样开发者无需手动管理Token状态。

🔄 第三步:开发动态路由加载系统

然后基于用户角色实现路由的动态生成。后端根据用户部门返回对应的路由配置(超级管理员拥有全部8个模块,人事部仅有用户管理,产品部拥有产品相关3个模块)。前端通过import.meta.glob动态导入组件,使用router.addRoute将权限路由添加到Vue Router中,配合Pinia的数据持久化,确保页面刷新后路由状态保持。

🎯 第四步:实现按钮级权限控制

接着建立细粒度的权限控制体系。采用部门级 + 职位级双重权限模型:通过v-if="userAdmin||superAdmin"控制菜单显示;基于职位进行功能权限控制,如审核功能用v-if="!admin()"限制仅经理可操作;在关键操作函数中进行权限拦截,如if(position=='员工') return阻止员工编辑产品。形成了从路由→菜单→按钮→操作的四级权限防护。

🔄 第五步:状态管理与持久化

最后使用Pinia管理权限状态,配合pinia-plugin-persistedstate插件实现数据持久化。将用户信息、权限数据、动态路由等关键状态进行统一管理,确保系统重启或页面刷新后用户无需重新登录。

说说你对keep-alive的理解是什么?

一、keep-alive 的核心功能

keep-alive是 Vue 的内置组件,主要作用是缓存组件实例避免组件切换时重复渲染 DOM,从而保留组件状态。当它包裹动态组件时,未激活的组件会被缓存到内存中,而非销毁,再次切换时直接从缓存中恢复状态。

通过keep-alive,Vue 实现了高效的组件状态管理,尤其适用于标签页切换、多视图缓存等场景,能显著提升用户体验和应用性能。

二、关键属性与用法
  1. 核心属性

    • include:指定需要缓存的组件名称(字符串、正则或数组)。
    • exclude:指定不需要缓存的组件名称(字符串、正则或数组)。
    • max:限制缓存的组件数量,超出时按 LRU(最近最少使用)规则移除最早的缓存。
  2. 基本用法

    <!-- 缓存所有动态组件 -->
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
    
    <!-- 按名称缓存指定组件 -->
    <keep-alive include="Home,About">
      <component :is="currentComponent"></component>
    </keep-alive>
    
     

    组件匹配规则:优先检查name选项,若不存在则匹配局部注册名称,匿名组件无法被缓存。

三、生命周期钩子

keep-alive缓存的组件会新增两个钩子

  • activated:组件被激活(从缓存中恢复)时调用。
  • deactivated:组件被停用(缓存到内存)时调用。

生命周期对比

  • 首次进入组件:beforeCreate → created → mounted → activated
  • 切换后再次进入:activated(直接从缓存恢复,跳过mounted
四、在路由中的应用

通过路由元信息meta.keepAlive控制组件缓存:

  1. 路由配置

    const routes = [
      {
        path: '/list',
        name: 'ItemList',
        component: ItemList,
        meta: { keepAlive: true } // 标记需要缓存
      }
    ]
    
  2. 组件使用

    <keep-alive>
      <!-- 缓存标记为keepAlive的路由组件 -->
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <!-- 不缓存的组件直接渲染 -->
    <router-view v-if="!$route.meta.keepAlive"></router-view>
    
五、原理分析
  1. 缓存机制

    • keep-alive通过cache对象存储组件实例,键为组件key,值为组件 VNode。
    • keys数组记录缓存顺序,用于实现max限制下的 LRU 淘汰策略。
  2. 渲染逻辑

    • 首次渲染时,组件会被存入cachekeys添加对应key
    • 再次切换时,key存在于cache中,直接从缓存中获取组件实例,避免重新创建。
  3. 源码核心片段

    render() {
      const vnode = getFirstComponentChild(this.$slots.default)
      const componentOptions = vnode?.componentOptions
      const name = getComponentName(componentOptions)
      
      // 检查是否需要缓存(通过include/exclude过滤)
      if (需要缓存) {
        const key = vnode.key || componentOptions.Ctor.cid + '::' + componentOptions.tag
        if (this.cache[key]) {
          // 命中缓存,直接使用实例
          vnode.componentInstance = this.cache[key].componentInstance
          this.keys.push(this.keys.splice(this.keys.indexOf(key), 1)[0]) // 调整缓存顺序
        } else {
          // 未命中,存入缓存
          this.cache[key] = vnode
          this.keys.push(key)
          // 超出max时删除最早的缓存
          if (this.max && this.keys.length > this.max) {
            pruneCacheEntry(this.cache, this.keys[0], this.keys, this._vnode)
          }
        }
        vnode.data.keepAlive = true
      }
      return vnode
    }
    
六、缓存数据更新方案

当组件被缓存时,数据可能不会自动更新,可通过以下方式解决:

  1. activated钩子

    • 组件激活时重新获取数据:
    activated() {
      this.fetchData() // 重新请求数据
    }
    
  2. beforeRouteEnter导航守卫

    • 路由进入前触发,通过next(vm => ...)访问组件实例:
    beforeRouteEnter(to, from, next) {
      next(vm => {
        vm.fetchData() // 在组件实例中调用方法
      })
    }
    
七、注意事项
  • 服务器端渲染(SSR)activateddeactivated在 SSR 期间不生效。
  • 组件销毁:被exclude匹配的组件会被从缓存中移除,并触发$destroy
  • 性能优化:合理设置max,避免缓存过多组件导致内存占用过高。

Javascript本地存储的方式有哪些?区别及应用场景?

一、本地存储方式及特点

1. Cookie
  • 本质:小型文本文件,用于解决 HTTP 无状态问题,由服务器设置并发送到客户端存储。
  • 特点
    • 存储容量:单个 Cookie 不超过 4KB,每个域名最多存储 20 个 Cookie。
    • 生命周期:可通过 Expires 或 Max - Age 设置过期时间,若未设置则随浏览器关闭失效。
    • 数据类型仅支持字符串,需手动解析(如 JSON.stringify())。
    • 网络传输:每次 HTTP 请求时自动携带到服务器,增加请求头开销。
    • 安全性:可通过 Secure 属性限制仅在 HTTPS 下传输,HttpOnly 属性防止 XSS 攻击。
  • 使用示例
    // 设置 Cookie
    document.cookie = "username=John; expires=Thu, 31 Dec 2025 23:59:59 GMT; path=/";
    // 获取 Cookie
    const cookies = document.cookie.split('; ').reduce((obj, cookie) => {
      const [key, value] = cookie.split('=');
      obj[key] = value;
      return obj;
    }, {});
    // 删除 Cookie(设置过期时间为过去)
    document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
    
2. localStorage
  • 本质:HTML5 新增的本地存储 API,基于域名持久化存储数据。
  • 特点
    • 存储容量:通常为 5 - 10MB,不同浏览器略有差异。
    • 生命周期永久存储,除非手动删除或浏览器清除缓存。
    • 数据类型:仅支持字符串,需通过 JSON.stringify() 和 JSON.parse() 处理对象。
    • 共享性同一域名下的所有页面共享数据,支持跨窗口通信(通过 storage 事件)
    • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 使用示例
    // 存储对象
    localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));
    // 获取数据
    const user = JSON.parse(localStorage.getItem('user'));
    // 删除单个数据
    localStorage.removeItem('user');
    // 清空所有数据
    localStorage.clear();
    

localStorage 也不是完美的,它有两个缺点:

  • 无法像Cookie一样设置过期时间
  • 只能存入字符串,无法直接存对象
3. sessionStorage
  • 本质:与 localStorage 类似,但生命周期更短。
  • 特点
    • 存储容量:同 localStorage,约 5 - 10MB。
    • 生命周期:仅在当前浏览器会话(窗口)中有效,关闭窗口后数据自动删除。
    • 共享性:同一域名、同一窗口下的页面共享数据,不同窗口间不共享。
    • 数据类型:同 localStorage,仅支持字符串。
  • 使用示例
    // 存储临时数据
    sessionStorage.setItem('tempData', '临时内容');
    // 在同窗口其他页面获取数据
    const temp = sessionStorage.getItem('tempData');
    
4. IndexedDB
  • 本质:低级异步数据库 API,用于存储大量结构化数据。
  • 特点
    • 存储容量:理论上无上限(受浏览器和硬件限制),适合存储大量数据。
    • 生命周期:永久存储,除非手动删除。
    • 数据类型:支持存储 JavaScript 对象、文件(Blob)等复杂数据类型。
    • 操作方式:异步操作,不阻塞主线程,支持事务(Transaction)机制。
    • 索引机制:支持创建索引,实现高效查询(如按字段排序、模糊搜索)。
  • 使用示例(简化流程)
    // 打开/创建数据库
    const request = indexedDB.open('MyDatabase', 1);
    request.onsuccess = (event) => {
      const db = event.target.result;
      // 开始事务
      const transaction = db.transaction(['users'], 'readwrite');
      const store = transaction.objectStore('users');
      // 添加数据
      const user = { id: 1, name: 'Bob', email: 'bob@example.com' };
      const addRequest = store.add(user);
      addRequest.onsuccess = () => console.log('数据添加成功');
    };
    

二、核心区别对比

维度 Cookie localStorage sessionStorage IndexedDB
存储容量 4KB 以内 5 - 10MB 5 - 10MB 理论无上限
生命周期 可设置过期时间,或随浏览器关闭失效 永久存储,手动删除才失效 窗口关闭后失效 永久存储,手动删除才失效
数据类型 仅字符串 仅字符串(需序列化对象) 仅字符串(需序列化对象) 支持对象、Blob 等复杂类型
网络传输

自动携带到服务器

(每次 HTTP 请求时自动携带到服务器,增加请求头开销。)

仅本地存储,不参与传输 仅本地存储,不参与传输 仅本地存储,不参与传输
异步性 同步操作 部分浏览器同步 部分浏览器同步 异步操作
共享性 同域名下共享 同域名下所有窗口共享 同域名、同窗口下共享 同域名下共享
安全性 可通过 Secure/HttpOnly 增强 本地存储,需前端自行控制 本地存储,需前端自行控制 本地存储,需前端自行控制

三、应用场景

1. Cookie
  • 场景
    • 身份验证:存储登录令牌(如 JWT),随请求发送到服务器验证身份。
    • 用户偏好:记录用户主题设置、语言偏好等轻量级数据。
    • 购物车标记:临时存储未登录用户的购物车商品(结合后端同步)。
  • 注意:由于每次请求都会携带 Cookie,避免存储大量数据,且需注意安全风险(如 XSS、CSRF)。
2. localStorage
  • 场景
    • 持久化用户数据:存储用户设置、收藏夹、已读状态等长期需要保留的数据。
    • 离线缓存:缓存静态资源(如图片、JS/CSS 文件),减少重复请求。
    • 跨页面状态共享:如多标签页间共享购物车信息(通过 storage 事件监听)。
  • 示例
    • 电商网站存储用户勾选的「记住登录状态」选项。
    • 新闻类应用缓存已浏览的文章列表,避免重复加载。
3. sessionStorage
  • 场景
    • 临时会话数据:存储表单临时输入内容(如多步骤表单的中间数据),防止刷新丢失。
    • 单次会话隐私数据:如敏感操作的临时验证码、一次性登录凭证。
    • 单窗口状态管理:如视频播放进度(仅在当前窗口有效)。
  • 示例
    • 银行网站的转账页面,存储临时输入的金额和账号信息。
    • 在线考试系统中存储当前题目的答案,防止窗口关闭后数据丢失。
4. IndexedDB
  • 场景
    • 大规模数据存储:如离线地图、电子书库、大量用户日志记录。
    • 复杂数据查询:需要按字段索引查询(如按时间排序的聊天记录)。
    • 离线应用:如 PWA(渐进式 Web 应用)的本地数据存储,支持离线访问。
    • 文件存储:存储用户上传的文件(Blob),如在线文档编辑器的历史版本。
  • 示例
    • 笔记类应用存储数千条笔记及其附件。
    • 音乐播放器缓存用户下载的歌曲列表和元数据。

四、选择建议

  • 轻量级、需服务器交互的数据:选 Cookie(如登录令牌)。
  • 持久化、跨会话的简单数据:选 localStorage(如用户设置)。
  • 临时、单会话的简单数据:选 sessionStorage(如表单临时输入)。
  • 大量、复杂结构化数据或离线应用:选 IndexedDB(如大型文档库)。

hash 路由和 history 路由的区别是什么

一、核心原理差异
  1. hash 路由(锚点路由)

    1. 原理:利用 URL 中的 # 符号(锚点)及其后的内容(如 #/home)作为路由标识。浏览器对 # 后的内容不会向服务器发送请求,仅作为前端页面内部的锚点定位。
    2. 示例https://example.com/#/user/123,其中 #/user/123 是 hash 部分。
    3. 核心机制:通过监听 hashchange 事件(如用户手动修改 hash 或通过 location.hash 操作),前端解析 hash 内容并触发路由切换。
  2. history 路由(HTML5 History API)

    1. 原理:借助 HTML5 提供的 History API(如 pushState()replaceState())修改 URL 的路径部分(不含 #),模拟传统服务器路由的 URL 结构。
    2. 示例https://example.com/user/123,路径 /user/123 由前端控制。
    3. 核心机制:前端通过 API 修改 URL 后,需配合服务器配置(如 Nginx 重定向),确保刷新时服务器返回正确页面,避免 404 错误。

    二、详细对比表
    维度 hash 路由 history 路由
    URL 结构 包含 #,如 example.com/#/path 无 #,如 example.com/path
    服务器请求 hash 变化不触发服务器请求,仅前端处理 路径变化可能触发服务器请求(需后端支持)
    浏览器历史记录 修改 hash 不会新增历史记录,仅更新当前 URL 每次 pushState() 会新增历史记录,支持前进 / 后退
    SEO 支持 搜索引擎难以解析 hash 内容,SEO 效果差 URL 更符合传统格式,SEO 友好性更好
    服务器配置 无需额外配置,兼容性强 需要服务器配置重定向(如将所有路径指向首页)
    参数传递 可通过 hash 传递简单参数(如 #/user?id=1 可通过路径参数(/user/1)或 state 对象传递复杂数据
    兼容性 支持所有主流浏览器(包括 IE8+) 依赖 HTML5 History API,IE9 及以下不支持
    三、典型应用场景

    hash 路由适用场景

    • 简单单页应用:如移动端 H5 页面、无需 SEO 的内部管理系统,无需后端配置即可快速实现路由。
    • 兼容性要求高:需支持低版本浏览器(如 IE8)时,hash 是更稳妥的选择。
    • 纯前端交互:如图片查看器的锚点定位、表单步骤切换,无需服务器参与。

    history 路由适用场景

    • 需要 SEO 的网站:如博客、新闻类网站,清晰的 URL 结构有利于搜索引擎抓取。
    • 复杂应用架构:路径参数可直接映射资源(如 https://api.com/posts/1),便于前后端路由统一。
    • 模拟传统网站体验:用户可直接复制路径、通过书签收藏页面,刷新后仍能正确显示内容(需后端配合)。

    hash 路由:简单、兼容性强,适合纯前端场景,但 URL 不够美观且 SEO 能力弱。

    history 路由:URL 更符合直觉,支持 SEO 和复杂交互,但需要后端配置支持,且兼容性略差。

    四、后端支持与配置示例

    history 路由的服务器配置(以 Nginx 为例)
    当用户刷新 example.com/path 时,服务器需将请求重定向到首页(如 index.html),由前端路由处理路径:

    server {
        listen 80;
        server_name example.com;
        root /path/to/your/app;
        
        location / {
            try_files $uri $uri/ /index.html;  # 所有路径重定向到首页
        }
    }
    

    前端路由库的选择

    • hash 路由:Vue Router 可通过 mode: 'hash' 启用(默认模式),React Router 需使用 HashRouter
    • history 路由:Vue Router 需设置 mode: 'history',React Router 使用 BrowserRouter,同时确保后端已配置重定向。

    当 JWT 过期时,浏览器会发起一个新的请求来刷新 token。如果此时多个请求同时发起,那么每个请求都会触发一次 token 刷新,就会导致多次请求刷新的问题,你该怎么进行解决呢

    解决措施:维护一个 task queue 和 isRefresh 的标识,如果处于正在刷新 token 的阶段,之后发出的多个请求保存在一个队列中,等 token 刷新完后再请求(避免多次刷新 token)

    JWT 的原理是什么

    JWT 的原理可以参考阮一峰的这篇文章:JWT 原理,需要核心掌握的就是 JWT 的结构以及 Base64Url 算法(后面会提到)

    JWT 分为三个部分:

    • Header(头部):以 JSON 对象的形式描述 JWT 的元数据,包括签名的算法以及 token 的类型。最后,将上述的 JSON 通过 Base64Url 算法转成字符串
    • Payload(负载):用来存放实际需要传递的数据,也通过 Base64Url 算法转为字符串
    • Signature(签名):对前两部分的签名,防止数据被篡改

    Base64Url 算法:

    JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法

    一、JWT 核心原理
    1. 结构与组成

      • Header:声明使用的签名算法(如 HS256、RS256)和类型(固定为 JWT)
        {
          "alg": "HS256",
          "typ": "JWT"
        }
        
      • Payload:存储声明(Claims),包括注册声明(如issexp)和自定义声明(如userIdroles
        {
          "sub": "1234567890",
          "name": "John Doe",
          "iat": 1516239022,
          "exp": 1516242622
        }
        
      • Signature:对 Header 和 Payload 的哈希签名,确保数据未被篡改
        HMACSHA256(
          base64UrlEncode(header) + "." +
          base64UrlEncode(payload),
          secret
        )
        
    2. 工作流程

      • 用户登录:客户端发送凭证(用户名 / 密码)到服务器
      • 生成 Token:服务器验证通过后,用私钥生成 JWT 并返回
      • 存储 Token:客户端将 Token 存储在 localStorage 或 Cookie 中
      • 携带 Token:每次请求时在Authorization头中添加Bearer {token}
      • 验证 Token:服务器用公钥或相同私钥验证签名和有效期
    二、JWT 鉴权的实现步骤
    1. 生成 JWT(服务端)
    const jwt = require('jsonwebtoken');
    
    // 登录接口
    app.post('/api/login', async (req, res) => {
      // 验证用户凭证
      const user = await User.findOne({ username: req.body.username });
      if (!user || !comparePassword(req.body.password, user.password)) {
        return res.status(401).json({ message: '认证失败' });
      }
    
      // 生成Token(包含用户ID和角色)
      const token = jwt.sign(
        { 
          userId: user._id,
          roles: user.roles,
          username: user.username
        },
        process.env.JWT_SECRET, // 私钥
        { 
          expiresIn: '1h', // 有效期
          algorithm: 'HS256' // 签名算法
        }
      );
    
      res.json({ token });
    });
    
    2. 前端存储与请求携带
    // 登录成功后存储Token
    localStorage.setItem('auth_token', token);
    
    // 配置axios拦截器自动携带Token
    axios.interceptors.request.use(config => {
      const token = localStorage.getItem('auth_token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
    
    3. 验证 JWT(服务端)
    const expressJwt = require('express-jwt');
    
    // 验证中间件
    const authMiddleware = expressJwt({
      secret: process.env.JWT_SECRET,
      algorithms: ['HS256'],
      credentialsRequired: true // 要求必须有Token
    }).unless({
      path: [
        '/api/login', // 白名单:登录接口无需验证
        '/api/register'
      ]
    });
    
    // 应用中间件
    app.use(authMiddleware);
    
    // 受保护的路由
    app.get('/api/userInfo', (req, res) => {
      // 验证通过后,用户信息会被注入到req.user中
      res.json({
        message: '用户信息',
        user: req.user
      });
    });
    
    4. 处理 Token 过期与刷新
    // 错误处理中间件
    app.use((err, req, res, next) => {
      if (err.name === 'UnauthorizedError') {
        // Token过期或无效
        if (err.code === 'token_expired') {
          return res.status(401).json({
            message: 'Token已过期,请刷新或重新登录',
            code: 40101
          });
        }
        return res.status(401).json({ message: '无效Token' });
      }
      next(err);
    });
    
    // Token刷新接口
    app.post('/api/refreshToken', async (req, res) => {
      const oldToken = req.headers.authorization?.split(' ')[1];
      if (!oldToken) {
        return res.status(400).json({ message: '缺少Token' });
      }
    
      try {
        // 验证旧Token签名(忽略过期时间)
        const decoded = jwt.verify(oldToken, process.env.JWT_SECRET, { ignoreExpiration: true });
        
        // 生成新Token
        const newToken = jwt.sign(
          { userId: decoded.userId, roles: decoded.roles },
          process.env.JWT_SECRET,
          { expiresIn: '1h' }
        );
        
        res.json({ token: newToken });
      } catch (err) {
        res.status(401).json({ message: '刷新失败,请重新登录' });
      }
    });
    
    三、安全增强措施
    1. 使用 HTTPS:防止 Token 在传输过程中被截获
    2. 设置合理的过期时间
      • 短期访问 Token(如 15 分钟)+ 长期刷新 Token(如 7 天)
      • 敏感操作(如支付)使用一次性 Token
    3. 黑名单机制
      // 登出时将Token加入黑名单(Redis实现)
      app.post('/api/logout', async (req, res) => {
        const token = req.headers.authorization?.split(' ')[1];
        if (token) {
          const decoded = jwt.decode(token);
          const expireTime = decoded.exp - Math.floor(Date.now() / 1000);
          await redisClient.set(`revoked:${token}`, 'true', 'EX', expireTime);
        }
        res.json({ message: '登出成功' });
      });
      
      // 验证中间件中检查黑名单
      authMiddleware.unless({
        path: ['/api/login', '/api/refreshToken']
      });
      
      app.use(async (req, res, next) => {
        if (req.user) {
          const token = req.headers.authorization?.split(' ')[1];
          const isRevoked = await redisClient.exists(`revoked:${token}`);
          if (isRevoked) {
            return res.status(401).json({ message: 'Token已失效' });
          }
        }
        next();
      });
      
    4. CSRF 防护
      • 不使用 Cookie 存储 Token
      • 敏感操作添加验证码或二次验证
    5. 非对称加密(RS256)
      // 生成Token(服务端)
      const privateKey = fs.readFileSync('private.key');
      const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
      
      // 验证Token(客户端/其他服务)
      const publicKey = fs.readFileSync('public.key');
      jwt.verify(token, publicKey, { algorithm: 'RS256' });
      

    你还知道其他什么登录方式么,能不能说一下这些方式和 JWT 方案的区别

    Session

    Session 是在用户登录成功后,由服务器自动生成的唯一标识

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    浏览器        ⇄        服务器

    ----------           -------------------------

    输入账号密码         验证成功 → 生成 Session

         ↓                        ↓

    发送 POST /login    Set-Cookie: sessionid=xxx

         ↓                        ↓

    保存 Cookie           服务器内存保存 sessionid → userInfo

         

    后续请求自动带 Cookie

         

    服务器根据 sessionid 查找用户信息

    也就是说,当浏览器接收到服务器返回的 sessionID 之后,会先将此信息存入 cookie,同时 cookie 记录此 sessionID 属于哪个域名;在后续请求的时候,cookie 信息会被自动发送给服务端,服务端也会从 cookie 中获取 sessionID,再根据 ID 寻找响应的 session 信息

    适用场景

    适合于同源 Web 应用,且无需跨域

    Session 和 Token 的区别

    对比项 SessionToken                                                    Token

    存储位置 存储在服务端(如内存或 Redis) 存储在客户端(如 Cookie 或 LocalStorage)
    状态管理 有状态(需要服务器存储用户会话) 无状态(不需要服务器存储用户信息)
    跨域支持 不支持跨域(需要特殊配置或共享存储) 支持跨域(特别是 JWT)
    安全性 相对安全,易于控制(服务器存储) 需要注意防止 XSS 攻击,存储需小心

    详细解析你的JWT认证和权限控制系统

    🔐 第一步:后端 JWT 认证实现

    1. JWT 配置 (backend_wz/jwt_config/index.js)
    jwtSecretKey:'gbms',
    

            定义了 JWT 的密钥,用于 token 的加密和解密

    1. 登录认证逻辑 (backend_wz/router_handler/login.js)
    // 第四步 生成返回给前端的token
    // 剔除加密后的密码,头像,创建时间,更新时间
    const user = {
        ...results[0],
        password: '',
        imageUrl: '',
        create_time: '',
        update_time: '',
    }
    // 设置token的有效时长 有效期为7个小时
    const tokenStr = jwt.sign(user, jwtconfig.jwtSecretKey, {
        expiresIn: '7h'
    })
    res.send({
        results: results[0],
        status: 0,
        message: '登录成功',
        token: 'Bearer ' + tokenStr,
    })
    

    核心功能:

    • 验证用户账号密码
    • 生成包含用户信息的 JWT token
    • 设置 7 小时的过期时间
    • 返回 "Bearer" + token 格式

    1. 基于角色的路由配置
    const superAdminRouter = [{
            name: 'set',
            path: '/set',
            component: 'set/index'
        },
        {
            name: 'user',
            path: '/user',
            component: 'user/index'
        }, {
            name: 'login_log',
            path: '/login_log',
            component: 'login_log/index'
        }, {
            name: 'operation_log',
            path: '/operation_log',
            component: 'operation_log/index'
        }, {
            name: 'product',
            path: '/product',
            component: 'product/index'
        }, {
            name: 'audit',
            path: '/audit',
            component: 'product/audit'
        }, {
            name: 'outbound',
            path: '/outbound',
            component: 'product/outbound'
        },
        {
            name: 'statistics',
            path: '/statistics',
            component: 'statistics/index'
        }
    ]
    

    🌐 第二步:前端 axios 拦截器实现

    1. 请求拦截器添加 Token
    instance.interceptors.request.use(function (config) {
        // 添加请求之前的逻辑 - 从localStorage获取token并添加到请求头
        const token = localStorage.getItem('token')
        if (token) {
            config.headers.Authorization = token // 后端返回的已经包含 "Bearer " 前缀
        }
        return config;
    }, function (error) {
    
    1. 响应拦截器处理 Token 过期
                case 401:
                    ElMessage.error('未授权,请登录')
                    // token过期或无效,清除本地存储并跳转到登录页
                    localStorage.clear()
                    router.push('/')
                    break
    

    🔄 第三步:动态路由加载实现

    1. 菜单 Store 管理 (后台前端_wz/src/stores/menu.ts)
            function compilerMenu (arr:any) {
                if(!arr) return
                menuData.value = arr
                arr.forEach((item:any)=>{
                    let rts = {
                        name:item.name,
                        path:item.path,
                        component:item.component
                    }
                    if(item.children && item.children.length){
                        compilerMenu(item.children)
                    }
                    if(!item.children){
                        let path = loadComponent(item.component)
                        // console.log(path)
                        rts.component = path;
                        router.addRoute('menu',rts)
                    }
    
                    function loadComponent(url:string){
                        let Module = import.meta.glob("@/views/**/*.vue")
                        return Module[`/src/views/${url}.vue`]
    

    关键特性:

    • 动态导入组件:使用 import.meta.glob 实现组件的动态加载
    • 路由动态添加:使用 router.addRoute 将权限路由添加到路由表
    • 递归处理:支持嵌套路由的处理
    1. 登录后获取用户权限路由
        userStore.userInfo(account)
        const routerList = await returnMenuList(id)
        // console.log(routerList)
        menuStore.setRouter(routerList)
        router.push('/menu') // 登录成功后跳转至菜单页面
    

    🛡️ 第四步:路由守卫权限控制


    路由守卫实现 (后台前端_wz/src/router/index.ts)

    // 路由守卫 - 基于token的权限控制
    router.beforeEach((to, from, next) => {
      const token = localStorage.getItem('token')
      
      // 白名单路由(不需要token验证的页面)
      const whiteList = ['/']
      
      if (token) {
        // 已登录状态
        if (to.path === '/') {
          // 如果已登录,访问登录页则重定向到菜单页
          next('/menu')
        } else {
          // 正常访问其他页面
          next()
        }
      } else {
        // 未登录状态
        if (whiteList.indexOf(to.path) !== -1) {
          // 访问白名单页面,放行
          next()
        } else {
          // 访问需要权限的页面,重定向到登录页
          ElMessage.error('请先登录')
          next('/')
        }
      }
    })
    

    📊 系统架构总结
    你的权限控制系统解决了以下问题:

    • 身份认证:基于 JWT 的无状态认证
    • 权限管理:基于角色的权限控制(RBAC)
    • 路由保护:防止未授权访问
    • 动态菜单:根据权限动态生成菜单
    • 按钮级控制:细粒度的功能权限控制
    • 安全性:token 过期处理、错误处理等

    按钮级权限控制

    通过对你原始代码的详细分析,我发现你确实已经实现了多层次的按钮级权限控制:

    🎯 1. 菜单级权限控制

    12:27:后台前端_wz/src/views/menu/index.vue

              <el-menu-item index="user" v-if="userAdmin||superAdmin">
                <el-icon><User /></el-icon>
                <span>用户模块</span>
              </el-menu-item>
              <el-menu-item index="login_log" v-if="superAdmin">
                <el-icon><Clock /></el-icon>
                <span>登录日志</span>
              </el-menu-item>
              <el-menu-item index="operation_log" v-if="superAdmin">
                <el-icon><Operation /></el-icon>
                <span>操作日志</span>
              </el-menu-item>
              <el-sub-menu index="产品管理" v-if="productAdmin||superAdmin">
                <template #title>
                  <el-icon><Goods /></el-icon>
                  <span>产品管理</span>
                </template>
    

    🔐 2. 基于职位的按钮权限控制

    A. 审核功能权限(产品审核)

    18:25:后台前端_wz/src/views/product/components/notes.vue

            <el-input
                v-model="audit_notes"
                :rows="2"
                type="textarea"
                :disabled="admin()"
            />
          </div>
    
        <template #footer v-if="!admin()">
    

    75:82:后台前端_wz/src/views/product/components/notes.vue

    const admin = () => {
      if(localStorage.getItem('position')=='经理'){
        return true
      }else{
        return false
      }
    }
    
    B. 用户管理权限(删除用户)

    在 user/components/user_info.vue 中:

    47:49:后台前端_wz/src/views/user/components/user_info.vue

        <template #footer v-if="admin()">
          <span class="delete" @click="openMessageBox">删除用户</span>
        </template>
    

    130:137:后台前端_wz/src/views/user/components/user_info.vue

    const admin = () =>{
      if(localStorage.getItem('position')=='员工'){
        return false
      }else{
        return true
      }
    }
    
    C. 产品编辑权限

    在 product/index.vue 中:

    147:151:后台前端_wz/src/views/product/index.vue

    // 编辑产品信息
    const openEdit = (row:any) =>{
      if(localStorage.getItem('position')=='员工') return
      bus.emit('editRow',row)
      editProduct.value.open()
    

    📊 3. 权限控制的层次结构

    你的系统实现了三个层次的权限控制:

    1. 部门级权限

      • 超级管理员:全部功能
      • 人事部:用户管理
      • 产品部:产品相关功能
    2. 职位级权限

      • 经理:具有审核权限、编辑权限、删除权限
      • 员工:只有基础操作权限
    3. 功能级权限

      • 审核功能:仅经理可操作
      • 编辑功能:员工被限制
      • 删除功能:员工无权限

    🎯 4. 权限控制的具体实现方式

    1. 条件渲染:使用 v-if 控制元素显示
    2. 功能禁用:使用 :disabled 控制按钮状态
    3. 函数拦截:在事件处理函数中进行权限检查
    4. LocalStorage 检查:基于用户的 department 和 position
    • ✅ 菜单权限控制(基于部门)
    • ✅ 按钮显示 / 隐藏控制(v-if)
    • ✅ 按钮禁用控制(:disabled)
    • ✅ 功能级权限控制(职位检查)
    • ✅ 操作拦截(函数内权限验证)

    Vue项目中你是如何解决跨域的呢?

    怎么对 axios 进行二次封装的,请求拦截器和响应拦截器分别做了什么

    1

    2

    3

    4

    5

    6

    7

    - 请求拦截器

        * 对 Get、Post、Put、Delete 请求进行封装

        * 请求头统一配置

        * 如果登录方案使用 token,还需要将 token 放在请求头下面的 authorization 中

    - 响应拦截器

        * 对 http 错误码进行拦截,比如说`401  403  404`等

        * 简化返回信息,什么意思呢,比如后端初始返回的结构如下:

    1

    2

    3

    4

    5

    6

    7

    8

    {

      data: {

        code: 200,

        data: {

        }

      }

    }

    而我们只需要if (data.code) === 200,就直接将data.data进行resolve

    Get 请求和 Post 请求的区别

    因为上述在 axios 封装中提到了这些请求,所以这些问题也需要详细准备一下

    1

    2

    3

    4

    - 缓存:Get 请求有缓存,Post 没有

    - 参数:Get 参数直接添加到 URL 后面,Post 请求放在请求体中

    - 幂等:Get 无论请求多少次,所得的资源都是相同的

    - 编码:Get 只能进行 URL 编码,Post 没有限制

    Get、Post、Put、Delete 请求的含义

    1

    2

    3

    4

    - Get:请求资源

    - Post:创建资源

    - Put:更新资源

    - Delete:删除资源

    HTTP 常见的状态码有哪些

    2XX

        * 200:表示请求成功,数据放在响应体中

        * 201:表示在服务器中创建了新资源。比如创建一个新的用户

        * 202:表示服务器端已经收到了请求,但是还没有处理

        * 204:表示资源请求成功,但是响应体中没有数据

    3XX

        * 301:永久重定向

        * 302:暂时重定向

        * 304:可以使用协商缓存

    4XX

        * 400:笼统的错误码,表示资源请求错误

        * 401:表示需要认证后才能访问的资源

        * 403:表示服务器直接拒绝 http 请求,一般用来针对非法请求

        * 404:未找到资源

        * 409:表示请求的资源与当前服务器的状态发生冲突

        * 429:请求过多

    5XX

        * 500:笼统的错误码,表示服务器发生错误

        * 501:表示请求的功能还未支持,类似于敬请期待

        * 502:访问后端服务器发生了错误

        * 503:表示服务器很忙

    HTTP 和 HTTPS 的区别

    https = http + 加密 + 认证 + 完整性保护

    具体来说,分为以下四点:

    1. HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传
    2. HTTP 连接建立相对简单,TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输
    3. 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443
    4. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的

    可以看出, HTTPS 相比于 HTTP,多了一层 SSL/TLS 协议,并且针对于 http 的问题,提出了相应的解决方式

    HTTP 存在问题 HTTPS 对应解决方式

    窃听风险 实现信息加密:采用混合加密的方式实现信息的机密性
    篡改风险 实现校验机制:通过摘要算法来实现完整性,它能够为数据生成独一无二的指纹从而来校验数据的完整性
    冒充风险 身份证书:通过第三方认证机构来对身份进行鉴定,即将服务器公钥放在了数字证书中,解决了冒充的风险

    HTTPS 的加密方式及其过程

    HTTPS 采用的是对称加密和非对称加密结合的混合加密的方式

    对称加密

    含义:解密和加密都使用的相同的密钥

    过程:

    1. 浏览器发送所支持的加密套件列表和一个随机数 client-random
    2. 服务器从加密套件列表中选取一个加密套件,然后生成一个随机数service-random,并将这个随机数和加密套件列表返回给浏览器
    3. 最后浏览器和服务器分别返回确认信息
    4. 然后两者混合生成密钥 master-secret,这样就可以进行数据的加密传输了

    缺陷:传输是明文的,通过可能被监听到从而可以获得密钥

    非对称加密

    含义:公钥是每个人都可以获取到的,存储在浏览器中,私钥只有服务器才知道,不对任何人公开

    • 公钥:服务器会将公钥以明文的形式发送给客户端
    • 私钥:必须由服务器管理,不可以泄露,两个密钥可以双向加解密

    过程:

    1. 浏览器发送加密套件列表给服务器
    2. 然后服务器会选择一个加密套件,不过与对称加密不同的是:非对称加密中服务器会存在用户浏览器加密的公钥和服务器解密 HTTP 数据的私钥
    3. 由于公钥是给浏览器使用的,所以服务器会将加密套件和公钥一同发送给浏览器
    4. 最后浏览器和服务器返回确认信息

    缺点

    1. 非对称加密的效率太低,处理大数据低效
    2. 无法保证服务器发送给浏览器的数据安全:虽然浏览器可以使用公钥来加密,但是服务器只能使用私钥来加密,私钥加密只有公钥能解密,但黑客也是可以获取到公钥的,这样就不能保证服务器端的数据安全

    混合加密

    含义在传输数据阶段使用对称加密,但是对称加密的密钥使用非对称加密来传输

    具体流程

    1. 首先浏览器向服务器发送对称加密套件列表,非对称加密套件列表和随机数 client-random
    2. 服务器保存随机数 client-random,向浏览器发送选择的加密套件、service-random 和公钥
    3. 浏览器保存公钥,并生成随机数 pre-master,利用公钥对 pre-master 加密,并向服务器发送加密后的数据
    4. 最后服务器拿出自己的私钥,解密出 pre-master 数据,并返回确认信息
    5. 然后服务器和浏览器会使用三组随机数生成对称密钥

    缺点: 被 DNS 劫持替换 IP 地址在自己服务器上实现公钥和私钥,将使用 ca 证书来解决

    CA 认证和摘要算法

    在上述已经说过,https = http + 加密 + 认证 + 完整性保护

    我们已经详细讲过加密这一小节了,接下来讲讲认证和完整性保护(摘要算法+数字签名)

    CA 认证

    CA 认证解决的是什么问题呢,混合加密的缺陷我们已经知道,可能被伪造假的公钥和私钥,从而造成数据泄露。

    那么这时候,我们就需要一家权威的机构来验证身份是否合法,这家权威的机构就是 CA(数字证书认证机构),将服务器公钥放在数字证书中(由数字证书认证机构颁发),只要证书是可信的,公钥就是可信的

    摘要算法 + 数字签名

    通过摘要算法+数字签名来确保数据的完整性

    简单来说,就是为了保证传输的内容不受到篡改,我们需要对内容计算出一个【指纹】,然后同内容一同传输给对方。对方收到也会先对内容计算出一个【指纹】,然后与传输过来的指纹进行对比,如果一致,则说明内容并没有被篡改。

    那么,在计算机中,会使用摘要算法(哈希算法)来计算出内容的哈希值,这个哈希值是唯一的,且无法通过哈希值推导出内容。

    通过摘要算法,我们可以确保内容无法被篡改,但是无法确保【内容+哈希值】不会被中间人所替换,因为这里缺乏对客户端收到的消息是否来自服务端的认证。

    为了解决这个问题,计算机中会采用非对称加密的方法来解决,跟之前讲的一样,非对称加密会有两个密钥:

    • 公钥:存储在客户端中,可以公开给所有人
    • 私钥:存储在服务端中,不可泄露

    这两个密钥可以双向加解密:

    • 公钥加密,私钥解密:这个目的是为了保证内容传输
    • 私钥加密,公钥解密:

    因为非对称加密是比较耗时的,所以我们一般不会使用非对称加密来加密时机传输的内容,而只是对内容的哈希值进行加密

    所以非对称加密的用途主要通过私钥加密,公钥解密的方式来确认消息的身份,我们常说的数字签名算法,就是使用的这种方式。

    私钥是由服务端保管,然后服务端会向客户端颁发对应的公钥。如果客户端收到的信息,能被公钥解密,那说明该消息是由服务器推送的

    HTTPS 建立连接的全流程

    也就是 TLS 的握手过程:

    1.客户端发起请求

    首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。

    在这一步,客户端主要向服务器发送以下信息:

    1

    2

    3

    1. 客户端支持的 TLS 协议版本,如 TLS 1.2 版本。

    2. 客户端生产的随机数(`Client Random`),后面用于生成「会话秘钥」条件之一。

    3. 客户端支持的密码套件列表,如 RSA 加密算法。

    2.服务器响应

    服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:

    1

    2

    3

    4

    1. 确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信

    2. 服务器生产的随机数(`Server Random`),也是后面用于生产「会话秘钥」条件之一。

    3. 确认的密码套件列表,如 RSA 加密算法

    4. 服务器的数字证书。

    3.验证证书

    客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。

    如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:

    1

    2

    3

    1. 一个随机数(`pre-master key`)。该随机数会被服务器公钥加密。

    2. 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。

    3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。

    上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的

    服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。

    4.服务器的最后回应

    服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。

    然后,向客户端发送最后的信息:

    1

    2

    1. 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信

    2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验

    至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容

    HTTP2.0 做了哪些改进

    简单来说有以下五点:

    多路复用:将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2 最重要的一项增强。事实上,这个机制会在整个网络技术栈中引发一系列连锁反应,从而带来巨大的性能提升

    头部压缩:利用霍夫曼编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对

    新的二进制格式:HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。 这是 HTTP/2 协议所有其他功能和性能优化的基础

    服务端推送(Server Push)

    数据流

    项目亮点

    🌟 1. 多层级权限控制系统

    这是你项目最突出的亮点之一,实现了三个维度的权限控制:

    Search files...

    权限控制系统的三个维度:

    1. 菜单级权限(部门维度)

    javascript

    // 后端:基于部门返回不同的路由配置
    const superAdminRouter = [/* 所有权限 */]
    const userAdminRouter = [/* 人事部权限 */] 
    const productAdminRouter = [/* 产品部权限 */]
    
    // 前端:动态显示菜单
    <el-menu-item index="user" v-if="userAdmin||superAdmin">
    <el-menu-item index="login_log" v-if="superAdmin">
    <el-sub-menu index="产品管理" v-if="productAdmin||superAdmin">
    
    2. 功能级权限(职位维度)

    javascript

    // 基于职位控制功能访问
    const admin = () => {
      if(localStorage.getItem('position')=='员工'){
        return false  // 员工无权限
      }else{
        return true   // 经理有权限
      }
    }
    
    // 实际应用:产品审核只有经理能操作
    <template #footer v-if="!admin()">
      <el-button type="success">通过审核</el-button>
      <el-button type="danger">拒绝审核</el-button>
    </template>
    
    3. 操作级权限(业务逻辑维度)

    javascript

    // 防止自己删除自己
    const openMessageBox = () => {
      let id = localStorage.getItem('id') as any as number
      if(id == userId.value){
        return ElMessage.error('不能删除自己!')
      }
      // 删除逻辑...
    }
    

    🚀 2. 动态路由加载系统

    Search files...
    这个动态路由系统非常巧妙,我来详细分析:

    核心实现原理:

    typescript

    // 1. 使用 import.meta.glob 批量导入组件
    function loadComponent(url: string) {
      let Module = import.meta.glob("@/views/**/*.vue")
      return Module[`/src/views/${url}.vue`]
    }
    
    // 2. 动态添加路由
    function compilerMenu(arr: any) {
      arr.forEach((item: any) => {
        let rts = {
          name: item.name,
          path: item.path, 
          component: item.component
        }
        
        if (!item.children) {
          let path = loadComponent(item.component)
          rts.component = path
          router.addRoute('menu', rts) // 关键:动态添加到menu父路由下
        }
      })
    }
    

    完整流程:

    1. 登录时:后端根据用户部门返回路由配置
    2. 存储路由:前端通过 Pinia 存储路由数据并持久化
    3. 应用启动main.ts中自动重建路由
    4. 组件加载:通过import.meta.glob实现懒加载

    🔧 3. Axios 二次封装的亮点

    Read file: 后台前端_wz/src/http/index.ts

    Axios 封装的亮点:

    typescript

    // 1. 环境变量配置 - 支持多环境部署
    baseURL: import.meta.env.VITE_API_BASEURL
    
    // 2. 业务状态码处理 - 区分HTTP状态码和业务状态码
    if(response.data.status||response.data.message){
      if(response.data.status==0){
        ElMessage.success(response.data.message) // 业务成功
      }else{
        ElMessage.error(response.data.message)   // 业务失败
      }
    }
    
    // 3. 完善的HTTP错误处理
    switch (error.response.status){
      case 400: ElMessage.error('请求错误'); break
      case 401: ElMessage.error('未授权,请登录'); break
      case 403: ElMessage.error('拒绝访问'); break
      case 404: ElMessage.error(`请求地址出错: ${error.response.config.url}`); break
      case 500: ElMessage.error('服务器内部错误'); break
      default: ElMessage.error(`连接出错:${error.response.status}`)
    }
    

    🎯 4. 操作日志系统

    Search files...

    操作日志系统的亮点:

    typescript

    // 1. 详细的操作记录 - 记录关键业务操作
    const content = `删除了用户${userAccount.value}`
    await operationLog(
      localStorage.getItem('account'),
      localStorage.getItem('name'), 
      content,
      '高级',  // 操作等级
      '成功'   // 操作状态
    )
    
    // 2. 操作等级分类
    // 高级:删除、审核等重要操作
    // 中级:编辑、修改等常规操作  
    // 低级:查看、搜索等只读操作
    
    // 3. 操作状态跟踪 - 成功/失败都记录
    if(res.status==0){
      await operationLog(/* 参数 */, '成功')
    }else{
      await operationLog(/* 参数 */, '失败')
    }
    

    🔄 5. 自定义 Hooks 复用

    Read file: 后台前端_wz/src/hooks/log.ts

    自定义 Hooks 的亮点:

    typescript

    // 1. 高度复用的表格逻辑
    const {
      tableData,
      paginationData, 
      getFirstPageList,
      changePage,
      clearLog
    } = logHooks('operation') // 传入类型即可复用
    
    // 2. 智能分发API调用
    if(logName=='operation'){
      res = await getOperationLogLength()
    }else{
      res = await getLoginLogLength()
    }
    
    // 3. 自动化的响应式管理
    watch(paginationData, () => {
      returnListLength()
      getFirstPageList()
    }, {immediate: true, deep: true})
    

    🌟 项目的综合亮点总结:

    1. 架构设计亮点

    • 前后端分离:清晰的职责划分
    • 模块化开发:Router Handler 分层设计
    • RESTful API:标准化的接口设计

    2. 安全性亮点

    • JWT 认证:7 小时有效期,安全可控
    • 密码加密:bcrypt 加密存储
    • 权限拦截:多层级权限控制
    • CORS 配置:跨域安全处理

    3. 用户体验亮点

    • 动态路由:根据权限显示菜单
    • 操作日志:完整的审计追踪
    • 错误处理:友好的错误提示
    • 加载优化:路由懒加载

    4. 代码质量亮点

    • TypeScript:类型安全
    • 组合式 API:更好的逻辑复用
    • 自定义 Hooks:抽象公共逻辑
    • 环境配置:支持多环境部署

    项目亮点深度解析:企业级后台管理系统的设计精髓

    核心架构理念与设计思路

    首先,这个后台管理系统最突出的亮点在于其多维度权限控制体系的精妙设计。基于现代企业管理的复杂需求,系统不仅仅停留在简单的角色权限划分,而是构建了一套三层递进的权限控制架构。具体而言,系统从部门级菜单权限出发,进而细化到职位级功能权限,最终深入到业务级操作权限,形成了一个立体化的权限管控网络。

    更为重要的是,这种设计理念体现了对真实企业场景的深刻理解。由于不同部门具有不同的业务职能,因此系统根据用户所属部门(人事部、产品部、超级管理员)返回相应的路由配置,确保了菜单显示的精准性。然后,在此基础上,系统进一步根据用户职位(经理vs员工)控制具体功能的访问权限,比如产品审核功能仅对经理开放,从而实现了更细粒度的权限管控。

    动态路由系统的技术创新

    紧接着,系统的动态路由加载机制展现了另一个技术亮点。与传统的静态路由配置不同,该系统采用了基于后端数据驱动的动态路由生成策略。具体来说,当用户登录成功后,后端会根据用户的部门信息返回对应的路由配置数组,然后前端通过import.meta.glob批量导入组件,并使用router.addRoute动态添加到路由表中。

    这种设计的巧妙之处在于,它不仅实现了路由的按需加载,提升了应用性能,同时也为系统的扩展性提供了强有力的支撑。换言之,当需要新增角色或调整权限时,开发者只需要修改后端的路由配置,而无需改动前端代码,大大降低了系统维护成本。

    技术架构的深度思考

    从技术实现角度来看,系统展现出了对现代前端开发最佳实践的深刻理解。首先,在状态管理方面,系统采用了Pinia配合持久化插件的方案,确保了用户权限信息在页面刷新后依然有效。其次,在网络请求层面,系统对Axios进行了精心的二次封装,不仅支持环境变量配置以适应多环境部署,还实现了业务状态码与HTTP状态码的分离处理,使得错误处理更加精准和用户友好。

    另外,系统在安全性方面的考量也颇为周全。通过JWT令牌的7小时有效期设计,既保证了用户体验的流畅性,又确保了安全风险的可控性。同时,结合bcrypt密码加密存储和CORS跨域配置,系统构建了一个相对完善的安全防护体系。

    用户体验与开发效率的平衡

    值得一提的是,系统在提升用户体验方面也做出了诸多创新。通过实现操作日志系统,系统不仅记录了用户的关键操作行为,还根据操作的重要程度进行了等级分类(高级、中级、低级),并且对操作结果(成功/失败)进行了详细追踪。这种设计不仅满足了企业内部的审计需求,也为系统问题排查提供了有力支撑。

    进一步地,系统通过自定义Hooks的方式实现了代码逻辑的高度复用。以logHooks为例,通过传入不同的日志类型参数,就能够复用整套表格分页、数据加载、清空操作的逻辑,极大地提升了开发效率。这种抽象能力的展现,体现了开发者对Vue3 Composition API的深度掌握。

    代码质量与可维护性考量

    从代码质量角度分析,系统全面采用了TypeScript,不仅提供了类型安全保障,还增强了代码的可读性和可维护性。同时,系统采用了Vue3的组合式API,相比于Options API,这种方式能够更好地组织相关逻辑,使得代码结构更加清晰。

    此外,系统在项目结构组织方面也体现了良好的工程化思维。后端采用了Router Handler的分层设计,将路由定义与业务逻辑处理分离;前端则通过统一的API管理、组件封装、样式规范等方式,确保了代码的一致性和可维护性。

    性能优化与扩展性设计

    在性能优化方面,系统通过路由懒加载、组件按需导入等技术手段,有效降低了首屏加载时间。特别是动态路由系统的实现,使得用户只会加载其有权限访问的页面组件,进一步优化了资源利用效率。

    最后,从系统的扩展性来看,无论是权限体系的调整、新功能模块的添加,还是新角色的引入,当前的架构设计都能够很好地支撑这些变更需求。这种前瞻性的设计思维,体现了开发者对企业级应用开发的深刻理解。

    综合评价与技术价值

    综合来看,这个后台管理系统不仅在功能实现上满足了企业管理的实际需求,更在技术架构、代码质量、用户体验等多个维度展现了企业级应用开发的最佳实践。通过多维度权限控制、动态路由加载、完善的日志审计、优雅的错误处理等核心特性的有机结合,系统构建了一个既安全可靠又灵活可扩展的技术解决方案,为类似项目的开发提供了极具参考价值的技术范例。


      网站公告

      今日签到

      点亮在社区的每一天
      去签到