-
Notifications
You must be signed in to change notification settings - Fork 19
/
FEBS-Vue-Document.html
1 lines (1 loc) · 196 KB
/
FEBS-Vue-Document.html
1
<!-- build time:Wed Mar 09 2022 10:17:49 GMT+0800 (GMT+08:00) --><!doctype html><html class="theme-next mist" lang="zh-Hans"><head><meta name="generator" content="Hexo 3.8.0"><meta name="google-site-verification" content="7Tau9WyVgxnsEY9oYedu9g0U6_8akOX3wiKbaYcrg9A"><meta name="baidu-site-verification" content="EVwLiaxdxX"><link href="/css/xps13.css" rel="stylesheet" type="text/css"><link href="/css/message.css" rel="stylesheet" type="text/css"><link href="//fonts.googleapis.com/css?family=Baloo+Bhaijaan|Inconsolata|Josefin+Sans|Montserrat" rel="stylesheet"><script type="text/javascript" src="/js/jquery-1.11.3.min.js"></script><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><link href="/lib/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css"><link href="/css/main.css?v=5.1.1" rel="stylesheet" type="text/css"><meta name="keywords" content="FEBS,"><link rel="alternate" href="/atom.xml" title="MrBird" type="application/atom+xml"><link rel="shortcut icon" type="image/x-icon" href="/bird.png?v=5.1.1"><meta name="description" content="FEBS-Vue为FEBS-Shiro的前后端分离版本,前端使用Vue全家桶,组件库采用Ant-Design-Vue。文档里介绍的示例是在Windows10操作系统下完成的,后端编辑器使用IDEA,前端编辑器使用WebStorm。"><meta name="keywords" content="FEBS"><meta property="og:type" content="article"><meta property="og:title" content="FEBS-Vue文档"><meta property="og:url" content="http://mrbird.cc/FEBS-Vue-Document.html"><meta property="og:site_name" content="MrBird"><meta property="og:description" content="FEBS-Vue为FEBS-Shiro的前后端分离版本,前端使用Vue全家桶,组件库采用Ant-Design-Vue。文档里介绍的示例是在Windows10操作系统下完成的,后端编辑器使用IDEA,前端编辑器使用WebStorm。"><meta property="og:locale" content="zh-Hans"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409165245.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409170853.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409171301.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409171954.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409172902.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409173934.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409184301.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409185417.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409184818.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409185112.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409185643.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409190322.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409191649.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409191833.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409204143.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409204745.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409204849.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409205834.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409210335.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409210901.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409211225.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409211912.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409212130.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409212334.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409213345.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409223016.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_094404.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409220000.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409225225.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409223442.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_094842.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/asdfasdfasd.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_100815.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_101222.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_095141.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_095533.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_101510.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_101826.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-10_104514.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/backend-xmind.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-08_113758.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190408141755.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-08_145115.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190408145646.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190408153154.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/redis-zset.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190408154805.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/2019-04-08_185510.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409101831.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409133947.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409134916.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409135425.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409135740.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409141220.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/asdfasdfasfdasdf.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409142251.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409142642.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409143223.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409143905.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409143943.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409144206.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409144908.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/20190409150427.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409152413.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409152550.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409153002.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409153227.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409153710.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409153800.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409161742.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409161947.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409162214.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ截图20190409162930.png"><meta property="og:image" content="http://mrbird.cc/img/febsvue/QQ图片20190409164403.jpg"><meta property="og:updated_time" content="2020-01-21T03:30:03.126Z"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="FEBS-Vue文档"><meta name="twitter:description" content="FEBS-Vue为FEBS-Shiro的前后端分离版本,前端使用Vue全家桶,组件库采用Ant-Design-Vue。文档里介绍的示例是在Windows10操作系统下完成的,后端编辑器使用IDEA,前端编辑器使用WebStorm。"><meta name="twitter:image" content="http://mrbird.cc/img/febsvue/20190409165245.png"><script type="text/javascript" id="hexo.configurations">var NexT=window.NexT||{},CONFIG={root:"/",scheme:"Mist",sidebar:{position:"left",display:"always",offset:12,offset_float:0,b2t:!1,scrollpercent:!0},fancybox:!1,motion:!1}</script><title>FEBS-Vue文档 | MrBird</title></head><body ondragstart="return!1" class="animsition" lang="zh-Hans" style="overflow-x:hidden;padding-left:280px"><script type="text/javascript" src="/js/mo.min.js"></script><style>body{text-rendering:geometricPrecision!important;font-family:'Josefin Sans','PingFang SC'!important;-webkit-font-smoothing:antialiased!important;-webkit-text-size-adjust:100%!important;background-color:#f4f6f7}@media (min-width:768px) and (max-width:991px){body .header-innerr{width:700px!important}}.header-innerr{margin:0 auto;padding:100px 0 70px;width:880px}@media (min-width:1600px){.container .header-innerr{width:996px}}.header-innerr{position:relative}.header-innerr{padding:0}.header-innerr:after,.header-innerr:before{content:" ";display:table}.header-innerr:after{clear:both}@media (max-width:767px){.header-innerr{width:auto;padding:10px;margin-bottom:-20px}}</style><div class="container sidebar-position-left page-post-detail"><div class="headband"></div><header id="header" class="header"><div class="header-inner"><div class="site-brand-wrapper"><div class="site-meta"><link href="https://fonts.font.im/css?family=Merienda" rel="stylesheet"><div class="custom-logo-site-title"></div><h1 class="site-subtitle" itemprop="description"></h1></div><div class="site-nav-toggle"><button><span class="btn-bar"></span> <span class="btn-bar"></span> <span class="btn-bar"></span></button></div></div><nav class="site-nav"><div class="site-search"><div class="popup search-popup local-search-popup"><div class="local-search-header clearfix"><span class="search-icon"><i class="fa fa-search"></i> </span><span class="popup-btn-close"><i class="fa fa-times-circle"></i></span><div class="local-search-input-wrapper"><input autocomplete="off" placeholder="Search" spellcheck="false" type="text" id="local-search-input"></div></div><div id="local-search-result"></div></div></div></nav></div><div class="header-innerr"></div></header><main id="main" class="main"><div class="main-inner"><div class="content-wrap"><div id="content" class="content"><div id="posts" class="posts-expand"><article class="post post-type-normal" itemscope itemtype="http://schema.org/Article"><link itemprop="mainEntityOfPage" href="http://mrbird.cc/FEBS-Vue-Document.html"><span hidden itemprop="author" itemscope itemtype="http://schema.org/Person"><meta itemprop="name" content="MrBird"><meta itemprop="description" content=""><meta itemprop="image" content="/images/blogImage.jpg"></span><span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization"><meta itemprop="name" content="MrBird"></span><header class="post-header"><h2 class="post-title" itemprop="name headline">FEBS-Vue文档</h2><div class="post-meta"><span class="post-time"><span class="post-meta-item-icon"><i class="fa fa-calendar-o"></i> </span><span class="post-meta-item-text"></span> <time title="创建于" itemprop="dateCreated datePublished" datetime="2019-01-01T09:24:51+08:00">2019-01-01 </time></span><span></span> <span class="post-meta-divider">|</span> <span class="page-pv"><i class="fa fa-laptop"></i> Visit count <span class="busuanzi-value" id="busuanzi_value_page_pv"></span></span></div></header><div class="post-body" itemprop="articleBody"><p>FEBS-Vue为<a href="https://github.com/wuyouzhuguli/FEBS-Shiro" target="_blank" rel="noopener">FEBS-Shiro</a>的前后端分离版本,前端使用Vue全家桶,组件库采用<a href="https://vuecomponent.github.io/ant-design-vue/docs/vue/introduce-cn/" target="_blank" rel="noopener">Ant-Design-Vue</a>。</p><p>文档里介绍的示例是在Windows10操作系统下完成的,后端编辑器使用IDEA,前端编辑器使用WebStorm。<a id="more"></a></p><h2 id="项目导入"><a href="#项目导入" class="headerlink" title="项目导入"></a>项目导入</h2><p>为了方便,我直接在桌面上通过git bash克隆项目:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/wuyouzhuguli/FEBS-Vue.git</span><br></pre></td></tr></table></figure><p></p><p>克隆后,桌面上多出一个FEBS-Vue文件夹:</p><p><img src="img/febsvue/20190409165245.png" alt="QQ截图20190409165245.png"></p><p>backend为后端项目源码,frontend为前端项目源码,sql为数据库初始化脚本。</p><h3 id="JDK"><a href="#JDK" class="headerlink" title="JDK"></a>JDK</h3><p>因为项目用到了JDK 8的一些特性,所以JDK最低版本不能低于8。</p><p>JDK 8官方下载地址:<a href="https://www.oracle.com/technetwork/java/javase/downloads。" target="_blank" rel="noopener">https://www.oracle.com/technetwork/java/javase/downloads。</a></p><h3 id="安装Node-js"><a href="#安装Node-js" class="headerlink" title="安装Node.js"></a>安装Node.js</h3><p>Node.js下载地址:<a href="http://nodejs.cn/download/" target="_blank" rel="noopener">http://nodejs.cn/download/</a>,直接安装即可,安装后查看其版本:</p><p><img src="img/febsvue/QQ截图20190409170853.png" alt="QQ截图20190409170853.png"></p><p>Node.js集成了npm,所以安装好Node.js后npm就可以使用了:</p><p><img src="img/febsvue/QQ截图20190409171301.png" alt="QQ截图20190409171301.png"></p><h3 id="安装yarn"><a href="#安装yarn" class="headerlink" title="安装yarn"></a>安装yarn</h3><p>在CMD中执行<code>npm install -g yarn</code>:</p><p><img src="img/febsvue/QQ截图20190409171954.png" alt="QQ截图20190409171954.png"></p><p>因为我之前已经安装过了,所以这里就相当于更新操作了。</p><h3 id="安装Redis"><a href="#安装Redis" class="headerlink" title="安装Redis"></a>安装Redis</h3><p>项目缓存数据库使用的是Redis,所以在导入项目前需先安装Redis。</p><p>Redis Windows版本下载地址:<a href="https://github.com/MicrosoftArchive/redis/releases" target="_blank" rel="noopener">https://github.com/MicrosoftArchive/redis/releases</a>。直接下载zip版本解压到任意目录即可。</p><p>下载后,使用cmd命令切换到Redis根目录,然后运行<code>redis-server.exe redis.windows.conf</code>启动即可:</p><p><img src="img/febsvue/QQ截图20190409172902.png" alt="QQ截图20190409172902.png"></p><h3 id="安装MySQL"><a href="#安装MySQL" class="headerlink" title="安装MySQL"></a>安装MySQL</h3><p>项目数据库采用MySQL社区版,版本为5.7.x。</p><p>下载地址:<a href="https://dev.mysql.com/downloads/windows/installer/5.7.html" target="_blank" rel="noopener">https://dev.mysql.com/downloads/windows/installer/5.7.html</a></p><h3 id="导入SQL"><a href="#导入SQL" class="headerlink" title="导入SQL"></a>导入SQL</h3><p>使用Navicat新建一个数据库:</p><p><img src="img/febsvue/QQ截图20190409173934.png" alt="QQ截图20190409173934.png"></p><p>然后导入SQL脚本即可。</p><h3 id="导入后端项目"><a href="#导入后端项目" class="headerlink" title="导入后端项目"></a>导入后端项目</h3><p>IDEA选择backend: <img src="img/febsvue/QQ截图20190409184301.png" alt="QQ截图20190409184301.png"></p><p>导入项目后安装lombok插件(不懂lombok可以自行百度):</p><p><img src="img/febsvue/QQ截图20190409185417.png" alt="QQ截图20190409185417.png"></p><p>安装完重启IDEA才能生效。</p><p>接着修改application.yml中的数据库和Redis配置,修改完后通过Spring Boot入口类FebsApplication启动即可:</p><p><img src="img/febsvue/QQ截图20190409184818.png" alt="QQ截图20190409184818.png"></p><p><img src="img/febsvue/QQ截图20190409185112.png" alt="QQ截图20190409185112.png"></p><p>接着开始导入前端项目。</p><h3 id="导入前端项目"><a href="#导入前端项目" class="headerlink" title="导入前端项目"></a>导入前端项目</h3><p>使用WebStorm打开frontend:</p><p><img src="img/febsvue/QQ截图20190409185643.png" alt="QQ截图20190409185643.png"></p><p>在终端输入<code>yarn install</code>命令安装依赖:</p><p><img src="img/febsvue/QQ截图20190409190322.png" alt="QQ截图20190409190322.png"></p><p>稍等片刻,坐与放宽。</p><p>依赖下载完毕后,输入yarn start启动前端项目:</p><p><img src="img/febsvue/QQ截图20190409191649.png" alt="QQ截图20190409191649.png"></p><p>浏览器访问<a href="http://localhost:8081" target="_blank" rel="noopener">http://localhost:8081</a>:</p><p><img src="img/febsvue/QQ截图20190409191833.png" alt="QQ截图20190409191833.png"></p><h2 id="项目部署"><a href="#项目部署" class="headerlink" title="项目部署"></a>项目部署</h2><p>下面演示如何在Linux上部署项目(例子采用CentOS7)。</p><h3 id="Vagrant创建CentOS"><a href="#Vagrant创建CentOS" class="headerlink" title="Vagrant创建CentOS"></a>Vagrant创建CentOS</h3><p>如果没有CentOS7环境可以使用Vagrant快速构建一个CentOS虚拟机,具体可以参考:<a href="https://mrbird.cc/Create-Virtual-Machine-By-Vagrant.html">https://mrbird.cc/Create-Virtual-Machine-By-Vagrant.html</a>。我的CentOS虚拟机IP为:192.168.33.11。</p><div class="note danger"><p>使用命令<code>timedatectl set-timezone Asia/Shanghai</code>设置CentOS的时区,以避免因时区带来的BUG。</p></div><h3 id="Java环境配置"><a href="#Java环境配置" class="headerlink" title="Java环境配置"></a>Java环境配置</h3><ol><li>下载JDK8:</li></ol><p><img src="img/febsvue/QQ截图20190409204143.png" alt="QQ截图20190409204143.png"></p><p>下载后通过Vagrant共享到CentOS上(我的Vagrantfile共享配置为<code>config.vm.synced_folder "./sync", "/vagrant", create:true, owner: "root", group: "root"</code>):</p><p><img src="img/febsvue/QQ截图20190409204745.png" alt="QQ截图20190409204745.png"></p><ol start="2"><li>安装JDK8:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rpm -ivh jdk-8u201-linux-x64.rpm</span><br></pre></td></tr></table></figure><p><img src="img/febsvue/QQ截图20190409204849.png" alt="QQ截图20190409204849.png"></p><ol start="3"><li>配置环境变量</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /etc/profile</span><br></pre></td></tr></table></figure><p>输入以下内容:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">JAVA_HOME=/usr/java/jdk1.8.0_201</span><br><span class="line">JRE_HOME=/usr/java/jdk1.8.0_201/jre</span><br><span class="line">PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$JRE_HOME</span>/bin</span><br><span class="line">CLASSPATH=.:<span class="variable">$JAVA_HOME</span>/lib/dt.jar:<span class="variable">$JAVA_HOME</span>/lib/tools.jar:<span class="variable">$JRE_HOME</span>/lib</span><br><span class="line"><span class="built_in">export</span> JAVA_HOME JRE_HOME PATH CLASSPATH</span><br></pre></td></tr></table></figure><p>然后执行以下命令生效:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /etc/profile</span><br></pre></td></tr></table></figure><p></p><h3 id="安装Docker"><a href="#安装Docker" class="headerlink" title="安装Docker"></a>安装Docker</h3><p>官方安装教程:<a href="https://docs.docker.com/install/linux/docker-ce/centos/" target="_blank" rel="noopener">https://docs.docker.com/install/linux/docker-ce/centos/</a></p><p>安装好后:</p><p><img src="img/febsvue/QQ截图20190409205834.png" alt="QQ截图20190409205834.png"></p><h3 id="Docker安装MySQL"><a href="#Docker安装MySQL" class="headerlink" title="Docker安装MySQL"></a>Docker安装MySQL</h3><ol><li>拉取MySQL镜像:</li></ol><p><img src="img/febsvue/QQ截图20190409210335.png" alt="QQ截图20190409210335.png"></p><ol start="2"><li>创建目录/home/febs/mysql,用于挂载MySQL volume:</li></ol><p><img src="img/febsvue/QQ截图20190409210901.png" alt="QQ截图20190409210901.png"></p><ol start="3"><li>创建MySQL容器:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --name mysql -p 3306:3306 \</span><br><span class="line"> -v $(<span class="built_in">pwd</span>):/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7.25</span><br></pre></td></tr></table></figure><p><img src="img/febsvue/QQ截图20190409211225.png" alt="QQ截图20190409211225.png"></p><ol start="4"><li>使用Navicat连接MySQL,创建数据库并导入数据:</li></ol><p>连接:</p><p><img src="img/febsvue/QQ截图20190409211912.png" alt="QQ截图20190409211912.png"></p><p>新增数据库:</p><p><img src="img/febsvue/QQ截图20190409212130.png" alt="QQ截图20190409212130.png"></p><p>导入SQL:</p><p><img src="img/febsvue/QQ截图20190409212334.png" alt="QQ截图20190409212334.png"></p><h3 id="Docker安装Redis"><a href="#Docker安装Redis" class="headerlink" title="Docker安装Redis"></a>Docker安装Redis</h3><ol><li>拉取Redis镜像:</li></ol><p><img src="img/febsvue/QQ截图20190409213345.png" alt="QQ截图20190409213345.png"></p><ol start="2"><li>创建文件/home/febs/redis/conf/redis.conf,用于挂载Redis配置文件:</li></ol><p><img src="img/febsvue/QQ截图20190409223016.png" alt="QQ截图20190409213604.png"></p><ol start="3"><li>创建Redis容器:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -p 6379:6379 \</span><br><span class="line"> -v /home/febs/redis/conf/redis.conf:/usr/<span class="built_in">local</span>/etc/redis/redis.conf \</span><br><span class="line"> --name redis redis:4.0.14</span><br></pre></td></tr></table></figure><p>测试连接:</p><p><img src="img/febsvue/2019-04-10_094404.png" alt="QQ截图20190410094147.png"></p><h3 id="Docker安装Nginx"><a href="#Docker安装Nginx" class="headerlink" title="Docker安装Nginx"></a>Docker安装Nginx</h3><ol><li>拉取Nginx镜像:</li></ol><p><img src="img/febsvue/QQ截图20190409220000.png" alt="QQ截图20190409220000.png"></p><ol start="2"><li>创建目录/home/febs/nginx/html、/home/febs/nginx/logs和文件/home/febs/nginx/conf/nginx.conf,分别用于挂载Nginx html,logs和配置文件:</li></ol><p><img src="img/febsvue/20190409225225.png" alt="QQ截图20190409225225.png"> <img src="img/febsvue/20190409223442.png" alt="QQ截图20190409220212.png"></p><ol start="3"><li>修改Nginx配置:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /home/febs/nginx/conf/nginx.conf</span><br></pre></td></tr></table></figure><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">worker_processes</span> auto;</span><br><span class="line"></span><br><span class="line"><span class="attribute">error_log</span> /var/log/nginx/error.log;</span><br><span class="line"><span class="attribute">pid</span> /run/nginx.pid;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> {</span><br><span class="line"> <span class="attribute">worker_connections</span> <span class="number">1024</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="section">http</span> {</span><br><span class="line"> <span class="attribute">include</span> mime.types;</span><br><span class="line"> <span class="attribute">default_type</span> application/octet-stream;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">server_names_hash_bucket_size</span> <span class="number">512</span>;</span><br><span class="line"> <span class="attribute">client_header_buffer_size</span> <span class="number">32k</span>;</span><br><span class="line"> <span class="attribute">large_client_header_buffers</span> <span class="number">4</span> <span class="number">32k</span>;</span><br><span class="line"> <span class="attribute">client_max_body_size</span> <span class="number">50m</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">sendfile</span> <span class="literal">on</span>;</span><br><span class="line"> <span class="attribute">tcp_nopush</span> <span class="literal">on</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">keepalive_timeout</span> <span class="number">60</span>;</span><br><span class="line"> <span class="attribute">tcp_nodelay</span> <span class="literal">on</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">fastcgi_connect_timeout</span> <span class="number">300</span>;</span><br><span class="line"> <span class="attribute">fastcgi_send_timeout</span> <span class="number">300</span>;</span><br><span class="line"> <span class="attribute">fastcgi_read_timeout</span> <span class="number">300</span>;</span><br><span class="line"> <span class="attribute">fastcgi_buffer_size</span> <span class="number">64k</span>;</span><br><span class="line"> <span class="attribute">fastcgi_buffers</span> <span class="number">4</span> <span class="number">64k</span>;</span><br><span class="line"> <span class="attribute">fastcgi_busy_buffers_size</span> <span class="number">128k</span>;</span><br><span class="line"> <span class="attribute">fastcgi_temp_file_write_size</span> <span class="number">256k</span>;</span><br><span class="line"> <span class="attribute">fastcgi_intercept_errors</span> <span class="literal">on</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"> <span class="attribute">gzip_min_length</span> <span class="number">1k</span>;</span><br><span class="line"> <span class="attribute">gzip_buffers</span> <span class="number">16</span> <span class="number">8k</span>;</span><br><span class="line"> <span class="attribute">gzip_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"> <span class="attribute">gzip_comp_level</span> <span class="number">6</span>;</span><br><span class="line"> <span class="attribute">gzip_types</span> text/plain application/javascript application/x-javascript text/javascript text/css application/xml;</span><br><span class="line"> <span class="attribute">gzip_vary</span> <span class="literal">on</span>;</span><br><span class="line"> <span class="attribute">gzip_proxied</span> expired <span class="literal">no</span>-cache <span class="literal">no</span>-store private auth;</span><br><span class="line"> <span class="attribute">gzip_disable</span> <span class="string">"MSIE [1-6]\."</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">limit_conn_zone</span> <span class="variable">$binary_remote_addr</span> zone=perip:<span class="number">10m</span>;</span><br><span class="line"> <span class="attribute">limit_conn_zone</span> <span class="variable">$server_name</span> zone=perserver:<span class="number">10m</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">server_tokens</span> <span class="literal">off</span>;</span><br><span class="line"> <span class="attribute">access_log</span> <span class="literal">off</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="section">server</span> {</span><br><span class="line"> <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line"> <span class="attribute">server_name</span> localhost;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">charset</span> utf-<span class="number">8</span>;</span><br><span class="line"></span><br><span class="line"> <span class="attribute">location</span> / {</span><br><span class="line"> <span class="attribute">root</span> html;</span><br><span class="line"> <span class="attribute">index</span> index.html index.htm;</span><br><span class="line"> }</span><br><span class="line"> <span class="attribute">location</span> = /50x.html {</span><br><span class="line"> <span class="attribute">root</span> html;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="4"><li>创建Nginx容器:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run --name nginx -d -p 80:80 \</span><br><span class="line">-v /home/febs/nginx/conf/nginx.conf:/etc/ng inx/nginx.conf \</span><br><span class="line">-v /home/febs/nginx/html:/etc/nginx/html \</span><br><span class="line">-v /home/febs/nginx/logs:/var/<span class="built_in">log</span>/nginx nginx:1.14.2</span><br></pre></td></tr></table></figure><h3 id="后端部署"><a href="#后端部署" class="headerlink" title="后端部署"></a>后端部署</h3><p>修改application.yml中数据库和redis的连接配置,然后将项目打包成jar文件:</p><p><img src="img/febsvue/2019-04-10_094842.png" alt="2019-04-10_094842.png"></p><p>将其上传到CentOS虚拟机的/home/febs/backend目录下:</p><p><img src="img/febsvue/asdfasdfasd.png" alt="2019-04-10_100008.png"></p><p>编写一个启动项目的shell脚本:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim start.sh</span><br></pre></td></tr></table></figure><p></p><p>内容:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup java -jar febs_shiro_jwt-1.0.0-release.jar &</span><br></pre></td></tr></table></figure><p></p><p>编写一个关停项目的shell脚本:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim stop.sh</span><br></pre></td></tr></table></figure><p></p><p>内容:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">PID=`ps -ef | grep febs_shiro_jwt-1.0.0-release.jar | grep -v grep | awk '{print $2}'`</span><br><span class="line">if [ -z "$PID" ]</span><br><span class="line">then</span><br><span class="line"> echo Application is already stopped</span><br><span class="line">else</span><br><span class="line"> echo kill $PID</span><br><span class="line"> kill -9 $PID</span><br><span class="line">fi</span><br></pre></td></tr></table></figure><p></p><p>授权,让其可执行:</p><p><img src="img/febsvue/2019-04-10_100815.png" alt="2019-04-10_100815.png"></p><p>启动项目:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">./start.sh</span><br><span class="line"></span><br><span class="line">tail -f nohup.out</span><br></pre></td></tr></table></figure><p></p><p>看到如下输出的时候说明后端项目启动成功:</p><p><img src="img/febsvue/2019-04-10_101222.png" alt="2019-04-10_101222.png"></p><h3 id="前端部署"><a href="#前端部署" class="headerlink" title="前端部署"></a>前端部署</h3><p>点击build:</p><p><img src="img/febsvue/2019-04-10_095141.png" alt="2019-04-10_095141.png"></p><p>build成功后,项目目录下会多出个dist文件夹:</p><p><img src="img/febsvue/2019-04-10_095533.png" alt="2019-04-10_095533.png"></p><p>将这个目录下的文件上传到CentOS虚拟机的/home/febs/nginx/html目录下:</p><p><img src="img/febsvue/2019-04-10_101510.png" alt="2019-04-10_101510.png"></p><p>浏览器访问<a href="http://192.168.33.11/#/login" target="_blank" rel="noopener">http://192.168.33.11/#/login</a>:</p><p><img src="img/febsvue/2019-04-10_101826.png" alt="2019-04-10_101826.png"></p><p><img src="img/febsvue/2019-04-10_104514.png" alt="2019-04-10_104514.png"></p><p>部署成功。</p><h2 id="后端项目介绍"><a href="#后端项目介绍" class="headerlink" title="后端项目介绍"></a>后端项目介绍</h2><h3 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a>项目结构</h3><p>后端项目目录含义如下图所示:</p><p><img src="img/febsvue/backend-xmind.png" alt="backend-xmind.png"></p><p>Common模块文件含义如下图所示:</p><p><img src="img/febsvue/2019-04-08_113758.png" alt="2019-04-08_113758.png"></p><p>其他模块文件较为简单,略。</p><h3 id="项目配置"><a href="#项目配置" class="headerlink" title="项目配置"></a>项目配置</h3><p>application.yml中除了各个插件的配置外,下面这段配置为系统配置:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">febs:</span></span><br><span class="line"><span class="attr"> openAopLog:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr"> max:</span></span><br><span class="line"><span class="attr"> batch:</span></span><br><span class="line"><span class="attr"> insert:</span></span><br><span class="line"><span class="attr"> num:</span> <span class="number">1000</span></span><br><span class="line"><span class="attr"> shiro:</span></span><br><span class="line"><span class="attr"> anonUrl:</span> <span class="string">/login,/logout/**,/regist,/user/check/**</span></span><br><span class="line"><span class="attr"> jwtTimeOut:</span> <span class="number">3600</span></span><br></pre></td></tr></table></figure><p></p><ul><li><p><code>febs.opAopLog</code>:Boolean类型,取值true或者false,为true时表示开启Aop记录用户的操作日志,需和<code>@Log</code>注解搭配使用。</p></li><li><p><code>febs.max.batch.insert.num</code>:大于0的Integer类型,表示Excel导入数据当次最大入库数据量。比如配置为1000时表示入库数据为0 - 1000 时只会执行一次数据库commit操作。</p></li><li><p><code>febs.shiro.anonUrl</code>:逗号分隔的字符串,表示无需认证的资源路径。</p></li><li><p><code>febs.shiro.jwtTimeOut</code>:定义token的有效时间,单位为秒,比如配置为3600表示token一个小时内有效,超过一个小时后需要重新认证。</p></li></ul><h3 id="RESTful风格"><a href="#RESTful风格" class="headerlink" title="RESTful风格"></a>RESTful风格</h3><p>系统Controller暴露的接口风格为RESTful,通过HTTP请求method对应增删改查类型,响应以HTTP Code为判断依据。</p><p>以UserController为例子:</p><table><tr><th>描述</th><th>请求URI</th><th>HTTP Method</th><th>对应注解</th></tr><tr><td>查询所有用户</td><td>/user</td><td>GET</td><td>@GetMapping</td></tr><tr><td>通过用户名查找用户</td><td>/user/{username}</td><td>GET</td><td>@GetMapping</td></tr><tr><td>新增用户</td><td>/user</td><td>POST</td><td>@PostMapping</td></tr><tr><td>修改用户</td><td>/user</td><td>PUT</td><td>@PutMapping</td></tr><tr><td>删除用户</td><td>/user/{userIds}</td><td>DELETE</td><td>@DeleteMapping</td></tr></table><p>Controller方法默认返回200状态码,当Controller抛出异常时,将被<code>GlobalExceptionHandler</code>捕获,根据异常类型,返回不同的HTTP状态码:</p><table><tr><th>异常类型</th><th>异常描述</th><th>状态码</th><th>对应常量</th></tr><tr><td>UnauthorizedException</td><td>未授权异常,权限不足异常</td><td>403</td><td>HttpStatus.FORBIDDEN</td></tr><tr><td>LimitAccessException</td><td>限制访问异常,访问接口频率超限</td><td>429</td><td>HttpStatus.TOO_MANY_REQUESTS</td></tr><tr><td>ConstraintViolationException</td><td>参数校验异常(普通传参)</td><td>400</td><td>HttpStatus.BAD_REQUEST</td></tr><tr><td>BindException</td><td>参数校验异常(实体对象传参)</td><td>400</td><td>HttpStatus.BAD_REQUEST</td></tr><tr><td>FebsException</td><td>Febs系统异常</td><td>500</td><td>HttpStatus.INTERNAL_SERVER_ERROR</td></tr><tr><td>Exception</td><td>剩下的别的异常</td><td>500</td><td>HttpStatus.INTERNAL_SERVER_ERROR</td></tr></table><h3 id="数据层介绍"><a href="#数据层介绍" class="headerlink" title="数据层介绍"></a>数据层介绍</h3><p>首先看看表结构,数据表分为两大类:定时任务表和系统表。</p><p><img src="img/febsvue/20190408141755.png" alt="QQ截图20190408141755.png"></p><p>以qrtz_开头的为定时任务表,定时任务有基于内存和基于数据库的,本项目使用的是基于数据库持久化的方案。要详细了解这些表可以参考文章:<a href="http://www.ibloger.net/article/2650.html" target="_blank" rel="noopener">http://www.ibloger.net/article/2650.html</a>。</p><p>以t_开头的为系统表,他们的关系如下所示:</p><p><img src="img/febsvue/2019-04-08_145115.png" alt="2019-04-08_145115.png"></p><p>其中用户,角色和权限之间的关系使用的是经典的RBAC(Role-Based Access Control,基于角色的访问控制)模型。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。如下图所示:</p><p><img src="img/febsvue/QQ截图20190408145646.png" alt="QQ截图20190408145646.png"></p><p>比如获取用户名为mrbrid的用户权限过程为:</p><ol><li><p>通过mrbrid的user_id从t_user_role表获取对应的role_id;</p></li><li><p>通过第1步获取的role_id从t_role_menu表获取对应的menu_id;</p></li><li><p>通过第2步获取的menu_id从t_menu获取menu相关信息(t_menu表的permission为权限信息)。</p></li></ol><p>数据层框架采用的是<a href="https://mp.baomidou.com/guide/" target="_blank" rel="noopener">MybatisPlus</a>,具体可以参考其官方文档。</p><h3 id="登录逻辑"><a href="#登录逻辑" class="headerlink" title="登录逻辑"></a>登录逻辑</h3><p>登录逻辑如下图所示:</p><p><img src="img/febsvue/QQ截图20190408153154.png" alt="QQ截图20190408153154.png"></p><p>这里详细说明下登录成功后的第2步、第3步和第4步过程:</p><ul><li><p>登录成功后,构建一个ActiveUser对象,对应<code>LoginController</code>的<code>saveTokenToRedis</code>方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 构建在线用户</span></span><br><span class="line">ActiveUser activeUser = <span class="keyword">new</span> ActiveUser();</span><br><span class="line">activeUser.setUsername(user.getUsername());</span><br><span class="line">activeUser.setIp(ip);</span><br><span class="line">activeUser.setToken(token.getToken());</span><br><span class="line">activeUser.setLoginAddress(AddressUtil.getCityInfo(DbSearcher.BTREE_ALGORITHM, ip));</span><br><span class="line"></span><br><span class="line"><span class="comment">// zset 存储登录用户,score 为过期时间戳</span></span><br><span class="line"><span class="keyword">this</span>.redisService.zadd(FebsConstant.ACTIVE_USERS_ZSET_PREFIX, Double.valueOf(token.getExipreAt()), mapper.writeValueAsString(activeUser));</span><br></pre></td></tr></table></figure><p>然后将<code>activeUser</code>通过<code>ObjectMapper</code>序列化,存储到Redis的Zset结构中,key为<code>FebsConstant.ACTIVE_USERS_ZSET_PREFIX</code>(即febs.user.active),score为该用户的登录过期时间点(即Tokean失效时间),value为<code>activeUser</code>序列化值。</p><div class="note info"><p>Zset它在set的基础上增加了一个顺序属性(score),这一属性在添加修改元素时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。可以理解为有两列字段的数据表,一列存value,一列存顺序编号。</p></div><p>Zset相关Redis命令:</p><p><img src="img/febsvue/redis-zset.png" alt="redis-zset.png"></p><p>比如当用户mrbird和scott登录成功后,查看Redis中key为febs.user.active的值:</p><p><img src="img/febsvue/QQ截图20190408154805.png" alt="QQ截图20190408154805.png"></p></li><li><p>将Token存储到Redis中:key为febs.cache.token.token值.IP地址,value为token值,有效期为token的有效时长,对应的源码为:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.redisService.set(FebsConstant.TOKEN_CACHE_PREFIX + token.getToken() + StringPool.DOT + ip, token.getToken(), properties.getShiro().getJwtTimeOut() * <span class="number">1000</span>);</span><br></pre></td></tr></table></figure></li><li><p>返回前端数据包括:</p><p>1.token:token;</p><p>2.exipreTime:token过期时间;</p><p>3.roles:用户角色;</p><p>4.permissions:用户权限;</p><p>5.config:用户前端系统的个性化配置;</p><p>6.user:用户信息(不包括密码)。</p><p>比如,当scott登录成功后,接口返回数据如下所示:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"data"</span>: {</span><br><span class="line"> <span class="attr">"permissions"</span>: [</span><br><span class="line"> <span class="string">"user:view"</span>,</span><br><span class="line"> <span class="string">"dept:add"</span>,</span><br><span class="line"> <span class="string">"job:export"</span>,</span><br><span class="line"> <span class="string">"role:add"</span>,</span><br><span class="line"> <span class="string">"weather:view"</span>,</span><br><span class="line"> <span class="string">"dict:add"</span>,</span><br><span class="line"> <span class="string">"role:export"</span>,</span><br><span class="line"> <span class="string">"menu:export"</span>,</span><br><span class="line"> <span class="string">"dict:view"</span>,</span><br><span class="line"> <span class="string">"dept:export"</span>,</span><br><span class="line"> <span class="string">"menu:view"</span>,</span><br><span class="line"> <span class="string">"role:view"</span>,</span><br><span class="line"> <span class="string">"user:export"</span>,</span><br><span class="line"> <span class="string">"job:add"</span>,</span><br><span class="line"> <span class="string">"dept:view"</span>,</span><br><span class="line"> <span class="string">"article:view"</span>,</span><br><span class="line"> <span class="string">"log:view"</span>,</span><br><span class="line"> <span class="string">"jobLog:view"</span>,</span><br><span class="line"> <span class="string">"job:view"</span>,</span><br><span class="line"> <span class="string">"menu:add"</span>,</span><br><span class="line"> <span class="string">"redis:view"</span>,</span><br><span class="line"> <span class="string">"log:export"</span>,</span><br><span class="line"> <span class="string">"movie:coming"</span>,</span><br><span class="line"> <span class="string">"movie:hot"</span>,</span><br><span class="line"> <span class="string">"dict:export"</span>,</span><br><span class="line"> <span class="string">"jobLog:export"</span>,</span><br><span class="line"> <span class="string">"user:online"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"roles"</span>: [</span><br><span class="line"> <span class="string">"注册用户"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"exipreTime"</span>: <span class="string">"20190408164521"</span>,</span><br><span class="line"> <span class="attr">"config"</span>: {</span><br><span class="line"> <span class="attr">"userId"</span>: <span class="number">2</span>,</span><br><span class="line"> <span class="attr">"theme"</span>: <span class="string">"light"</span>,</span><br><span class="line"> <span class="attr">"layout"</span>: <span class="string">"side"</span>,</span><br><span class="line"> <span class="attr">"multiPage"</span>: <span class="string">"0"</span>,</span><br><span class="line"> <span class="attr">"fixSiderbar"</span>: <span class="string">"1"</span>,</span><br><span class="line"> <span class="attr">"fixHeader"</span>: <span class="string">"1"</span>,</span><br><span class="line"> <span class="attr">"color"</span>: <span class="string">"rgb(24, 144, 255)"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"user"</span>: {</span><br><span class="line"> <span class="attr">"userId"</span>: <span class="number">2</span>,</span><br><span class="line"> <span class="attr">"username"</span>: <span class="string">"scott"</span>,</span><br><span class="line"> <span class="attr">"password"</span>: <span class="string">"it's a secret"</span>,</span><br><span class="line"> <span class="attr">"deptId"</span>: <span class="number">6</span>,</span><br><span class="line"> <span class="attr">"deptName"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"email"</span>: <span class="string">"scott@qq.com"</span>,</span><br><span class="line"> <span class="attr">"mobile"</span>: <span class="string">"15134627380"</span>,</span><br><span class="line"> <span class="attr">"status"</span>: <span class="string">"1"</span>,</span><br><span class="line"> <span class="attr">"createTime"</span>: <span class="string">"2017-12-30 00:16:39"</span>,</span><br><span class="line"> <span class="attr">"modifyTime"</span>: <span class="string">"2019-01-18 08:59:09"</span>,</span><br><span class="line"> <span class="attr">"lastLoginTime"</span>: <span class="string">"2019-01-23 15:34:28"</span>,</span><br><span class="line"> <span class="attr">"ssex"</span>: <span class="string">"0"</span>,</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"我是scott,嗯嗯"</span>,</span><br><span class="line"> <span class="attr">"avatar"</span>: <span class="string">"gaOngJwsRYRaVAuXXcmB.png"</span>,</span><br><span class="line"> <span class="attr">"roleId"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"roleName"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"sortField"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"sortOrder"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"createTimeFrom"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"createTimeTo"</span>: <span class="literal">null</span>,</span><br><span class="line"> <span class="attr">"id"</span>: <span class="string">"YBeNsMJ0ZJm9GLJP1rlO"</span>,</span><br><span class="line"> <span class="attr">"authCacheKey"</span>: <span class="number">2</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"token"</span>: <span class="string">"b25e39b47e774b4a05b3cb1555fc377f209457c3fd339d373d3fca7b1ea8be56fdc6ed05b7ffb0700e7300d242fb83b57b35f45ee1b155b380 50a0671bc7ec54c2f2c5bb1aee0651db69ce657e8ab4cb79c7806209103eda8a3bc96aa043a0144ae3c06a5c549ac168183c37384cf4347e450bf11644d0 62c31ffc3059e63722f849a5de4540b0d1"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"message"</span>: <span class="string">"认证成功"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><h3 id="Redis缓存使用"><a href="#Redis缓存使用" class="headerlink" title="Redis缓存使用"></a>Redis缓存使用</h3><p>在系统启动过程中,会执行缓存初始化操作,对应<code>CacheInitRunner</code>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CacheInitRunner</span> <span class="keyword">implements</span> <span class="title">ApplicationRunner</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserService userService;</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> UserManager userManager;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">(ApplicationArguments args)</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> ...</span><br><span class="line"> List<User> list = <span class="keyword">this</span>.userService.list();</span><br><span class="line"> <span class="keyword">for</span> (User user : list) {</span><br><span class="line"> userManager.loadUserRedisCache(user);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>loadUserRedisCache</code>方法源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 将用户相关信息添加到 Redis缓存中</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> user user</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadUserRedisCache</span><span class="params">(User user)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="comment">// 缓存用户</span></span><br><span class="line"> cacheService.saveUser(user.getUsername());</span><br><span class="line"> <span class="comment">// 缓存用户角色</span></span><br><span class="line"> cacheService.saveRoles(user.getUsername());</span><br><span class="line"> <span class="comment">// 缓存用户权限</span></span><br><span class="line"> cacheService.savePermissions(user.getUsername());</span><br><span class="line"> <span class="comment">// 缓存用户个性化配置</span></span><br><span class="line"> cacheService.saveUserConfigs(String.valueOf(user.getUserId()));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,这一过程缓存了用户信息,用户角色信息,用户权限信息,用户的个性化配置信息,缓存的具体key,value可以查看上述方法的源码。通过这些缓存,可以一定程度减轻数据库压力。</p><p>为了确保缓存数据和数据库数据的一致性,我们必须在相应的增删改方法中对缓存进行相应的操作。比如在更新用户后,我们必须更新相应的缓存:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">updateUser</span><span class="params">(User user)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 重新将用户信息,用户角色信息,用户权限信息 加载到 redis中</span></span><br><span class="line"> cacheService.saveUser(user.getUsername());</span><br><span class="line"> cacheService.saveRoles(user.getUsername());</span><br><span class="line"> cacheService.savePermissions(user.getUsername());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><div class="note danger"><p>总而言之,由于我们在启动系统的时候缓存了用户信息,用户角色信息,用户权限信息,用户的个性化配置信息,之后凡是涉及到用户,用户角色,用户权限和用户个性化配置的相关增删改操作都应该及时更新相应的缓存。</p></div><h3 id="动态路由构建"><a href="#动态路由构建" class="headerlink" title="动态路由构建"></a>动态路由构建</h3><p>不同的用户拥有不同的角色,不同的角色对应不同的菜单权限,所以我们需要通过用户查询出对应的菜单列表,然后将列表构建成前端需要的路由(前端根据路由信息构建相应的菜单)。</p><p>获取用户路由的方法为<code>UserManage#getUserRouters</code>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 通过用户名构建 Vue路由</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> username 用户名</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 路由集合</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> ArrayList<VueRouter<Menu>> getUserRouters(String username) {</span><br><span class="line"> List<VueRouter<Menu>> routes = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> List<Menu> menus = <span class="keyword">this</span>.menuService.findUserMenus(username);</span><br><span class="line"> menus.forEach(menu -> {</span><br><span class="line"> VueRouter<Menu> route = <span class="keyword">new</span> VueRouter<>();</span><br><span class="line"> route.setId(menu.getMenuId().toString());</span><br><span class="line"> route.setParentId(menu.getParentId().toString());</span><br><span class="line"> route.setIcon(menu.getIcon());</span><br><span class="line"> route.setPath(menu.getPath());</span><br><span class="line"> route.setComponent(menu.getComponent());</span><br><span class="line"> route.setName(menu.getMenuName());</span><br><span class="line"> route.setMeta(<span class="keyword">new</span> RouterMeta(<span class="keyword">true</span>, <span class="keyword">null</span>));</span><br><span class="line"> routes.add(route);</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> TreeUtil.buildVueRouter(routes);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>比如用户mrbird对应的前端路由为:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"主页"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"MenuView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"none"</span>,</span><br><span class="line"> <span class="attr">"redirect"</span>: <span class="string">"/home"</span>,</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/home"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"系统主页"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"HomePageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"home"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"isShow"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"系统管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"PageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"appstore-o"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system/user"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"用户管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"system/user/User"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system/role"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"角色管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"system/role/Role"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system/menu"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"菜单管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"system/menu/Menu"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system/dept"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"部门管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"system/dept/Dept"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/system/dict"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"字典管理"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"system/dict/Dict"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"系统监控"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"PageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"dashboard"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/online"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"在线用户"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/Online"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/systemlog"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"系统日志"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/SystemLog"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/redis/info"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"Redis监控"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/RedisInfo"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/httptrace"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"请求追踪"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/Httptrace"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/system"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"系统信息"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"EmptyPageView"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/system/jvminfo"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"JVM信息"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/JvmInfo"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/system/tomcatinfo"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"Tomcat信息"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/TomcatInfo"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/monitor/system/info"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"服务器信息"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"monitor/SystemInfo"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/job"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"任务调度"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"PageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"clock-circle-o"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/job/job"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"定时任务"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"quartz/job/Job"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/job/log"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"调度日志"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"quartz/log/JobLog"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"网络资源"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"PageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"compass"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web/weather"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"天气查询"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"web/Weather"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web/dailyArticle"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"每日一文"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"web/DailyArticle"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web/movie"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"影视资讯"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"EmptyPageView"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web/movie/hot"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"正在热映"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"web/MovieHot"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/web/movie/coming"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"即将上映"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"web/MovieComing"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/others"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"其他模块"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"PageView"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"coffee"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"children"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/others/excel"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"导入导出"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"others/Excel"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"/profile"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"个人中心"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"personal/Profile"</span>,</span><br><span class="line"> <span class="attr">"icon"</span>: <span class="string">"none"</span>,</span><br><span class="line"> <span class="attr">"meta"</span>: {</span><br><span class="line"> <span class="attr">"closeable"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"isShow"</span>: <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"path"</span>: <span class="string">"*"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"404"</span>,</span><br><span class="line"> <span class="attr">"component"</span>: <span class="string">"error/404"</span></span><br><span class="line"> }</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p></p><p>关于Vue Router可以参考:<a href="https://router.vuejs.org/zh/" target="_blank" rel="noopener">https://router.vuejs.org/zh/</a>。</p><h3 id="权限控制"><a href="#权限控制" class="headerlink" title="权限控制"></a>权限控制</h3><p>我们可以在Controller的方法上通过Shiro相关的权限注解进行权限控制,比如下面这个方法只有当用户拥有<code>user:add</code>权限才能访问:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping</span>(<span class="string">"/user"</span>)</span><br><span class="line"><span class="meta">@RequiresPermissions</span>(<span class="string">"user:add"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addUser</span><span class="params">(@Valid User user)</span> </span>{</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>当用户没有<code>user:add</code>权限时,系统将抛出<code>UnauthorizedException</code>异常,由<code>GlobalExceptionHandler</code>捕获,返回403状态码。</p><p>更多Shiro提供的权限注解可以参考:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。</span></span><br><span class="line"><span class="meta">@RequiresAuthentication</span> </span><br><span class="line"> </span><br><span class="line"><span class="comment">// 表示当前Subject已经身份验证或者通过记住我登录的。</span></span><br><span class="line"><span class="meta">@RequiresUser</span> </span><br><span class="line"></span><br><span class="line"><span class="comment">// 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。</span></span><br><span class="line"><span class="meta">@RequiresGuest</span> </span><br><span class="line"></span><br><span class="line"><span class="comment">// 表示当前Subject需要角色admin和user。 </span></span><br><span class="line"><span class="meta">@RequiresRoles</span>(value={<span class="string">"admin"</span>, <span class="string">"user"</span>}, logical= Logical.AND) </span><br><span class="line"></span><br><span class="line"><span class="comment">// 表示当前Subject需要权限user:a或user:b。</span></span><br><span class="line"><span class="meta">@RequiresPermissions</span> (value={<span class="string">"user:a"</span>, <span class="string">"user:b"</span>}, logical= Logical.OR)</span><br></pre></td></tr></table></figure><p></p><h3 id="多数据源"><a href="#多数据源" class="headerlink" title="多数据源"></a>多数据源</h3><p>多数据源采用的是MyBatis Plus提供的方案:<a href="https://mp.baomidou.com/guide/dynamic-datasource.html" target="_blank" rel="noopener">https://mp.baomidou.com/guide/dynamic-datasource.html</a></p><h3 id="代码生成"><a href="#代码生成" class="headerlink" title="代码生成"></a>代码生成</h3><p>目前只有后端代码生成器,采用的是MyBatis Plus提供的方案,对应源码<code>cc.mrbird.febs.common.generator.CodeGenerator</code>,执行其main方法,然后输入表名,就可以生成相应的domain、dao、service、controller、mapper.xml。</p><h3 id="Excel导入导出"><a href="#Excel导入导出" class="headerlink" title="Excel导入导出"></a>Excel导入导出</h3><p>Excel导入导出使用的插件为:<a href="https://gitee.com/wuwenze/ExcelKit" target="_blank" rel="noopener">https://gitee.com/wuwenze/ExcelKit</a>,具体操作规则可以仔细阅读这个项目的Readme.md</p><h3 id="统一参数校验"><a href="#统一参数校验" class="headerlink" title="统一参数校验"></a>统一参数校验</h3><p>统一参数校验可以参考我的博客:<a href="https://mrbird.cc/Spring-Boot-Hibernate-Validator-Params-Check.html">Spring Boot配合Hibernate Validator参数校验</a>。</p><h3 id="SQL打印"><a href="#SQL打印" class="headerlink" title="SQL打印"></a>SQL打印</h3><p>SQL打印采用的插件为<a href="https://p6spy.readthedocs.io/en/latest/index.html" target="_blank" rel="noopener">p6spy</a>,要开启p6spy的SQL打印功能,只需将配置文件application.yml中的<code>spring.datasource.dynamic.p6spy</code>改为<code>true</code>即可。</p><p>在p6spy.properties文件中可以配置打印规则:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"># 使用日志系统记录 sql</span><br><span class="line">appender=com.p6spy.engine.spy.appender.Slf4JLogger</span><br><span class="line"># 自定义日志打印</span><br><span class="line">logMessageFormat=cc.mrbird.febs.common.config.P6spySqlFormatConfig</span><br><span class="line"># 是否开启慢 SQL记录</span><br><span class="line">outagedetection=true</span><br><span class="line"># 慢 SQL记录标准 2 秒</span><br><span class="line">outagedetectioninterval=2</span><br><span class="line"># 开启过滤</span><br><span class="line">filter=true</span><br><span class="line"># 包含 QRTZ的不打印</span><br><span class="line">exclude=QRTZ</span><br></pre></td></tr></table></figure><p></p><p>SQL打印效果如下所示:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">2019-04-08 15:29:50 | INFO | http-nio-9527-exec-1 | p6spy | 2019-04-08 15:29:50 | 耗时 73 ms | SQL 语句:</span><br><span class="line">UPDATE t_user SET last_login_time='2019-04-08T15:29:50.724+0800' WHERE username = 'mrbird';</span><br><span class="line">2019-04-08 15:29:50 | INFO | http-nio-9527-exec-1 | p6spy | 2019-04-08 15:29:50 | 耗时 0 ms | SQL 语句:</span><br><span class="line">SELECT USER_ID,username,password,dept_id,email,mobile,status,create_time,modify_time,last_login_time,ssex,description,avatar FROM t_user WHERE username = 'mrbird';</span><br><span class="line">2019-04-08 15:29:52 | INFO | http-nio-9527-exec-1 | p6spy | 2019-04-08 15:29:52 | 耗时 489 ms | SQL 语句:</span><br><span class="line">INSERT INTO t_login_log ( username, login_time, location, ip ) VALUES ( 'mrbird', '2019-04-08T15:29:50.874+0800', '', '127.0.0.1' );</span><br><span class="line">2019-04-08 15:45:20 | INFO | http-nio-9527-exec-7 | p6spy | 2019-04-08 15:45:20 | 耗时 1 ms | SQL 语句:</span><br><span class="line">UPDATE t_user SET last_login_time='2019-04-08T15:45:20.193+0800' WHERE username = 'scott';</span><br><span class="line">2019-04-08 15:45:20 | INFO | http-nio-9527-exec-7 | p6spy | 2019-04-08 15:45:20 | 耗时 0 ms | SQL 语句:</span><br><span class="line">SELECT USER_ID,username,password,dept_id,email,mobile,status,create_time,modify_time,last_login_time,ssex,description,avatar FROM t_user WHERE username = 'scott';</span><br><span class="line">2019-04-08 15:45:21 | INFO | http-nio-9527-exec-7 | p6spy | 2019-04-08 15:45:21 | 耗时 89 ms | SQL 语句:</span><br><span class="line">INSERT INTO t_login_log ( username, login_time, location, ip ) VALUES ( 'scott', '2019-04-08T15:45:20.466+0800', '', '127.0.0.1' );</span><br></pre></td></tr></table></figure><div class="note info"><p>开启这个功能方便我们开发调试,生产环境最好关闭这个功能,因为它在一定程度上会造成性能耗损。</p></div><p>更多p6psy的配置可以参考:<a href="https://p6spy.readthedocs.io/en/latest/configandusage.html" target="_blank" rel="noopener">https://p6spy.readthedocs.io/en/latest/configandusage.html</a></p><h3 id="AOP记录操作日志"><a href="#AOP记录操作日志" class="headerlink" title="AOP记录操作日志"></a>AOP记录操作日志</h3><p>具体可以参考我的博客:<a href="https://mrbird.cc/Spring-Boot-AOP%20log.html">Spring Boot AOP记录用户操作日志</a>。</p><p>记录操作日志的过程可以改为异步的方式,这样不会造成接口性能损耗,可以参考我的博客:<a href="https://mrbird.cc/Spring-Boot-Async.html">Spring Boot 中的异步调用</a>。</p><h3 id="接口限流"><a href="#接口限流" class="headerlink" title="接口限流"></a>接口限流</h3><p>项目中<code>@Limit</code>注解可以实现接口的限流。即规定一段时间内最多可以访问该接口的次数,超过这个次数则抛出<code>LimitAccessException</code>异常。<code>@Limit</code>注解如下所示:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> cc.mrbird.common.domain.LimitType;</span><br><span class="line"><span class="keyword">import</span> java.lang.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Target</span>(ElementType.METHOD)</span><br><span class="line"><span class="meta">@Retention</span>(RetentionPolicy.RUNTIME)</span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> Limit {</span><br><span class="line"> <span class="comment">// 资源名称,用于描述接口功能</span></span><br><span class="line"> <span class="function">String <span class="title">name</span><span class="params">()</span> <span class="keyword">default</span> ""</span>;</span><br><span class="line"> <span class="comment">// 资源 key</span></span><br><span class="line"> <span class="function">String <span class="title">key</span><span class="params">()</span> <span class="keyword">default</span> ""</span>;</span><br><span class="line"> <span class="comment">// key prefix</span></span><br><span class="line"> <span class="function">String <span class="title">prefix</span><span class="params">()</span> <span class="keyword">default</span> ""</span>;</span><br><span class="line"> <span class="comment">// 时间的,单位秒</span></span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">period</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="comment">// 限制访问次数</span></span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">count</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="comment">// 限制类型</span></span><br><span class="line"> <span class="function">LimitType <span class="title">limitType</span><span class="params">()</span> <span class="keyword">default</span> LimitType.CUSTOMER</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>其中,limitType包含传统类型限流和根据IP限流,其为枚举类型:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> LimitType {</span><br><span class="line"> <span class="comment">// 传统类型</span></span><br><span class="line"> CUSTOMER,</span><br><span class="line"> <span class="comment">// 根据 IP 限制</span></span><br><span class="line"> IP;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>下面举个使用示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> cc.mrbird.common.annotation.Limit;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.atomic.AtomicInteger;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestController</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> AtomicInteger ATOMIC_INTEGER = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Limit</span>(key = <span class="string">"test"</span>, period = <span class="number">600</span>, count = <span class="number">10</span>, name = <span class="string">"resource"</span>, prefix = <span class="string">"limit"</span>)</span><br><span class="line"> <span class="meta">@GetMapping</span>(<span class="string">"/test"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">testLimiter</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ATOMIC_INTEGER.incrementAndGet();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>上面配置表示使用传统限流的方式,testLimiter方法在600秒内最多只能访问10次。当600秒内第11次访问该接口时,接口将抛出<code>LimitAccessException</code>异常。</p><h3 id="Shiro教程"><a href="#Shiro教程" class="headerlink" title="Shiro教程"></a>Shiro教程</h3><ol><li><p><a href="https://mrbird.cc/Apache%20Shiro%E7%AE%80%E4%BB%8B.html">Apache Shiro简介</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-shiro%20Authentication.html">Spring Boot Shiro用户认证</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-Shiro%20Remember-Me.html">Spring Boot Shiro 添加记住我功能</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-Shiro%20Authorization.html">Spring Boot Shiro权限控制</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-Shiro%20cache.html">Spring Boot Shiro中使用缓存</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-Themeleaf%20Shiro%20tag.html">Spring Boot Thymeleaf中使用Shiro标签</a></p></li><li><p><a href="https://mrbird.cc/Spring-Boot-Shiro%20session.html">Spring Boot Shiro在线会话管理</a></p></li></ol><h3 id="Shiro如何整合JWT"><a href="#Shiro如何整合JWT" class="headerlink" title="Shiro如何整合JWT"></a>Shiro如何整合JWT</h3><p>Shiro如何整合JWT可以参考:<a href="https://gitlab.com/wuyouzhuguli/shiro_jwt" target="_blank" rel="noopener">https://gitlab.com/wuyouzhuguli/shiro_jwt</a></p><p>为了简化过程,例子没有使用数据库和Redis,在内存中模拟了两个用户:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 模拟两个用户</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> List<User></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> List<User> <span class="title">users</span><span class="params">()</span> </span>{</span><br><span class="line"> List<User> users = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="comment">// 模拟两个用户:</span></span><br><span class="line"> <span class="comment">// 1. 用户名 admin,密码 123456,角色 admin(管理员),权限 "user:add","user:view"</span></span><br><span class="line"> <span class="comment">// 1. 用户名 scott,密码 123456,角色 regist(注册用户),权限 "user:view"</span></span><br><span class="line"> users.add(<span class="keyword">new</span> User(</span><br><span class="line"> <span class="string">"admin"</span>,</span><br><span class="line"> <span class="string">"bfc62b3f67a4c3e57df84dad8cc48a3b"</span>,</span><br><span class="line"> <span class="keyword">new</span> HashSet<>(Collections.singletonList(<span class="string">"admin"</span>)),</span><br><span class="line"> <span class="keyword">new</span> HashSet<>(Arrays.asList(<span class="string">"user:add"</span>, <span class="string">"user:view"</span>))));</span><br><span class="line"> users.add(<span class="keyword">new</span> User(</span><br><span class="line"> <span class="string">"scott"</span>,</span><br><span class="line"> <span class="string">"11bd73355c7bbbac151e4e4f943e59be"</span>,</span><br><span class="line"> <span class="keyword">new</span> HashSet<>(Collections.singletonList(<span class="string">"regist"</span>)),</span><br><span class="line"> <span class="keyword">new</span> HashSet<>(Collections.singletonList(<span class="string">"user:view"</span>))));</span><br><span class="line"> <span class="keyword">return</span> users;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>可以使用PostMan进行接口测试。</p><h2 id="前端项目介绍"><a href="#前端项目介绍" class="headerlink" title="前端项目介绍"></a>前端项目介绍</h2><h3 id="项目结构-1"><a href="#项目结构-1" class="headerlink" title="项目结构"></a>项目结构</h3><p>项目机构如下图所示:</p><p><img src="img/febsvue/2019-04-08_185510.png" alt="2019-04-08_185510.png"></p><h3 id="数据存储"><a href="#数据存储" class="headerlink" title="数据存储"></a>数据存储</h3><p>在用户登录成功后,项目会通过Vuex和localstorage存储一些数据供项目全局使用。</p><p>正如前面所说,当用户登录成功后,后端会返回如下数据:</p><ol><li><p>token:token;</p></li><li><p>exipreTime:token过期时间;</p></li><li><p>roles:用户角色;</p></li><li><p>permissions:用户权限;</p></li><li><p>config:用户前端系统的个性化配置;</p></li><li><p>user:用户信息(不包括密码)。</p></li></ol><p>在src/views/login/Login.vue文件中你会看到如下代码:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">...mapMutations({</span><br><span class="line"> setToken: <span class="string">'account/setToken'</span>,</span><br><span class="line"> setExpireTime: <span class="string">'account/setExpireTime'</span>,</span><br><span class="line"> setPermissions: <span class="string">'account/setPermissions'</span>,</span><br><span class="line"> setRoles: <span class="string">'account/setRoles'</span>,</span><br><span class="line"> setUser: <span class="string">'account/setUser'</span>,</span><br><span class="line"> setTheme: <span class="string">'setting/setTheme'</span>,</span><br><span class="line"> setLayout: <span class="string">'setting/setLayout'</span>,</span><br><span class="line"> setMultipage: <span class="string">'setting/setMultipage'</span>,</span><br><span class="line"> fixSiderbar: <span class="string">'setting/fixSiderbar'</span>,</span><br><span class="line"> fixHeader: <span class="string">'setting/fixHeader'</span>,</span><br><span class="line"> setColor: <span class="string">'setting/setColor'</span></span><br><span class="line">}),</span><br><span class="line">saveLoginData (data) {</span><br><span class="line"> <span class="keyword">this</span>.setToken(data.token)</span><br><span class="line"> <span class="keyword">this</span>.setExpireTime(data.exipreTime)</span><br><span class="line"> <span class="keyword">this</span>.setUser(data.user)</span><br><span class="line"> <span class="keyword">this</span>.setPermissions(data.permissions)</span><br><span class="line"> <span class="keyword">this</span>.setRoles(data.roles)</span><br><span class="line"> <span class="keyword">this</span>.setTheme(data.config.theme)</span><br><span class="line"> <span class="keyword">this</span>.setLayout(data.config.layout)</span><br><span class="line"> <span class="keyword">this</span>.setMultipage(data.config.multiPage === <span class="string">'1'</span>)</span><br><span class="line"> <span class="keyword">this</span>.fixSiderbar(data.config.fixSiderbar === <span class="string">'1'</span>)</span><br><span class="line"> <span class="keyword">this</span>.fixHeader(data.config.fixHeader === <span class="string">'1'</span>)</span><br><span class="line"> <span class="keyword">this</span>.setColor(data.config.color)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>这将把登录接口返回的数据存储到Vuex和浏览器的localstorage中。</p><p>存储Token的原因是,后续需要认证的请求,都会在请求头中携带这个Token;存储ExpireTime的原因是为了优化认证过期时的用户体验;存储用户信息的原因是为了供个人中心和系统主页使用;存储角色和权限是为了判断页面按钮渲染与否;存储用户个性化配置的原因是为了通过这些配置渲染不同的系统界面。</p><h3 id="路由导航守卫"><a href="#路由导航守卫" class="headerlink" title="路由导航守卫"></a>路由导航守卫</h3><p>路由导航守卫代码位置:frontend/src/router/index.js:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="keyword">const</span> whiteList = [<span class="string">'/login'</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> asyncRouter</span><br><span class="line"></span><br><span class="line"><span class="comment">// 导航守卫,渲染动态路由</span></span><br><span class="line">router.beforeEach(<span class="function">(<span class="params">to, <span class="keyword">from</span>, next</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (whiteList.indexOf(to.path) !== <span class="number">-1</span>) {</span><br><span class="line"> next()</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">let</span> token = db.get(<span class="string">'USER_TOKEN'</span>)</span><br><span class="line"> <span class="keyword">let</span> user = db.get(<span class="string">'USER'</span>)</span><br><span class="line"> <span class="keyword">let</span> userRouter = get(<span class="string">'USER_ROUTER'</span>)</span><br><span class="line"> <span class="keyword">if</span> (token.length && user) {</span><br><span class="line"> <span class="keyword">if</span> (!asyncRouter) {</span><br><span class="line"> <span class="keyword">if</span> (!userRouter) {</span><br><span class="line"> request.get(<span class="string">`menu/<span class="subst">${user.username}</span>`</span>).then(<span class="function">(<span class="params">res</span>) =></span> {</span><br><span class="line"> asyncRouter = res.data</span><br><span class="line"> save(<span class="string">'USER_ROUTER'</span>, asyncRouter)</span><br><span class="line"> go(to, next)</span><br><span class="line"> })</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> asyncRouter = userRouter</span><br><span class="line"> go(to, next)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> next()</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> next(<span class="string">'/login'</span>)</span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">go</span> (<span class="params">to, next</span>) </span>{</span><br><span class="line"> asyncRouter = filterAsyncRouter(asyncRouter)</span><br><span class="line"> router.addRoutes(asyncRouter)</span><br><span class="line"> next({...to, <span class="attr">replace</span>: <span class="literal">true</span>})</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p></p><p>主要逻辑分为下面几步:</p><ol><li><p>判断要跳转的路由地址是否在路由白名单内,是的话放行,不是的话进行第2步;</p></li><li><p>从内存中获取token和用户信息,如果存在则进行第3步,不存在跳转到登录页;</p></li><li><p>判断动态路由信息是否存在,存在则放行,不存在则进行第4步;</p></li><li><p>判断用户路由是否已经加载,是的话将用户路由赋值给动态路由,并执行路由添加操作,然后跳转;如果用户路由不存在,则执行第5步;</p></li><li><p>根据用户名从后台获取用户路由信息,并将其保存到内存中,再执行路由添加操作,然后跳转。</p></li></ol><h3 id="权限控制-1"><a href="#权限控制-1" class="headerlink" title="权限控制"></a>权限控制</h3><p>在前端页面中,我们已经实现了通过不同用户获取不同的路由,以此渲染出不同的菜单列表功能,此外页面上的操作按钮也必须进行权限控制。</p><p>正如前面所述,在登录成功后,系统会把用户的角色和权限信息存储到了内存中,所以我们可以通过这些信息结合<a href="https://cn.vuejs.org/v2/guide/custom-directive.html" target="_blank" rel="noopener"> 自定义Vue指令 </a>的方式来实现按钮的权限控制。</p><p>目前支持的和权限相关的Vue指令有:</p><table><thead><tr><th>指令</th><th>含义</th><th>示例</th></tr></thead><tbody><tr><td>v-hasPermission</td><td>当用户拥有列出的权限的时候,渲染该元素</td><td><code><template v-hasPermission="'user:add','user:update'"><span>hello</span></template></code></td></tr><tr><td>v-hasAnyPermission</td><td>当用户拥有列出的任意一项权限的时候,渲染该元素</td><td><code><template v-hasAnyPermission="'user:add','user:update'"><span>hello</span></template></code></td></tr><tr><td>v-hasRole</td><td>当用户拥有列出的角色的时候,渲染该元素</td><td><code><template v-hasRole="'admin','register'"><span>hello</span></template></code></td></tr><tr><td>v-hasAnyRole</td><td>当用户拥有列出的任意一个角色的时候,渲染该元素</td><td><code><template v-hasAnyRole="'admin','register'"><span>hello</span></template></code></td></tr><tr><td>hasNoPermission</td><td>当用户没有列出的权限的时候,渲染该元素</td><td><code><template v-hasNoPermission="'user:add','register'"><span>无操作权限</span></template></code></td></tr></tbody></table><p>以<code>v-hasPermission="user:add"</code>为例,详细介绍下自定义权限Vue指令的实现过程:</p><ol><li><p>在src/utils/permissionDirect.js中定义如下代码:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">export</span> <span class="keyword">const</span> hasPermission = {</span><br><span class="line"> install (Vue) {</span><br><span class="line"> Vue.directive(<span class="string">'hasPermission'</span>, {</span><br><span class="line"> bind (el, binding, vnode) {</span><br><span class="line"> <span class="keyword">let</span> permissions = vnode.context.$store.state.account.permissions</span><br><span class="line"> <span class="keyword">let</span> value = binding.value.split(<span class="string">','</span>)</span><br><span class="line"> <span class="keyword">let</span> flag = <span class="literal">true</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> v <span class="keyword">of</span> value) {</span><br><span class="line"> <span class="keyword">if</span> (!permissions.includes(v)) {</span><br><span class="line"> flag = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!flag) {</span><br><span class="line"> <span class="keyword">if</span> (!el.parentNode) {</span><br><span class="line"> el.style.display = <span class="string">'none'</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> el.parentNode.removeChild(el)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面代码中,我们从Vuex中获取了用户所拥有的权限,然后判断这些权限中是否包含<code>user:add</code>,如果不包含,则将对应的元素(el)移除或者隐藏。所以当用户没有<code>user:add</code>权限时,下面的按钮将不会被渲染在页面上:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">template</span> <span class="attr">v-hasPermission</span>=<span class="string">"'user:add'"</span>></span><span class="tag"><<span class="name">button</span>></span>新增用户<span class="tag"></<span class="name">button</span>></span><span class="tag"></<span class="name">template</span>></span></span><br></pre></td></tr></table></figure></li><li><p>要让自定义Vue指令生效,还需要在src/utils/install.js中将其添加到Plugins列表。</p></li></ol><h3 id="Axios封装"><a href="#Axios封装" class="headerlink" title="Axios封装"></a>Axios封装</h3><p>项目使用Axios插件来发送HTTP请求,并对它进行了封装(frontend/src/utils/request.js),这里主要讲述下request.js中主要逻辑。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 统一配置</span></span><br><span class="line"><span class="keyword">let</span> FEBS_REQUEST = axios.create({</span><br><span class="line"> baseURL: <span class="string">'http://127.0.0.1:9527/'</span>,</span><br><span class="line"> responseType: <span class="string">'json'</span>,</span><br><span class="line"> validateStatus (status) {</span><br><span class="line"> <span class="comment">// 200 外的状态码都认定为失败</span></span><br><span class="line"> <span class="keyword">return</span> status === <span class="number">200</span></span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>上面这段代码对请求进行了统一配置,baseURL定义了后端地址的基础路径,responseType定义了响应数据类型为JSON,只有状态码为200时才认定请求成功。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 拦截请求</span></span><br><span class="line">FEBS_REQUEST.interceptors.request.use(<span class="function">(<span class="params">config</span>) =></span> {</span><br><span class="line"> <span class="keyword">let</span> expireTime = store.state.account.expireTime</span><br><span class="line"> <span class="keyword">let</span> now = moment().format(<span class="string">'YYYYMMDDHHmmss'</span>)</span><br><span class="line"> <span class="comment">// 让token早10秒种过期,提升“请重新登录”弹窗体验</span></span><br><span class="line"> <span class="keyword">if</span> (now - expireTime >= <span class="number">-10</span>) {</span><br><span class="line"> Modal.error({</span><br><span class="line"> title: <span class="string">'登录已过期'</span>,</span><br><span class="line"> content: <span class="string">'很抱歉,登录已过期,请重新登录'</span>,</span><br><span class="line"> okText: <span class="string">'重新登录'</span>,</span><br><span class="line"> mask: <span class="literal">false</span>,</span><br><span class="line"> onOk: <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =></span> {</span><br><span class="line"> <span class="comment">// 为什么不直接跳转到登录页呢?那是因为Vue没有提供清空路由的方法,只能通过刷新页面的方式</span></span><br><span class="line"> <span class="comment">// 来清除之前动态添加的路由信息。</span></span><br><span class="line"> db.clear()</span><br><span class="line"> location.reload()</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 有 token就带上</span></span><br><span class="line"> <span class="keyword">if</span> (store.state.account.token) {</span><br><span class="line"> config.headers.Authentication = store.state.account.token</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> config</span><br><span class="line">}, (error) => {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>在Axios的前置拦截中,我们首先判断了token是否已经过期,如果过期了则清空在登录时保存的数据,并且刷新页面。这时候通过路由导航守卫,页面会被重定向到登录页面,引导用户重新登录。</p><p>如果token没有过期,则在请求头上带上token信息。<code>headers.Authentication</code>和后端代码中的TOKEN名称相对应:</p><p><img src="img/febsvue/QQ截图20190409101831.png" alt="QQ截图20190409101831.png"></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 拦截响应</span></span><br><span class="line">FEBS_REQUEST.interceptors.response.use(<span class="function">(<span class="params">config</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> config</span><br><span class="line">}, (error) => {</span><br><span class="line"> <span class="keyword">if</span> (error.response) {</span><br><span class="line"> <span class="keyword">let</span> errorMessage = error.response.data === <span class="literal">null</span> ? <span class="string">'系统内部异常,请联系网站管理员'</span> : error.response.data.message</span><br><span class="line"> <span class="keyword">switch</span> (error.response.status) {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">404</span>:</span><br><span class="line"> notification.error({</span><br><span class="line"> message: <span class="string">'系统提示'</span>,</span><br><span class="line"> description: <span class="string">'很抱歉,资源未找到'</span>,</span><br><span class="line"> duration: <span class="number">4</span></span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> <span class="keyword">case</span> <span class="number">403</span>:</span><br><span class="line"> <span class="keyword">case</span> <span class="number">401</span>:</span><br><span class="line"> notification.warn({</span><br><span class="line"> message: <span class="string">'系统提示'</span>,</span><br><span class="line"> description: <span class="string">'很抱歉,您无法访问该资源,可能是因为没有相应权限或者登录已失效'</span>,</span><br><span class="line"> duration: <span class="number">4</span></span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> notification.error({</span><br><span class="line"> message: <span class="string">'系统提示'</span>,</span><br><span class="line"> description: errorMessage,</span><br><span class="line"> duration: <span class="number">4</span></span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>上面代码中我们实现了异常响应的统一处理,根据不同的状态码给予不同的提示信息。</p><p>此外,项目还对各种HTTP请求进行了封装,下面一一列举出它们的用法:</p><p><strong>GET请求</strong></p><p>方式一:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$get(<span class="string">'user'</span>, {</span><br><span class="line"> ...params</span><br><span class="line">}).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p>因为我们在前面已经定义了后端地址的基础路径,所以上面这个请求的实际地址为:<a href="http://127.0.0.1:9527/user。" target="_blank" rel="noopener">http://127.0.0.1:9527/user。</a></p><p>在前面我们已经对异常响应进行统一处理,当然你也可以通过catch来覆盖:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$get(<span class="string">'user'</span>, {</span><br><span class="line"> ...params</span><br><span class="line">}).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">}).catch(<span class="function">(<span class="params">error</span>) =></span> {</span><br><span class="line"> alert(<span class="string">'出错啦'</span>)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>方式二(路径传参):</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$get(<span class="string">`menu/<span class="subst">${user.username}</span>`</span>).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p>方式三:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$get(<span class="string">`user?username=<span class="subst">${user.username}</span>`</span>).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p><strong>POST请求</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$post(<span class="string">'user'</span>, {</span><br><span class="line"> ...params</span><br><span class="line">}).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p><strong>PUT请求</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$put(<span class="string">'user'</span>, {</span><br><span class="line"> ...params</span><br><span class="line">}).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p><strong>DELETE请求</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$<span class="keyword">delete</span>(<span class="string">'user/${user.userId}'</span>).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p><strong>下载文件</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.$download(<span class="string">'file'</span>, { ...params }, <span class="string">'xxx.xx'</span>)</span><br></pre></td></tr></table></figure><p></p><p>xxx.xx为下载的文件名,比如下载xlsx文件的话为 Excel文件.xlsx,后端接口返回数据流即可。</p><p><strong>上传文件</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> formData = <span class="keyword">new</span> FormData($(<span class="string">"#form"</span>)[<span class="number">0</span>]);</span><br><span class="line"><span class="keyword">this</span>.$upload(<span class="string">'upload'</span>, formData).then(<span class="function">(<span class="params">r</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(r)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p></p><p>后端以<code>MultipartFile</code>接收文件即可。</p><h3 id="路径配置"><a href="#路径配置" class="headerlink" title="路径配置"></a>路径配置</h3><p>在build/webpack.base.conf.js中,我们定义了一些路径变量:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = {</span><br><span class="line"> ...</span><br><span class="line"> resolve: {</span><br><span class="line"> ...</span><br><span class="line"> alias: {</span><br><span class="line"> <span class="string">'vue$'</span>: <span class="string">'vue/dist/vue.esm.js'</span>,</span><br><span class="line"> <span class="string">'@'</span>: resolve(<span class="string">'src'</span>),</span><br><span class="line"> <span class="string">'~'</span>: resolve(<span class="string">'src/components'</span>),</span><br><span class="line"> <span class="string">'utils'</span>: resolve(<span class="string">'src/utils'</span>)</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p></p><p>比如<code>~</code>代表<code>src/components</code>路径,在项目中,如果要引入<code>src/components/test.vue</code>只需要这样做即可:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> ~<span class="regexp">/test.vue</span></span><br></pre></td></tr></table></figure><p></p><p>当然,你也可以使用相对路径和绝对路径,而不使用这些变量。</p><h3 id="第三方组件介绍"><a href="#第三方组件介绍" class="headerlink" title="第三方组件介绍"></a>第三方组件介绍</h3><p><a href="https://vue.ant.design/docs/vue/introduce-cn/" target="_blank" rel="noopener">Ant Design Vue</a>已经提供了非常丰富的组件,除此之外,项目里使用的图表插件为:<a href="https://apexcharts.com/vue-chart-demos/line-charts/basic/" target="_blank" rel="noopener">apexcharts.js</a>,其官方文档提供了很多示例,这里就不赘述了。</p><h3 id="开发建议"><a href="#开发建议" class="headerlink" title="开发建议"></a>开发建议</h3><p>相信正在阅读文档的你十有八九是一名后端开发者,可能对前端特别是对Vue不太熟悉,这里给出几点改造frontend的建议:</p><ol><li><p>要有一定的ES6语法基础,可以参考<a href="https://mrbird.cc/ES2015-Learn-Note.html"> ES6学习笔记 </a>,如果要系统学习ES6,推荐阮一峰的:<a href="http://es6.ruanyifeng.com/" target="_blank" rel="noopener"> ECMAScript 6 入门 </a>。</p></li><li><p>Vue的官方文档还是挺详细的,建议仔细阅读,我在学习Vue的时候也做了一些笔记,可以参考:<a href="https://mrbird.cc/Vue-Learn-Note.html">https://mrbird.cc/Vue-Learn-Note.html</a>。Vue的学习路线:Vue基础语法 -> Vuex -> Vue Router。</p></li><li><p>前端组件用的是<a href="https://vue.ant.design/docs/vue/introduce-cn/" target="_blank" rel="noopener">Ant Design Vue</a>,所以在使用它提供的组件的时候,多阅读它的使用文档。</p></li></ol><h2 id="开发示例"><a href="#开发示例" class="headerlink" title="开发示例"></a>开发示例</h2><h3 id="新建一个页面"><a href="#新建一个页面" class="headerlink" title="新建一个页面"></a>新建一个页面</h3><p>在frontend工程的src/views下新建一个test目录,在该目录下新建一个test.vue文件: <img src="img/febsvue/QQ截图20190409133947.png" alt="QQ截图20190409133947.png"></p><p>内容如下所示:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">template</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"test"</span>></span>{{hello}}<span class="tag"></<span class="name">div</span>></span></span><br><span class="line"><span class="tag"></<span class="name">template</span>></span></span><br><span class="line"><span class="tag"><<span class="name">script</span>></span><span class="undefined"></span></span><br><span class="line"><span class="javascript"><span class="keyword">export</span> <span class="keyword">default</span> {</span></span><br><span class="line"><span class="javascript"> name: <span class="string">'test'</span>,</span></span><br><span class="line"><span class="undefined"> data () {</span></span><br><span class="line"><span class="javascript"> <span class="keyword">return</span> {</span></span><br><span class="line"><span class="javascript"> hello: <span class="string">'hello world'</span></span></span><br><span class="line"><span class="undefined"> }</span></span><br><span class="line"><span class="undefined"> }</span></span><br><span class="line"><span class="undefined">}</span></span><br><span class="line"><span class="undefined"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"><span class="tag"><<span class="name">style</span> <span class="attr">lang</span>=<span class="string">"less"</span> <span class="attr">scoped</span>></span><span class="undefined"></span></span><br><span class="line"><span class="css"> <span class="selector-class">.test</span> {</span></span><br><span class="line"><span class="css"> <span class="selector-tag">color</span>: <span class="selector-id">#42b984</span>;</span></span><br><span class="line"><span class="css"> <span class="selector-tag">font-size</span>: 1<span class="selector-class">.1rem</span>;</span></span><br><span class="line"><span class="undefined"> font-weight: 600;</span></span><br><span class="line"><span class="undefined"> }</span></span><br><span class="line"><span class="undefined"></span><span class="tag"></<span class="name">style</span>></span></span><br></pre></td></tr></table></figure><p></p><p>启动项目,使用管理员账号登录,然后在菜单管理中新建一个菜单:</p><p><img src="img/febsvue/20190409134916.png" alt="QQ截图20190409134916.png"></p><ol><li><p>菜单URL:这里填写/test的话,在访问这个页面的时候,浏览器地址栏为<a href="http://localhost:8081/#/test" target="_blank" rel="noopener">http://localhost:8081/#/test</a>,只要确保这个值不重复即可;</p></li><li><p>组件地址:对应要渲染的Vue组件地址,在路由导航守卫中有如下一段代码:</p><p><img src="img/febsvue/20190409135425.png" alt="QQ截图20190409135425.png"></p><p>所以这里填test/test,对应<code>@/views/test/test.vue</code>组件。</p></li><li><p>相关权限:访问这个页面需要<code>test:view</code>权限。</p></li></ol><p>点击确定后,这个菜单就被建好了:</p><p><img src="img/febsvue/20190409135740.png" alt="QQ截图20190409135740.png"></p><p>接着修改管理员角色,将刚刚新建的菜单授权给管理员:</p><p><img src="img/febsvue/QQ截图20190409141220.png" alt="QQ截图20190409141220.png"></p><p>点击确定修改后,重新登录系统:</p><p><img src="img/febsvue/asdfasdfasfdasdf.png" alt="QQ截图20190409141410.png"></p><h3 id="如何新建一个多级菜单"><a href="#如何新建一个多级菜单" class="headerlink" title="如何新建一个多级菜单"></a>如何新建一个多级菜单</h3><p>在新增多级菜单前,先了解下系统中的一个约定:在一个多级菜单中,<strong>顶级菜单对应的组件为PageView,末级菜单对应的组件为需要渲染的页面组件,剩下的(非顶级,非末级的中间菜单对应的组件为EmptyPageView)</strong>。</p><p>我们来建一个四级菜单,首先新增一个顶级菜单:</p><p><img src="img/febsvue/QQ截图20190409142251.png" alt="QQ截图20190409142251.png"></p><p>因为是顶级菜单,所以对应组件填PageView。</p><p>接着新增第二级菜单:</p><p><img src="img/febsvue/QQ截图20190409142642.png" alt="QQ截图20190409142642.png"></p><p>因为它数据中间菜单(非顶级非末级)所以对应组件填EmptyPageView,上级菜单勾选刚刚新建的一级菜单。</p><p>继续新增三级菜单:</p><p><img src="img/febsvue/QQ截图20190409143223.png" alt="QQ截图20190409143223.png"></p><p>因为它也是一个中间菜单,所以对应组件填EmptyPageView,上级菜单勾选刚刚新建的二级菜单。</p><p>最后将我们前面建好的测试页面作为末级,点击测试页面后面的小齿轮按钮,进行修改:</p><p><img src="img/febsvue/QQ截图20190409143905.png" alt="QQ截图20190409143905.png"></p><p>点击确定后一个四级菜单就建好了:</p><p><img src="img/febsvue/QQ截图20190409143943.png" alt="QQ截图20190409143943.png"></p><p>最后一步授权!修改管理员角色:</p><p><img src="img/febsvue/QQ截图20190409144206.png" alt="QQ截图20190409144206.png"></p><p>修改后,重新登录系统:</p><p><img src="img/febsvue/QQ截图20190409144908.png" alt="QQ截图20190409144908.png"></p><p>因为我没有设置排序,所以默认排在最前面了。</p><h3 id="如何隐藏路由"><a href="#如何隐藏路由" class="headerlink" title="如何隐藏路由"></a>如何隐藏路由</h3><p>有的时候,一些路由并不需要渲染成菜单,比如个人中心这类页面。而这些页面一般都是所有用户共有的,所以在后台系统里构建路由的时候写死即可。要隐藏路由只需要将路由meta的isShow属性设置为false即可:</p><p>参考后台代码cc.mrbird.febs.common.utils.TreeUtil:</p><p><img src="img/febsvue/20190409150427.png" alt="QQ截图20190409150427.png"></p><h3 id="如何分配权限"><a href="#如何分配权限" class="headerlink" title="如何分配权限"></a>如何分配权限</h3><p>权限是和角色绑定的,所以要分配权限实际就是对角色的增删改。假如现在要配置一个角色 ——— 系统监控管理员,负责系统监控模块的查看:</p><p><img src="img/febsvue/QQ截图20190409152413.png" alt="QQ截图20190409152413.png"></p><p>然后新增一个用户 ——— yuuki,角色为系统监控管理员:</p><p><img src="img/febsvue/QQ截图20190409152550.png" alt="QQ截图20190409152550.png"></p><p>新建好后,使用yuuki的账号登录:</p><p><img src="img/febsvue/QQ截图20190409153002.png" alt="QQ截图20190409153002.png"></p><p>可以看到,yuuki只有系统监控模块的权限。</p><h3 id="前端如何添加依赖"><a href="#前端如何添加依赖" class="headerlink" title="前端如何添加依赖"></a>前端如何添加依赖</h3><p>在<a href="https://www.npmjs.com/" target="_blank" rel="noopener">https://www.npmjs.com/</a>搜索需要安装的依赖,比如jQuery:</p><p><img src="img/febsvue/QQ截图20190409153227.png" alt="QQ截图20190409153227.png"></p><p>比如我需要安装jQuery 3.3.1版本,只需要在终端输入如下命令即可:</p><p><img src="img/febsvue/QQ截图20190409153710.png" alt="QQ截图20190409153710.png"></p><p>安装好后,在package.json的依赖列表里会多出一个jquery 3.3.1:</p><p><img src="img/febsvue/QQ截图20190409153800.png" alt="QQ截图20190409153800.png"></p><h3 id="如何处理排序"><a href="#如何处理排序" class="headerlink" title="如何处理排序"></a>如何处理排序</h3><p>对于前端来说,需要上送两个参数:</p><ol><li><p>sortField:需要排序的字段;</p></li><li><p>sortOrder:排序规则,ascend或者descend。</p></li></ol><p>对于后端来说,排序主要分为四种情况:</p><ol><li>结果需要分页的,并且是通过xml定义的SQL查询出来的结果:</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortUtil.handlePageSort(request, page, <span class="string">"userId"</span>, FebsConstant.ORDER_ASC, <span class="keyword">false</span>);</span><br></pre></td></tr></table></figure><p><code>userId</code>和<code>FebsConstant.ORDER_ASC</code>指定了默认的排序规则,即默认按照<code>userId</code>字段升序排序。最后一个参数表示是否需要开启驼峰转下划线,这种情况下不需要,false即可。</p><p>具体可以参考cc.mrbird.febs.system.service.impl.UserServiceImpl#findUserDetail。</p><p>如果不需要指定默认排序规则,使用handlePageSort的重载方法即可:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortUtil.handlePageSort(request, page, <span class="keyword">false</span>);</span><br></pre></td></tr></table></figure><ol start="2"><li>结果需要分页的,并且是通过Mybatis Plus插件查询出来的结果:</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortUtil.handlePageSort(request, page, <span class="keyword">true</span>);</span><br></pre></td></tr></table></figure><p>这种情况下,最后一个参数值必须为true。</p><p>具体可以参考cc.mrbird.febs.system.service.impl.DictServiceImpl#findDicts。</p><div class="note info"><p>1和2主要区别就是是否需要开启驼峰转下划线。</p></div><ol start="3"><li>结果不需要分页,并且是通过xml定义的SQL查询出来的结果:</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortUtil.handleWrapperSort(request, queryWrapper, <span class="string">"orderNum"</span>, FebsConstant.ORDER_ASC, <span class="literal">false</span>);</span><br></pre></td></tr></table></figure><p>具体可以参考:cc.mrbird.febs.system.service.impl.DeptServiceImpl#findDepts</p><ol start="4"><li>结果不需要分页,并且是通过Mybatis Plus插件查询出来的结果:</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SortUtil.handleWrapperSort(request, queryWrapper, <span class="string">"orderNum"</span>, FebsConstant.ORDER_ASC, <span class="keyword">true</span>);</span><br></pre></td></tr></table></figure><p>总之,对于处理排序的方法,第一个参数一定是cc.mrbird.febs.common.domain.QueryRequest。第二个参数如果需要分页,则传递com.baomidou.mybatisplus.extension.plugins.pagination.Page,不需要分页则传递com.baomidou.mybatisplus.core.conditions.query.QueryWrapper。最后一个参数如果查询结果是Mybatis Plus查询出来的结果,则需设置为true,否则为false。</p><h3 id="后端接口测试"><a href="#后端接口测试" class="headerlink" title="后端接口测试"></a>后端接口测试</h3><p>由于后端接口为RESTful接口,所以不能使用浏览器来测试,可以使用PostMan或者Chrome插件RestLet来测试后端接口。文档以PostMan为例。</p><p>因为后台接口大部分都需要用户认证后才能访问,所以在测试之前需要通过登录接口获取一个可用的token。</p><p><img src="img/febsvue/QQ截图20190409161742.png" alt="QQ截图20190409161742.png"></p><p>成功获取到了token。</p><p>测试获取mrbird的前端路由信息:</p><p><img src="img/febsvue/QQ截图20190409161947.png" alt="QQ截图20190409161947.png"></p><p>在Headers设置一个键值对,key为Authentication,value为刚刚获取到的token,发送请求便可以获取到mrbird的路由信息。其他接口测试以此类推。</p><p>如果token填错或者不填:</p><p><img src="img/febsvue/QQ截图20190409162214.png" alt="QQ截图20190409162214.png"></p><p>后端将返回401。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="导入项目编译出错,代码是否不全?"><a href="#导入项目编译出错,代码是否不全?" class="headerlink" title="导入项目编译出错,代码是否不全?"></a>导入项目编译出错,代码是否不全?</h3><p>编辑器安装lombok插件即可。</p><h3 id="导入SQL为何出错?"><a href="#导入SQL为何出错?" class="headerlink" title="导入SQL为何出错?"></a>导入SQL为何出错?</h3><p>MySQL数据库请使用5.7.x版本,不同版本SQL语法有差异。如果你SQL技术过硬可以通过错误信息去修改出错的SQL,更推荐的做法是安装推荐版本的MySQL数据库。</p><h3 id="ip2region是啥玩意,打开怎么乱码?"><a href="#ip2region是啥玩意,打开怎么乱码?" class="headerlink" title="ip2region是啥玩意,打开怎么乱码?"></a>ip2region是啥玩意,打开怎么乱码?</h3><p>通过ip获取地址的开源软件数据库文件,不要直接打开。ip2region地址:<a href="https://github.com/lionsoul2014/ip2region" target="_blank" rel="noopener">https://github.com/lionsoul2014/ip2region</a>。</p><h2 id="项目缺陷"><a href="#项目缺陷" class="headerlink" title="项目缺陷"></a>项目缺陷</h2><ol><li><p>前端页面不支持移动端(不能自适应);</p></li><li><p>前端打包后vendor.js较大,通过nginx压缩后在591kb左右,在我的渣渣服务器(1核1G1M)下,访问时间大约为7 - 8秒左右:</p></li></ol><p><img src="img/febsvue/QQ截图20190409162930.png" alt="QQ截图20190409162930.png"></p><p>如果你的服务器带宽够大,或者是部署在公司局域网内的话,这个问题可以忽略。如果要在根源上解决这个问题个人觉得可以从这几个地方入手:</p><ul><li><p>Ant Design采用SVG格式的图标后,导致项目打包体积过大,可以参考官方issue的讨论:<a href="https://github.com/vueComponent/ant-design-vue/issues/325" target="_blank" rel="noopener">https://github.com/vueComponent/ant-design-vue/issues/325</a>和<a href="https://github.com/ant-design/ant-design/issues/12011" target="_blank" rel="noopener">https://github.com/ant-design/ant-design/issues/12011</a>;</p></li><li><p>可以将依赖通过CDN来加载,参考连接:<a href="https://blog.csdn.net/qq_35844177/article/details/78599064" target="_blank" rel="noopener">https://blog.csdn.net/qq_35844177/article/details/78599064</a>;</p></li><li><p>webpack打包配置可能存在可优化的地方。</p></li></ul><p>由于我才疏学浅,前端技能薄弱,所以没能够很好地解决这个问题。欢迎来自五湖四海的能人志士pull request来改善这个问题,感激不尽。</p><p><img src="img/febsvue/QQ图片20190409164403.jpg" alt="QQ图片20190409164403.jpg"></p><script>$(".post-body a").not(".thispage").addClass("ignore-href").attr("target","_blank")</script></div><div></div><div><div style="padding:10px 0;margin:20px auto;width:90%;text-align:center;color:#878787" id="reward-div"><div>请作者喝瓶肥宅水🥤</div><button id="rewardButton" style="margin-top:10px" disable="enable" onclick='var e=document.getElementById("QR");"none"===e.style.display?e.style.display="block":e.style.display="none"'><span style="height:46px;width:46px;line-height:46px;border-radius:50%;color:#fe5f55;font-weight:600;background:#ffd5be;border:none;box-shadow:0 4px 8px 0 rgba(255,213,190,.4)">¥</span></button><div id="QR" style="display:none"><div id="wechat" style="display:inline-block"><img id="wechat_qr" src="/img/wechat_pay.png" alt="MrBird WeChat Pay"></div><div id="alipay" style="display:inline-block"><img id="alipay_qr" src="/img/ali_pay.png" alt="MrBird Alipay"></div></div></div><style>#QR img{width:auto;margin:.8em 1em 0 1em}</style></div><div><ul class="post-copyright"><li class="post-copyright-author"><strong>本文作者:</strong> MrBird</li><li class="post-copyright-link"><strong>本文链接:</strong> <a href="http://mrbird.cc/FEBS-Vue-Document.html" title="FEBS-Vue文档">http://mrbird.cc/FEBS-Vue-Document.html</a></li><li class="post-copyright-license"><strong>版权声明: </strong>本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="external nofollow" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明出处!</li></ul></div><footer class="post-footer"><div class="post-tags" style="margin-bottom:1.3rem"><a href="/tags/FEBS/" rel="tag"># FEBS</a></div><div class="post-nav"><div class="post-nav-next post-nav-item"><a href="/Spring-Boot-WebFlux-CRUD.html" rel="next" title="Spring Boot WebFlux增删改查样例"><i class="fa fa-chevron-left"></i> Spring Boot WebFlux增删改查样例</a></div><span class="post-nav-divider"></span><div class="post-nav-prev post-nav-item"><a href="/Guava-Collection.html" rel="prev" title="Guava 集合操作">Guava 集合操作 <i class="fa fa-chevron-right"></i></a></div></div></footer></article><hr><div id="container"></div><div class="post-spread"><div id="comment-div"></div><style>.valine .vlist{margin-bottom:3rem}.valine .vwrap .vcontrol .col.col-60{text-align:left}.valine .vlist .vcard .vhead,.valine .vlist .vcard section .vfooter{text-align:left}.valine .vlist .vcard section{padding-bottom:.5rem!important}.vname{color:#6db33f!important}div#comment-div{margin-bottom:-8rem}.valine .vlist .vcard .vcontent .code,.valine .vlist .vcard .vcontent code,.valine .vlist .vcard .vcontent pre{background:#fbfbfb}.valine .vlist .vcard .vcontent a{color:#6db33f}.valine .vlist .vcard .vimg{border-radius:3px}.valine .vinfo{text-align:left}.valine .vbtn{border-radius:2px;padding:.3rem 1.25rem}.valine .vbtn:active,.valine .vbtn:hover{color:#6db33f;border-color:#6db33f;background-color:#fff}.valine .vwrap .vheader .vinput:focus{border-bottom-color:#6db33f}.valine .vlist .vcard .vcontent.expand:before{background:-webkit-gradient(linear,left top,left bottom,from(hsla(0,0%,100%,0)),to(hsla(0,0%,100%,.2)));background:linear-gradient(180deg,hsla(0,0%,100%,0),hsla(0,0%,100%,.2))}.valine .vlist .vcard .vcontent.expand:after{content:"点击展开";font-size:.4rem;text-align:right;left:-1rem;background:hsla(0,0%,100%,.2)}.valine .vlist .vcard section .vfooter .vat{color:#b3b3b3}.valine .vlist .vcard section .vfooter .vat:hover{color:#6db33f}.vcontent img{margin:0}.valine .info{display:none}</style><script type="text/javascript" src="/js/av.min.js"></script><script type="text/javascript" src="/js/Valine.min.js"></script><script type="text/javascript" src="/js/activate-power-mode.js"></script><script>POWERMODE.colorful=!0,POWERMODE.shake=!1,document.body.addEventListener("input",POWERMODE),new Valine({el:"#comment-div",notify:!1,verify:!0,appId:"SMcDFP1bN1jgb9Lo8JmtICHm-gzGzoHsz",appKey:"dH4nrUrt3V5XiJiI6Qyejnbi",placeholder:"",path:window.location.pathname,avatar:"monsterid",guest_info:["nick","mail","link"]});var chicken='<a href="#"><img src="https://image.uisdc.com/wp-content/uploads/2018/06/uisdc-chat-chicken.gif" class="checken"></a>';$("#comment-div").prepend(chicken)</script></div></div><script>var $bqinline=$("img[alt='bq-inline']");$bqinline.css({width:"2.3rem",height:"2.3rem",display:"inline","vertical-align":"text-bottom"})</script></div><div class="comments" id="comments"></div></div><aside id="sidebar" class="sidebar" onselectstart="return!1"><div class="sidebar-inner"><ul class="sidebar-nav motion-element"><li class="sidebar-nav-toc sidebar-nav-active" data-target="post-toc-wrap">Contents</li><li class="sidebar-nav-overview" data-target="site-overview">Site Preview</li></ul><section class="site-overview sidebar-panel"><div class="sidebar-sticky sticky"><div itemscope itemtype="https://mrbird.cc"><div class="author__header"><div class="author__avatar"><img src="/images/blogImage.jpg" class="author__avatar" alt="MrBird" itemprop="image"></div><div class="author__content"><div class="author__name" itemprop="name">MrBird</div><p class="author__bio" itemprop="description">A simple blog, code repository, just keep blogging</p></div><div class="author__count"><a href="/archives" class="ignore-href"><span class="count">14</span> <span>Archives</span> </a><a href="/tags" class="ignore-href"><span class="count">2</span> <span>Labels</span></a></div></div><div class="author__urls-wrapper"><ul class="author__urls social-icons"><li><a href="/" itemprop="url" class="ignore-href">🏠 Home</a></li><li><a href="/archives" itemprop="url" class="ignore-href">📦 Archives</a></li><li><a href="/tags" itemprop="url" class="ignore-href">🔖 Labels</a></li><li><a href="/friends" itemprop="url" class="ignore-href">👬 Friends</a></li><li><a href="javascript:;" class="popup-trigger animsition-link">🔍 Search</a></li></ul></div></div></div></section><section class="post-toc-wrap motion-element sidebar-panel sidebar-panel-active"><div class="post-toc"><div class="post-toc-content"><ol class="nav"><li class="nav-item nav-level-2"><a class="nav-link" href="#项目导入"><span class="nav-number">1.</span> <span class="nav-text">项目导入</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#JDK"><span class="nav-number">1.1.</span> <span class="nav-text">JDK</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#安装Node-js"><span class="nav-number">1.2.</span> <span class="nav-text">安装Node.js</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#安装yarn"><span class="nav-number">1.3.</span> <span class="nav-text">安装yarn</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#安装Redis"><span class="nav-number">1.4.</span> <span class="nav-text">安装Redis</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#安装MySQL"><span class="nav-number">1.5.</span> <span class="nav-text">安装MySQL</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#导入SQL"><span class="nav-number">1.6.</span> <span class="nav-text">导入SQL</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#导入后端项目"><span class="nav-number">1.7.</span> <span class="nav-text">导入后端项目</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#导入前端项目"><span class="nav-number">1.8.</span> <span class="nav-text">导入前端项目</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#项目部署"><span class="nav-number">2.</span> <span class="nav-text">项目部署</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#Vagrant创建CentOS"><span class="nav-number">2.1.</span> <span class="nav-text">Vagrant创建CentOS</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Java环境配置"><span class="nav-number">2.2.</span> <span class="nav-text">Java环境配置</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#安装Docker"><span class="nav-number">2.3.</span> <span class="nav-text">安装Docker</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Docker安装MySQL"><span class="nav-number">2.4.</span> <span class="nav-text">Docker安装MySQL</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Docker安装Redis"><span class="nav-number">2.5.</span> <span class="nav-text">Docker安装Redis</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Docker安装Nginx"><span class="nav-number">2.6.</span> <span class="nav-text">Docker安装Nginx</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#后端部署"><span class="nav-number">2.7.</span> <span class="nav-text">后端部署</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#前端部署"><span class="nav-number">2.8.</span> <span class="nav-text">前端部署</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#后端项目介绍"><span class="nav-number">3.</span> <span class="nav-text">后端项目介绍</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#项目结构"><span class="nav-number">3.1.</span> <span class="nav-text">项目结构</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#项目配置"><span class="nav-number">3.2.</span> <span class="nav-text">项目配置</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#RESTful风格"><span class="nav-number">3.3.</span> <span class="nav-text">RESTful风格</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#数据层介绍"><span class="nav-number">3.4.</span> <span class="nav-text">数据层介绍</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#登录逻辑"><span class="nav-number">3.5.</span> <span class="nav-text">登录逻辑</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Redis缓存使用"><span class="nav-number">3.6.</span> <span class="nav-text">Redis缓存使用</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#动态路由构建"><span class="nav-number">3.7.</span> <span class="nav-text">动态路由构建</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#权限控制"><span class="nav-number">3.8.</span> <span class="nav-text">权限控制</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#多数据源"><span class="nav-number">3.9.</span> <span class="nav-text">多数据源</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#代码生成"><span class="nav-number">3.10.</span> <span class="nav-text">代码生成</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Excel导入导出"><span class="nav-number">3.11.</span> <span class="nav-text">Excel导入导出</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#统一参数校验"><span class="nav-number">3.12.</span> <span class="nav-text">统一参数校验</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#SQL打印"><span class="nav-number">3.13.</span> <span class="nav-text">SQL打印</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#AOP记录操作日志"><span class="nav-number">3.14.</span> <span class="nav-text">AOP记录操作日志</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#接口限流"><span class="nav-number">3.15.</span> <span class="nav-text">接口限流</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Shiro教程"><span class="nav-number">3.16.</span> <span class="nav-text">Shiro教程</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Shiro如何整合JWT"><span class="nav-number">3.17.</span> <span class="nav-text">Shiro如何整合JWT</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#前端项目介绍"><span class="nav-number">4.</span> <span class="nav-text">前端项目介绍</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#项目结构-1"><span class="nav-number">4.1.</span> <span class="nav-text">项目结构</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#数据存储"><span class="nav-number">4.2.</span> <span class="nav-text">数据存储</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#路由导航守卫"><span class="nav-number">4.3.</span> <span class="nav-text">路由导航守卫</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#权限控制-1"><span class="nav-number">4.4.</span> <span class="nav-text">权限控制</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#Axios封装"><span class="nav-number">4.5.</span> <span class="nav-text">Axios封装</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#路径配置"><span class="nav-number">4.6.</span> <span class="nav-text">路径配置</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#第三方组件介绍"><span class="nav-number">4.7.</span> <span class="nav-text">第三方组件介绍</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#开发建议"><span class="nav-number">4.8.</span> <span class="nav-text">开发建议</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#开发示例"><span class="nav-number">5.</span> <span class="nav-text">开发示例</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#新建一个页面"><span class="nav-number">5.1.</span> <span class="nav-text">新建一个页面</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#如何新建一个多级菜单"><span class="nav-number">5.2.</span> <span class="nav-text">如何新建一个多级菜单</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#如何隐藏路由"><span class="nav-number">5.3.</span> <span class="nav-text">如何隐藏路由</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#如何分配权限"><span class="nav-number">5.4.</span> <span class="nav-text">如何分配权限</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#前端如何添加依赖"><span class="nav-number">5.5.</span> <span class="nav-text">前端如何添加依赖</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#如何处理排序"><span class="nav-number">5.6.</span> <span class="nav-text">如何处理排序</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#后端接口测试"><span class="nav-number">5.7.</span> <span class="nav-text">后端接口测试</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#常见问题"><span class="nav-number">6.</span> <span class="nav-text">常见问题</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#导入项目编译出错,代码是否不全?"><span class="nav-number">6.1.</span> <span class="nav-text">导入项目编译出错,代码是否不全?</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#导入SQL为何出错?"><span class="nav-number">6.2.</span> <span class="nav-text">导入SQL为何出错?</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ip2region是啥玩意,打开怎么乱码?"><span class="nav-number">6.3.</span> <span class="nav-text">ip2region是啥玩意,打开怎么乱码?</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#项目缺陷"><span class="nav-number">7.</span> <span class="nav-text">项目缺陷</span></a></li></ol></div></div></section></div><div id="asider-footer"><div id="links"><li><a href="https://love.mrbird.cc" target="_blank" itemprop="url" class="ignore-href"><i class="fa fa-fw fa-diamond" aria-hidden="true"></i></a></li><li><a href="https://cloud.mrbird.cn" target="_blank" itemprop="url" class="ignore-href"><i class="fa fa-fw fa-skyatlas" aria-hidden="true"></i></a></li><li><a href="https://gitee.com/mrbirdd" target="_blank" itemprop="sameAs" class="ignore-href"><i class="fa fa-fw fa-codepen" aria-hidden="true"></i></a></li><li><a href="https://github.com/wuyouzhuguli" target="_blank" itemprop="sameAs" class="ignore-href"><i class="fa fa-fw fa-github-alt" aria-hidden="true"></i></a></li></div><div id="author"></div><script type="text/javascript">var $smsheoschzd100dn="@ 2016 - "+(new Date).getFullYear()+" MrBird";document.getElementById("author").innerHTML=$smsheoschzd100dn</script><div><script type="text/javascript" src="/js/busuanzi.js"></script> UV <span class="busuanzi-value" id="busuanzi_value_site_uv" style="cursor:pointer" title="统计开始时间:2019年7月5日"></span> PV <span class="busuanzi-value" id="busuanzi_value_site_pv" style="cursor:pointer" title="统计开始时间:2019年7月5日"></span></div></div><script>function c__(){var o=sidebar_nav_toc.attr("class");o.indexOf("active")!=-1?footer.hide(300):footer.show(300)}var sidebar_nav_toc=$(".sidebar-nav-toc"),footer=$("#asider-footer");c__(),$(".sidebar-nav").on("click",function(){c__()})</script></aside></div></main><div class="back-to-top"><span id="scrollpercent"><span>0</span></span></div></div><script type="text/javascript">"[object Function]"!==Object.prototype.toString.call(window.Promise)&&(window.Promise=null)</script><script type="text/javascript" src="/lib/jquery/index.js?v=2.1.3"></script><script type="text/javascript" src="/lib/fastclick/lib/fastclick.min.js?v=1.0.6"></script><script type="text/javascript" src="/lib/jquery_lazyload/jquery.lazyload.js?v=1.9.7"></script><script type="text/javascript" src="/lib/velocity/velocity.min.js?v=1.2.1"></script><script type="text/javascript" src="/lib/velocity/velocity.ui.min.js?v=1.2.1"></script><script type="text/javascript" src="/js/src/utils.js?v=5.1.1"></script><script type="text/javascript" src="/js/src/motion.js?v=5.1.1"></script><script type="text/javascript" src="/js/src/scrollspy.js?v=5.1.1"></script><script type="text/javascript" src="/js/src/post-details.js?v=5.1.1"></script><script type="text/javascript" src="/js/src/bootstrap.js?v=5.1.1"></script><script type="text/javascript">function proceedsearch(){$("body").append('<div class="search-popup-overlay local-search-pop-overlay"></div>').css("overflow","hidden"),$(".search-popup-overlay").click(onPopupClose),$(".popup").toggle();var t=$("#local-search-input");t.attr("autocapitalize","none"),t.attr("autocorrect","off"),t.focus()}var isfetched=!1,isXml=!0,search_path="search.xml";0===search_path.length?search_path="search.xml":search_path.endsWith("json")&&(isXml=!1);var path="/"+search_path,onPopupClose=function(t){$(".popup").hide(),$("#local-search-input").val(""),$(".search-result-list").remove(),$("#no-result").remove(),$(".local-search-pop-overlay").remove(),$("body").css("overflow","")},searchFunc=function(t,e,o){"use strict";$("body").append('<div class="search-popup-overlay local-search-pop-overlay"><div id="search-loading-icon"><i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i></div></div>').css("overflow","hidden"),$("#search-loading-icon").css("margin","20% auto 0 auto").css("text-align","center"),$.ajax({url:t,dataType:isXml?"xml":"json",async:!0,success:function(t){isfetched=!0,$(".popup").detach().appendTo(".header-inner");var n=isXml?$("entry",t).map(function(){return{title:$("title",this).text(),content:$("content",this).text(),url:$("url",this).text()}}).get():t,r=document.getElementById(e),s=document.getElementById(o),a=function(){var t=r.value.trim().toLowerCase(),e=t.split(/[\s\-]+/);e.length>1&&e.push(t);var o=[];if(t.length>0&&n.forEach(function(n){function r(e,o,n,r){for(var s=r[r.length-1],a=s.position,i=s.word,l=[],h=0;a+i.length<=n&&0!=r.length;){i===t&&h++,l.push({position:a,length:i.length});var p=a+i.length;for(r.pop();0!=r.length&&(s=r[r.length-1],a=s.position,i=s.word,p>a);)r.pop()}return c+=h,{hits:l,start:o,end:n,searchTextCount:h}}function s(t,e){var o="",n=e.start;return e.hits.forEach(function(e){o+=t.substring(n,e.position);var r=e.position+e.length;o+='<b class="search-keyword">'+t.substring(e.position,r)+"</b>",n=r}),o+=t.substring(n,e.end)}var a=!1,i=0,c=0,l=n.title.trim(),h=l.toLowerCase(),p=n.content.trim().replace(/<[^>]+>/g,""),u=p.toLowerCase(),f=decodeURIComponent(n.url),d=[],g=[];if(""!=l&&(e.forEach(function(t){function e(t,e,o){var n=t.length;if(0===n)return[];var r=0,s=[],a=[];for(o||(e=e.toLowerCase(),t=t.toLowerCase());(s=e.indexOf(t,r))>-1;)a.push({position:s,word:t}),r=s+n;return a}d=d.concat(e(t,h,!1)),g=g.concat(e(t,u,!1))}),(d.length>0||g.length>0)&&(a=!0,i=d.length+g.length)),a){[d,g].forEach(function(t){t.sort(function(t,e){return e.position!==t.position?e.position-t.position:t.word.length-e.word.length})});var v=[];0!=d.length&&v.push(r(l,0,l.length,d));for(var C=[];0!=g.length;){var $=g[g.length-1],m=$.position,x=$.word,w=m-20,y=m+80;w<0&&(w=0),y<m+x.length&&(y=m+x.length),y>p.length&&(y=p.length),C.push(r(p,w,y,g))}C.sort(function(t,e){return t.searchTextCount!==e.searchTextCount?e.searchTextCount-t.searchTextCount:t.hits.length!==e.hits.length?e.hits.length-t.hits.length:t.start-e.start});var T=parseInt("1");T>=0&&(C=C.slice(0,T));var b="";b+=0!=v.length?"<li><a href='"+f+"' class='search-result-title'>"+s(l,v[0])+"</a>":"<li><a href='"+f+"' class='search-result-title'>"+l+"</a>",C.forEach(function(t){b+="<a href='"+f+'\'><p class="search-result">'+s(p,t)+"...</p></a>"}),b+="</li>",o.push({item:b,searchTextCount:c,hitCount:i,id:o.length})}}),1===e.length&&""===e[0])s.innerHTML='<div id="no-result"><i class="fa fa-search fa-5x" /></div>';else if(0===o.length)s.innerHTML='<div id="no-result"><i class="fa fa-frown-o fa-5x" /></div>';else{o.sort(function(t,e){return t.searchTextCount!==e.searchTextCount?e.searchTextCount-t.searchTextCount:t.hitCount!==e.hitCount?e.hitCount-t.hitCount:e.id-t.id});var a='<ul class="search-result-list">';o.forEach(function(t){a+=t.item}),a+="</ul>",s.innerHTML=a}};r.addEventListener("input",a),$(".local-search-pop-overlay").remove(),$("body").css("overflow",""),proceedsearch()}})};$(".popup-trigger").click(function(t){t.stopPropagation(),isfetched===!1?searchFunc(path,"local-search-input","local-search-result"):proceedsearch()}),$(".popup-btn-close").click(onPopupClose),$(".popup").click(function(t){t.stopPropagation()}),$(document).on("keyup",function(t){var e=27===t.which&&$(".search-popup").is(":visible");e&&onPopupClose()})</script></body><script>$(function(){$("a").not(".nav-link,.cloud-tie-join-count,.ignore-href,.jstree-anchor").addClass("animsition-link")});var burst1=new mojs.Burst({left:0,top:0,radius:{5:40},children:{shape:"circle",fill:["red","cyan","orange"],fillOpacity:.8,radiusX:3.5,radiusY:3.5}});document.addEventListener("click",function(a){null==a.target.href&&"footer"!=a.target.className&&"copyright"!=a.target.className&&"author__urls social-icons"!=a.target.className&&"author__avatar"!=a.target.className&&"sidebar sidebar-active"!=a.target.className&&burst1.tune({x:a.pageX,y:a.pageY}).generate().replay()})</script><script type="text/javascript" src="/js/message.js"></script></html><!-- rebuild by neat -->