项目开发流程(前端 · 后端)(终版)

这是一份面向实战的终版手册,包含可直接复制运行的前端(Vite + Vue3 + Element Plus)和后端(Spring Boot + MyBatis + Spring Security + Redis)示例与配置。 适合需要把“能跑的原型”工程化的开发者:登录鉴权、JWT + Redis 会话、增删改查、分页、权限切面、缓存策略等常见模块都有实战代码。

本版在初版基础上精简并补充了生产级的注意点(序列化、异常处理、缓存回退、数据隔离等),更容易拿来即用。 阅读建议:先按“快速启动”把前后端连通,再按模块查阅需要的实现与代码片段。

前端开始:

由于我主要研究的是 Java 后端方面的开发,但目前开发流程要求程序员必须同时精通前端和后端。所以,在这本手册中,后端方面的手册,我会简略写。前端方面的手册,我会详细写。

一、新建 SpringBoot 项目步骤

Step1 用 Vite 脚手架工具创建 Vue 项目

在需要创建项目目录的 Dos 窗口输入以下内容,通过 vite 获取最新版本的 Vue 项目结构:

npm create vite@latest

Step2 键入该 Vue 项目的名称

继续在原 Dos 窗口输入该项目名称,如:

my-vue-project

Step3 选择 Vue 项目

Vite 工具还可以创建除了 Vue 项目之外的其他项目,这里选择 Vue 项目。

Step4 选择项目语言

TypeScriptJavaScript 等语言供选择,推荐选择 TypeScript

Step5 安装项目依赖

此时,Vue 的项目已经创建完成。

cd 进入刚刚创建的项目,在新的目录输入以下命令:

npm install

Step6 启动项目

继续在项目里输入以下命令以启动此 Vue 项目:

npm run dev

或者在 Idea 工具中打开此项目,点击项目根目录下的 "dev": "vite",,这一行左边绿色箭头以启动此 Vue 项目。

Step7 使用浏览器浏览项目

浏览器访问以下默认网址,以浏览该项目:

http://localhost:5173/

至此,Vue 项目已经创建完成。下面几个步骤都是更加详细的步骤。

Step8 添加常见依赖

继续在原 Dos 窗口输入以下命令以安装依赖:(注意,在这个 vue 项目的目录下,即该目录下一定有之前经过 npm install 指令生成的 node_modules 文件夹)

  • element-plus:(饿了么 plus 组件)

    npm install element-plus --save
  • @element-plus/icons-vue:(饿了么 plus icons 组件)

    npm install @element-plus/icons-vue --save
  • vue-router:(路由组件)

    npm install vue-router --save
  • axios:(axios 异步请求)

    npm install axios --save
  • xxx:

最后:

编辑 ~\src\main.js 文件中的所有内容,变成如下内容:

// 从 vue 框架,导入 createApp 函数
import { createApp } from 'vue'

// 从 ./App.vue 页面导入 App 组件(一般文件名叫什么,组件名也叫什么)
import App from './App.vue'
let app = createApp(App)

// 从 element-plus 中导入 ElementPlus 组件
import ElementPlus from 'element-plus'
// 导入 element-plus 的 CSS 样式,不需要 from 子句
import 'element-plus/dist/index.css'

// 导入 icons
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
}

// 从 router.js 中导入 router 组件
import router from "./router/router.js";
app.use(router)

// element-plus 的国际化,即将 element-plus 默认的语言改成中文
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus,{
   locale:zhCn,
})

// 利用上面所导入的 createApp 函数,创建一个 vue 应用,mount 是挂载到 #app 地方
app.mount('#app')

Step9 调整项目文件

  1. 可删除 ~\src\style.css

  2. 可删除 ~\src\components 目录及里面的所有文件

  3. 编辑 ~\index.html 文件,变成如下内容:

    <!doctype html>
    <html lang="en">
     <head>
       <meta charset="UTF-8" />
       <link rel="icon" type="image/svg+xml" href="/vite.svg" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <!--这里可以修改网站首页的标题-->
       <title>dlyk 管理系统</title>
     </head>
     <body>
       <div id="app"></div>
       <script type="module" src="/src/main.js"></script>
     </body>
     <style>
         /* 将网页的边距设置为 0*/
         body {
             margin: 0;
        }
     </style>
    </html>
  4. 编辑 ~\src\App.vue 文件中的所有内容,变成如下内容:

    <template>
     <!--渲染路由地址所对应的页面组件-->
     <router-view/>
    </template>
  5. 编辑 ~/vite.config.js 文件中的所有内容,变成如下内容:

    import {defineConfig} from 'vite'
    import vue from '@vitejs/plugin-vue'

    // https://vite.dev/config/
    export default defineConfig({
     plugins: [vue()],
     server: {
       // 设置访问的 ip 地址
       host: '0.0.0.0',
       // 设置前端 Vue 服务启动端口
       port: 8080,
       // 设置为 true 代表服务启动时自动打开浏览器
       open: true,
    }
    })
  6. 创建 ~/src/view/LoginVue.vue

    这里的 view 文件夹需额外创建。

    文件内容为:

    <template>
     <h1>Hello,这里是系统的首页!</h1>
    </template>

    <script>
    import {defineComponent} from 'vue'
    export default defineComponent({
     name: "LoginVue",
    })
    </script>

    <style scoped>
    </style>
  7. 创建 ~/src/router/router.js

    这里的 router 文件夹需额外创建。

    // 从 vue-router 中,导入 createRouter, createWebHistory 函数
    import {createRouter, createWebHistory} from "vue-router";

    // 定义变量
    let router = createRouter({
     // 路由历史
     history: createWebHistory(),

     // 配置路由,是一个数组,里面可以配置多个路由
     routes: [
      {
         // 路由路径,这里设置为"/",代表项目先进入以下vue页面
         path: "/",
         // 路由路径所对应的页面(此文件目录为:~/src/view/LoginVue.vue)
         component: () => import("../view/LoginVue.vue"),
      },
       // {
       //   // 路由路径
       //   path: "/dashboard",
       //   // 路由路径所对应的页面(此文件目录为:~/src/view/DashboardView.vue)
       //   component: () => import("../view/DashboardView.vue"),
       // },
       // 这里添加之后的路由,格式如上,如{},{},在大括号里写具体的路径对应
    ],
    })
    // 导出创建的路由对象
    export default router;

    仅可修改里面的routes数组里面的内容,每增加一个页面,就新增一个路由

  8. 创建 ~/src/util/util.js

    这里的 util 文件夹需额外创建。

    文件内容为:

    import {ElMessage, ElMessageBox} from "element-plus";

    /**
    * 消息提示工具方法
    * @param msg 消息框的提示信息
    * @param type 消息框的类型,可选 "error"|"success"|"warning"|"primary"
    */
    export function messageTip(msg,type){
       // messageTip 函数
       ElMessage({
           // true 代表提示信息可被关闭
           showClose: true,
           // true 代表显示居中
           center: true,
           // 设置提示信息的消失时间(单位:ms)
           duration: 3000,
           // 示例 msg 的值:'登录成功,欢迎回来!',
           message: msg,
           // 示例 type 的值:可选 "error"|"success"|"warning"|"primary"
           type: type,
      })
    }

    /**
    * 返回存储在 Session Storage 或 Local Storage 中的 token(jwt) 名字
    * @returns {string}
    */
    export function getTokenName(){
       return "dlyk_token";
    }

    /**
    * 从本地存储和会话存储中移除 token。
    * 此函数调用 getTokenName() 来获取 token 的键名,然后删除两种存储(localStorage 和 sessionStorage)中的对应条目。
    *
    * @return {void} 此函数不返回任何值
    */
    export function removeToken(){
       window.localStorage.removeItem(getTokenName());
       window.sessionStorage.removeItem(getTokenName());
    }

    /**
    * 消息确认函数
    * @param msg 消息信息详情
    * @param title 消息标题
    * @returns {Promise<MessageBoxData>}
    */
    export function messageConfirm(msg,title){
       return ElMessageBox.confirm(
           msg,
           title,
          {
               confirmButtonText: '确认',
               cancelButtonText: '取消',
               type: 'warning',
          }
      )
    }
  9. 创建 ~/src/http/HttpRequest.js

    这里的 http 文件夹需额外创建。

    文件内容为:

    import axios from "axios";
    import {getTokenName, messageConfirm, messageTip, removeToken} from "../util/util.js";

    // 定义后端接口地址的前缀
    axios.defaults.baseURL = "http://localhost:8089";

    export function doGet(url, params) {
       return axios({
           method: 'get',
           url: url,
           // params 参数形式:{name: "张三", age: 22}
           params: params,
           dataType: "json"
      })
    }

    export function doPost(url, data) {
       return axios({
           method: 'post',
           url: url,
           data: data,
           dataType: "json"
      })
    }

    export function doPut(url, data) {
       return  axios({
           method: 'put',
           url: url,
           data: data,
           dataType: "json"
      })
    }

    export function doDelete(url, params) {
       return axios({
           method: 'delete',
           url: url,
           params: params,
           dataType: "json"
      })
    }

    // 添加请求拦截器
    axios.interceptors.request.use((config) => {
       // 在发送请求之前做些什么,在请求头中放一个token(jwt),传给后端接口
       let token = window.sessionStorage.getItem(getTokenName());
       if (!token) { //前面加了一个!,表示token不存在,token是空的,token没有值,这个意思
           token = window.localStorage.getItem(getTokenName());
           if (token) {
               config.headers['rememberMe'] = true;
          }
      }
       if (token) { // 表示 token 存在,token不是空的,token有值,这个意思
           config.headers['Authorization'] = token;
      }
       return config;
    }, (error) => {
       // 对请求错误做些什么
       return Promise.reject(error);
    });

    // 添加响应拦截器
    axios.interceptors.response.use((response) => {
       // 2xx 范围内的状态码都会触发该函数。
       // 对响应数据做点什么,拦截 token 验证的结果,进行相应的提示和页面跳转
       if (response.data.code > 900) { // code 大于900说明是token验证未通过
           //给前端用户提示,并且跳转页面
           messageConfirm(response.data.msg + ",是否重新去登录?").then(() => { // 若点击"确定"按钮,走这个
               // 既然后端验证 token 未通过,token 是非法的,删除 token 先
               removeToken();
               // 跳到登录页
               window.location.href = ("/");
          })
               // 若点击"取消"按钮,走这个
              .catch(() => {
                   messageTip("已取消去登录!", "warning")
              })
           return;
      }
       return response;
    }, function (error) {
       // 超出 2xx 范围的状态码都会触发该函数。
       // 对响应错误做点什么
       return Promise.reject(error);
    });

二、一个登录的例子

需求:布置好网页的首页登录布局,要有字段的检测功能,免密登录功能。

Step1 在官网找合适的表单组件

我找的是:Form 表单 | Element Plus

Step2 找到想要的复制到自己的程序中

复制所有可以用到的组件,组件都是在 el-xxx 中的。我复制的是这样的:

<el-form :model="form" label-width="auto" style="max-width: 600px">
 <el-form-item label="Activity name">
   <el-input v-model="form.name" />
 </el-form-item>
 <el-form-item label="Activity type">
   <el-checkbox-group v-model="form.type">
     <el-checkbox value="Online activities" name="type">
      Online activities
     </el-checkbox>
     <el-checkbox value="Promotion activities" name="type">
      Promotion activities
     </el-checkbox>
     <el-checkbox value="Offline activities" name="type">
      Offline activities
 </el-checkbox>
     <el-checkbox value="Simple brand exposure" name="type">
Simple brand exposure
     </el-checkbox>
   </el-checkbox-group>
 </el-form-item>
 <el-form-item>
   <el-button type="primary" @click="onSubmit">Create</el-button>
   <el-button>Cancel</el-button>
 </el-form-item>
</el-form>

Step3 修改成自己所需要的

<el-form :model="loginForm">
 <!--表单的标题-->
 <el-form-item class="form-title">
   <h2>系统登录</h2>
 </el-form-item>

 <!--表单的账号-->
 <el-form-item label="账号">
   <el-input v-model="loginForm.loginAct" />
 </el-form-item>

 <!--表单的密码-->
 <el-form-item label="密码">
   <!--为该字段添加密码类型-->
   <el-input type="password" v-model="loginForm.loginPwd" />
 </el-form-item>

 <!--表单的注册按钮-->
 <el-form-item>
   <!--添加按钮点击函数:login-->
   <el-button type="primary" @click="login" class="form-button">登录</el-button>
 </el-form-item>

 <!--表单的记住我选项-->
 <el-form-item>
   <el-checkbox v-model="loginForm.rememberMe">
    记住我
   </el-checkbox>
 </el-form-item>
</el-form>
.el-form{
 width: 20%;
 margin: auto;
}
.form-title{
 margin-top: 200px;
}
.form-button{
 width: 100%;
}

Step4 注册 model

在 js 中注册所有提到的 model

<script>
import {defineComponent} from 'vue'
export default defineComponent({
 name: "LoginVue",
 // 所有的变量都需要注册在 data 里
 data(){
   return {
     // loginForm 是对象,所以注册为 {}
     loginForm: {},
     // loginAct 是字符串,所以注册为 ""
     loginAct: "",
     // loginPwd 是字符串,所以注册为 ""
     loginPwd: "",
     // rememberMe 是布尔类型,所以注册为 false
     rememberMe: false,
  }
},
 // 所有的方法都需要注册在 methods 里
 methods: {
   // 登录函数
   login() {
  }
}
})
</script>

Step5 添加表单的前端验证

  1. 为表单添加规则字段 :rules

  2. 为字段添加属性字段 prop

  3. 注册上述两步的字段

  4. 添加详细的验证规则

<!--为需要添加验证的表单,添加 rules-->
<el-form :model="loginForm" :rules="loginRules">
 <el-form-item class="form-title">
<h2>系统登录</h2>
 </el-form-item>

 <!--为需要添加验证的账号字段,加上 prop 属性,值为这个字段的字段名-->
 <el-form-item label="账号" prop="loginAct">
<el-input v-model="loginForm.loginAct" />
 </el-form-item>

 <!--为需要添加验证的账号字段,加上 prop 属性,值为这个字段的字段名-->
 <el-form-item label="密码" prop="loginPwd">
<el-input type="password" v-model="loginForm.loginPwd" />
 </el-form-item>

 <el-form-item>
<el-button type="primary" @click="login" class="form-button">登录</el-button>
 </el-form-item>

 <el-form-item>
<el-checkbox v-model="loginForm.rememberMe">
记住我
   </el-checkbox>
 </el-form-item>
</el-form>
<script>
import {defineComponent} from 'vue'
export default defineComponent({
 name: "LoginVue",
 data(){
   return {
     loginForm: {},
     loginAct: "",
     loginPwd: "",
     rememberMe: false,
     // loginRules 是对象,所以注册为 {}
     loginRules: {
       // 定义 loginAct 的规则,规则可以有多个,所以是数组,用 []
       loginAct: [
         // 添加账号不能为空的验证
        {required: true, message: '请输入账号!', trigger: 'blur'},
      ],
       // 定义 loginPwd 的规则,规则可以有多个,所以是数组,用 []
       loginPwd: [
         // 添加密码不能为空的验证
        {required: true, message: '请输入密码!', trigger: 'blur'},
         // 添加密码长度的验证
        {min: 6, max: 16, message: '密码的长度在 6-16 之间!', trigger: 'blur'},
      ],
    },
  }
},
 methods: {
   login() {
  }
}
})
</script>

Step6 添加表单的提交验证

  1. 为需要添加登录验证的表单,添加 ref

  2. 在提交方法中验证输入框的合法性(共用上一步写的验证)

  3. 选择数据,发送请求

  4. 根据请求的响应,做出不同的选择......

  5. 若响应信息为做出登录请求,则进入系统主页,这里要入路由模块

<template>
 <!--为需要添加登录验证的表单,添加 ref-->
 <el-form :model="loginForm" :rules="loginRules" ref="loginRefForm">
   <el-form-item class="form-title">
     <h2>系统登录</h2>
   </el-form-item>

   <el-form-item label="账号" prop="loginAct">
     <el-input v-model="loginForm.loginAct" />
   </el-form-item>

   <el-form-item label="密码" prop="loginPwd">
     <el-input type="password" v-model="loginForm.loginPwd" />
   </el-form-item>

   <el-form-item>
     <el-button type="primary" @click="login" class="form-button">登录</el-button>
   </el-form-item>

   <el-form-item>
       <el-checkbox v-model="loginForm.rememberMe">
        记住我
       </el-checkbox>
   </el-form-item>
 </el-form>
</template>
<script>
import {defineComponent} from 'vue'
// 自动导入依赖
import {doPost} from "../http/HttpRequest.js";
import {getTokenName, messageTip, removeToken} from "../util/util.js";
export default defineComponent({
 name: "LoginVue",
 data(){
   return {
     loginForm: {},
     loginAct: "",
     loginPwd: "",
     rememberMe: false,
     loginRules: {
       loginAct: [
        {required: true, message: '请输入账号!', trigger: 'blur'},
      ],
       loginPwd: [
        {required: true, message: '请输入密码!', trigger: 'blur'},
        {min: 6, max: 16, message: '密码的长度在 6-16 之间!', trigger: 'blur'},
      ],
    },
  }
},
 methods: {
   login() {
     // 提交前验证输入框的合法性
     this.$refs.loginRefForm.validate((isValid) => {
       if (isValid) {
         // 运行到这里说明验证通过
         // 使用 formData 上传数据
         let formData = new FormData();
         // 以键值对的形式写入数据
         formData.append('loginAct', this.loginForm.loginAct);
         formData.append('loginPwd', this.loginForm.loginPwd);
         formData.append('rememberMe', this.loginForm.rememberMe);
         doPost("/api/login",formData).then((resp) =>{
           // 看看响应的形式是怎么样的
           // console.log(resp);
           if (resp.data.code === 200){
             // 删除历史 token
             removeToken();
             messageTip("登录成功,欢迎回来!", "success");
             // 存储 jwt
             if (this.loginForm.rememberMe === true){
               window.localStorage.setItem(getTokenName(), resp.data.data);
            }else {
               window.sessionStorage.setItem(getTokenName(), resp.data.data);
            }
             window.location.href = "/dashboard";
          }else {
             messageTip("登录失败,账号或密码错误!", "error");
          }
        });
      }
    })
  },
}
})
</script>
import {createRouter, createWebHistory} from "vue-router";

let router = createRouter({
 history: createWebHistory(),

 routes: [
  {
     path: "/",
     component: () => import("../view/LoginVue.vue"),
  },
   // 在这里添加要跳转的页面的路由,一个完整的路由就是由下面的 {},这样在一块的,里面是 path 和 component
  {
     // 路由路径
     path: "/dashboard",
     // 路由路径所对应的页面(此文件目录为:~/src/view/DashboardView.vue)
     component: () => import("../view/DashboardView.vue"),
  },
],
})
export default router;

Step7 实现免密登录

当用户勾选了记住我之后,7天之内,用户再次登录项目首页的登录页时,自动跳转登录成功之后的页面。

在 js 中加入 mounted 这个钩子函数,触发自动登录方法,如下:

// 页面渲染完 dom 元素后会触发调用该函数(函数钩子)
mounted() {
this.freeLogin();
},
freeLogin(){
// 检查有没有选择记住我,(通过检查是否有 token 来检查之前是否有选择记住我)
let token = window.localStorage.getItem(getTokenName());
// 判断 token 是否有值
if (token){
doGet("/api/login/free",{}).then(resp =>{
if (resp.data.code === 200){
// token 验证通过,可以免登录
window.location.href = "/dashboard";
}else {
// token 错误
}
})
}
},

三、一个布局例子

需求:建立一个网站的首页(dashboard),这个首页有侧边的导航栏,导航栏可以放大和缩小,导航栏有一级、二级之分,点击可以前往目标页面,但总体框架还是这个首页的框架。顶部有一行导航条,用于显示必要信息。底部也有一行,用于显示版权信息,中间区域为主显示区域。

Step1 使用合适的布局组件

  1. 在官网找合适的布局组件

    我找的是:Container 布局容器 | Element Plus,具体是里面的:

    image-20250901143718339

  2. 找到想要的复制到自己的程序中

    复制所有可以用到的组件,组件都是在 el-xxx 中的。我复制的是这样的:

    <el-container>
     <el-aside width="200px">Aside</el-aside>
     <el-container>
       <el-header>Header</el-header>
       <el-main>Main</el-main>
       <el-footer>Footer</el-footer>
     </el-container>
    </el-container>
  3. 修改成自己所需要的

    先写好一切写死的东西

    <el-container>
     <!--左侧导航栏开始-->
     <el-aside width="200px">
      Aside
     </el-aside>
     <!--左侧导航栏结束-->
     <!--右侧三栏开始-->
     <el-container class="right-side">
       <!--上导航条开始-->
       <el-header>
        Header
       </el-header>
       <!--上导航条结束-->
       <!--主区域开始-->
       <el-main>
        Main
       </el-main>
       <!--主区域结束-->
       <!--底部版权信息条开始-->
       <el-footer>
        Copyright © 2025 dlyk All rights reserved. 站点地图 浙ICP备xxxx号
       </el-footer>
       <!--底部版权信息条结束-->
     </el-container>
     <!--右侧三栏结束-->
    </el-container>
    /* 设置上导航条的样式*/
    .el-header{
    /* 设置背景颜色*/
    background-color: azure;
    /* 设置高度*/
    height: 35px;
    /* 设置行高与高度一致,即可上下居中*/
    line-height: 35px;
    }
    /* 底部版权信息条的样式*/
    .el-footer{
    /* 设置背景颜色*/
    background-color: azure;
    /* 设置高度*/
    height: 35px;
    /* 设置行高与高度一致,即可上下居中*/
    line-height: 35px;
    /* 设置文本左右居中*/
    text-align: center;
    }
    /* 设置右侧三栏的样式*/
    .right-side{
    /* 设置高度是撑满*/
    height: calc(100vh);
    }

    image-20250901152654243

Step2 使用合适的侧导航栏目组件

  1. 在官网找合适的侧导航栏组件

    我找的是:Menu 菜单 | Element Plus,具体是这样的:

    image-20250901153126808

  2. 找到想要的复制到自己的程序中

    复制所有可以用到的组件,组件都是在 el-xxx 中的。我复制的是这样的:

    <el-menu
       default-active="2"
       class="el-menu-vertical-demo"
       @open="handleOpen"
       @close="handleClose"
    >
     <el-sub-menu index="1">
       <template #title>
         <el-icon><location /></el-icon>
         <span>Navigator One</span>
       </template>
       <el-menu-item-group title="Group One">
         <el-menu-item index="1-1">item one</el-menu-item>
         <el-menu-item index="1-2">item two</el-menu-item>
       </el-menu-item-group>
       <el-menu-item-group title="Group Two">
         <el-menu-item index="1-3">item three</el-menu-item>
       </el-menu-item-group>
       <el-sub-menu index="1-4">
         <template #title>item four</template>
         <el-menu-item index="1-4-1">item one</el-menu-item>
       </el-sub-menu>
     </el-sub-menu>
     <el-menu-item index="2">
       <el-icon><icon-menu /></el-icon>
       <span>Navigator Two</span>
     </el-menu-item>
     <el-menu-item index="3" disabled>
       <el-icon><document /></el-icon>
       <span>Navigator Three</span>
     </el-menu-item>
     <el-menu-item index="4">
       <el-icon><setting /></el-icon>
       <span>Navigator Four</span>
     </el-menu-item>
    </el-menu>
  3. 修改成自己所需要的侧导航栏

    • 先搭建好基础的侧导航栏架子

      <el-menu
         default-active="2"
         class="el-menu-vertical-demo">
       <el-sub-menu index="1">
         <template #title>
           <el-icon><location /></el-icon>
           <span>市场活动</span>
         </template>
           <el-menu-item index="1-1">市场活动</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="2">
         <template #title>
           <el-icon><location /></el-icon>
           <span>线索管理</span>
         </template>
         <el-menu-item index="2-1">item one</el-menu-item>
         <el-menu-item index="2-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="3">
         <template #title>
           <el-icon><location /></el-icon>
           <span>客户管理</span>
         </template>
         <el-menu-item index="3-1">item one</el-menu-item>
         <el-menu-item index="3-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="4">
         <template #title>
           <el-icon><location /></el-icon>
           <span>交易管理</span>
         </template>
         <el-menu-item index="4-1">item one</el-menu-item>
         <el-menu-item index="4-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="5">
         <template #title>
           <el-icon><location /></el-icon>
           <span>产品管理</span>
         </template>
         <el-menu-item index="5-1">item one</el-menu-item>
         <el-menu-item index="5-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="6">
         <template #title>
           <el-icon><location /></el-icon>
           <span>字典管理</span>
         </template>
         <el-menu-item index="6-1">item one</el-menu-item>
         <el-menu-item index="6-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="7">
         <template #title>
           <el-icon><location /></el-icon>
           <span>用户管理</span>
         </template>
         <el-menu-item index="7-1">item one</el-menu-item>
         <el-menu-item index="7-2">item two</el-menu-item>
       </el-sub-menu>
       <el-sub-menu index="8">
         <template #title>
           <el-icon><location /></el-icon>
           <span>系统管理</span>
         </template>
         <el-menu-item index="8-1">item one</el-menu-item>
         <el-menu-item index="8-2">item two</el-menu-item>
       </el-sub-menu>
      </el-menu>
    • 在对应地方加入图标

      在这里找合适的图标:Icon 图标 | Element Plus,粘贴进程序即可,idea 能自动一键导入。选择图标的时候适当整理下文字。

      <el-menu
         default-active="2"
         class="el-menu-vertical-demo">
       <!--市场活动开始-->
       <el-sub-menu index="1">
         <template #title>
           <el-icon><DataLine /></el-icon>
           <span>市场活动</span>
         </template>
           <el-menu-item index="1-1">
             <el-icon><Management /></el-icon>
            市场管理
           </el-menu-item>
       </el-sub-menu>
       <!--市场活动结束-->
       <!--线索管理开始-->
       <el-sub-menu index="2">
         <template #title>
           <el-icon><Key /></el-icon>
           <span>线索管理</span>
         </template>
         <el-menu-item index="2-1">
           <el-icon><Management /></el-icon>
          线索管理
         </el-menu-item>
       </el-sub-menu>
       <!--线索管理结束-->
       <!--客户管理开始-->
       <el-sub-menu index="3">
         <template #title>
           <el-icon><User /></el-icon>
           <span>客户管理</span>
         </template>
         <el-menu-item index="3-1">
           <el-icon><Management /></el-icon>
          客户管理
         </el-menu-item>
       </el-sub-menu>
       <!--客户管理结束-->
       <!--交易管理开始-->
       <el-sub-menu index="4">
         <template #title>
           <el-icon><Tickets /></el-icon>
           <span>交易管理</span>
         </template>
         <el-menu-item index="4-1">
           <el-icon><Management /></el-icon>
          交易管理
         </el-menu-item>
       </el-sub-menu>
       <!--交易管理结束-->
       <!--产品管理开始-->
       <el-sub-menu index="5">
         <template #title>
           <el-icon><Box /></el-icon>
           <span>产品管理</span>
         </template>
         <el-menu-item index="5-1">
           <el-icon><Management /></el-icon>
          产品管理
         </el-menu-item>
       </el-sub-menu>
       <!--产品管理结束-->
       <!--字典管理开始-->
       <el-sub-menu index="6">
         <template #title>
           <el-icon><Reading /></el-icon>
           <span>字典管理</span>
         </template>
         <el-menu-item index="6-1">
           <el-icon><Management /></el-icon>
          字典管理
         </el-menu-item>
       </el-sub-menu>
       <!--字典管理结束-->
       <!--用户管理开始-->
       <el-sub-menu index="7">
         <template #title>
           <el-icon><UserFilled /></el-icon>
           <span>用户管理</span>
         </template>
         <el-menu-item index="7-1">
           <el-icon><Management /></el-icon>
          用户管理
         </el-menu-item>
       </el-sub-menu>
       <!--用户管理结束-->
       <!--系统管理开始-->
       <el-sub-menu index="8">
         <template #title>
           <el-icon><Setting /></el-icon>
           <span>系统管理</span>
         </template>
         <el-menu-item index="8-1">
           <el-icon><Management /></el-icon>
          系统管理
         </el-menu-item>
       </el-sub-menu>
       <!--系统管理结束-->
      </el-menu>
    • 再加上菜单标题,再调整下小细节

      <el-menu> 之上加个菜单标题:

      <div class="menu-title">@dlyk 管理系统</div>
      /* 设置菜单标题的样式*/
      .menu-title{
      /* 设置背景颜色*/
      background-color: azure;
      /* 设置高度和右边一样*/
      height: 35px;
      /* 设置文本左右居中*/
      text-align: center;
      /* 设置行高与高度一致,即可上下居中*/
      line-height: 35px;
      }
      /* 设置菜单的样式*/
      .el-menu{
      /* 发现菜单天然有个 1px 的右边框,设置无边框*/
      border-right: 0;
      }
    • 添加功能:当展开一个菜单项时,关闭其他菜单项。

      只需在 <el-menu> 里加入 :unique-opened="true" 属性即可。如:

      <el-menu
      default-active="2"
      class="el-menu-vertical-demo"
      :unique-opened="true">
    • 添加功能:可以折叠我们的侧导航栏,仅显示图标,不显示文字,有更多的区域显示右边的主体。同时,也能恢复原样。

      • el-menu 添加折叠选项

        collapse 为 true,菜单就是折叠的;collapse 为 false,菜单就是扩展的,即正常状态。我们将这个属性的值定义为变量即可。

        <el-menu
        default-active="2"
        class="el-menu-vertical-demo"
        :unique-opened="true"
        :collapse="isCollapse">
        data(){
         return {
           isCollapse: false,
        }
        },
      • 在合适的地方加入折叠按钮图标

        设置按钮的点击函数,通过这个函数来控制侧导航栏的折叠与否。

        <el-icon class="fold-icon" @click="foldLeftSide"><Fold /></el-icon>
        /* 设置折叠图标的样式*/
        .fold-icon{
         /* 使鼠标悬停图标时,鼠标变成手*/
         cursor: pointer;
        }
        methods: {
         // 折叠左侧菜单的方法
         foldLeftSide(){
           this.isCollapse = !this.isCollapse;
        }
        },
      • 将整个 el-aside ,即侧导航栏进行折叠

        之前的折叠,仅仅是将 el-aside 里的 el-menu 折叠。

        同样的方法,将 el-asidewidth 设置成变量,如:

        <el-aside :width="isCollapse?'64px':'200px'">

        发现折叠动画很卡,将动画关闭,即在 el-menu 里添加 collapse-transition 属性为 false,如:

        <el-menu
         default-active="2"
         class="el-menu-vertical-demo"
         :unique-opened="true"
         :collapse="isCollapse"
         :collapse-transition="false">

Step3 完成顶部导航条设计

顶部导航条的设计只有一个要求,就是,显示当前用户的用户姓名,然后点击可以查看我的资料、修改密码、退出登录。

查看我的资料和修改密码暂时不实现。先实现动态显示用户姓名和退出登录功能。

  1. 设计下拉框

    选择一个合适的下拉栏,并作出自己所需要的效果,如:

    <el-dropdown :hide-on-click="false">
     <span class="el-dropdown-link">
      此处动态获取用户姓名
       <el-icon class="el-icon--right"><arrow-down /></el-icon>
     </span>
     <template #dropdown>
       <el-dropdown-menu>
         <el-dropdown-item>我的资料</el-dropdown-item>
         <el-dropdown-item>修改密码</el-dropdown-item>
         <el-dropdown-item divided>退出登录</el-dropdown-item>
       </el-dropdown-menu>
     </template>
    </el-dropdown>
    /* 设置用户姓名下拉列表的样式*/
    .el-dropdown{
     /* 往右漂移,即放在最右边*/
     float: right;
     /* 设置行高与高度一致,即可上下居中*/
     line-height: 35px;
    }
  2. 动态显示用户姓名功能实现

    <el-dropdown :hide-on-click="false">
     <span class="el-dropdown-link">
       <!--此处动态获取用户姓名-->
      {{user.name}}
       <el-icon class="el-icon--right"><arrow-down /></el-icon>
     </span>
     <template #dropdown>
       <el-dropdown-menu>
         <el-dropdown-item>我的资料</el-dropdown-item>
         <el-dropdown-item>修改密码</el-dropdown-item>
         <el-dropdown-item divided>退出登录</el-dropdown-item>
       </el-dropdown-menu>
     </template>
    </el-dropdown>
    data(){
     return {
       // 默认 collapse 为 false,即非展开状态
       isCollapse: false,
       user: {
         name: "",
      },
    }
    },
    mounted(){
     // 在这里加载当前用户
     // 调用 methods 里写好的方法
     this.loadLoginUser();
    },
    methods: {
    // 折叠左侧菜单的方法
    foldLeftSide(){
    this.isCollapse = !this.isCollapse;
    },
    loadLoginUser(){
    // 发送给后端请求,请求当前登录用户
    doGet("/api/login/info",{}).then((resp) =>{
    // 看看响应的形式是怎么样的
    // console.log(resp);
    // console.log(resp.data.data.name);
    this.user = resp.data.data;
    })
    },
    },
  3. 退出登录功能实现

    <el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
    // 退出登录的方法
    logout(){
     doGet("/api/logout",{}).then((resp) =>{
       // 看看响应的形式是怎么样的
       console.log(resp);
       if (resp.data.code === 200){
         messageTip("退出成功!","success");
         removeToken();
         window.location.href = "/";
      }else{
         messageConfirm("退出异常,是否强制退出?","温馨提示").then(() =>{
           removeToken();
           window.location.href = "/";
        }).catch(() => {
           // 用户点击取消就会触发 catch 里
           messageTip("取消强制退出","warning")
        })
      }
    })
    },

四、一个用户增删改查的例子

需求:建立一个网站的用户页,使用之前布局的框架。在这个页面里,可以完成对所用用户的新增、删除、修改、查看功能。

Step1 用户管理的结构的设计

  1. 开启 vue 的路由模式

    开启 vue 的路由模式,即在 <el-menu> 标签上加上 :router 这个属性,并开启,如:

    <el-menu
       default-active="2"
       class="el-menu-vertical-demo"
       :unique-opened="true"
       :collapse="isCollapse"
       :collapse-transition="false"
       :router="true">

    开启路由模式之后,就可以表单的 index 属性里,写上我们的路径,然后点击就可以跳转。如:

    <!--<el-menu-item index="7-1">-->
    <el-menu-item index="/dashboard/user">
    <el-icon><Management /></el-icon>
    用户管理
    </el-menu-item>

    访问 dashboard 下的 user 页面,即 /dashboard/user。就在对应用户管理的 index 里写下这个路径。

  2. 使用子路由

    在之前 dashboard 这个路由的基础之上,添加我们用户页的路由

    {
    path: "/dashboard",
    component: () => import("../view/DashboardView.vue"),
    // 子路由,子路由可以是多个,所以是数组
    children: [
    {
    // 子路由里面的每一个组件,都是和路由一样,path 和 component
    // 唯一的区别是,子路由的 path 不能带斜杠
    path: "user",
    component: () => import("../view/UserView.vue"),
    }
    ]
    },
  3. 将我们的需要渲染子路由的地方,即 dashboard 的主页面上写上 <router-view/>

    <!--主区域开始-->
    <el-main>
    <router-view/>
    </el-main>
    <!--主区域结束-->

这样,我们的 UserVier.vue 就运行在 dashboard 页面的主页面里了。

Step2 用户管理的界面的实现

  1. 界面上组件的先完成堆砌

    <!--两个按钮-->
    <el-button type="primary">添加用户</el-button>
    <el-button type="danger">批量删除</el-button>
    <!--表格开始-->
    <el-table
       :data="userList"
       style="width: 100%"
       @selection-change="handleSelectionChange">
     <el-table-column type="selection" :selectable="selectable" width="60"/>
     <!--若 type 为 id,则该字段会自动增长-->
     <el-table-column type="index" label="序号" width="60"/>
     <el-table-column property="loginAct" label="账号" width="120"/>
     <el-table-column property="name" label="姓名" width="120"/>
     <el-table-column property="phone" label="手机" width="120"/>
     <el-table-column property="email" label="邮箱" width="240"/>
     <el-table-column property="createTime" label="创建时间" />
     <el-table-column label="操作" >
       <el-button type="primary">详情</el-button>
    <el-button type="warning">编辑</el-button>
    <el-button type="danger">删除</el-button>
     </el-table-column>
    </el-table>
    <!--表格结束-->

    自行控制每个控件的 width 以控制每个字段的宽度

    data(){
     return{
       // 定义 List 对象
       userList: [{}]
    }
    },

    methods:{
     // 勾选或者取消勾选时触发该函数
     handleSelectionChange(){
    // 完成批量删除模块功能时再写这个方法
    },
    },
    <style scoped>
    .el-table {
     margin-top: 15px;
    }
    </style>
  2. 加载后端的数据

    使用 mounted 钩子函数,在页面一开始渲染的时候,就向后端请求加载数据。

    mounted(){
     this.getData(1);
    },
    // 查询用户列表数据
    getData(current){
     doGet("/api/users",{
       // 当前页
       current: current
    }).then(resp => {
       if (resp.data.code === 200){
         // console.log(resp)
         this.userList = resp.data.data.list;
      }
    })
    },
  3. 完成底部的页码样式

    在表格的下方加入页码栏,如:

    <el-pagination
    background
    layout="prev, pager, next"
    :page-size=myPageSize
    :total=myTotal />

    注册 myPageSize 和 myTotal

    data(){
     return{
       // 定义 List 对象
       userList: [{}],
       myPageSize: '',
       myTotal: '',
    }
    },

    在 getData 方法中新增 myTotal 和 myPageSize 的赋值方法

    getData(current){
     doGet("/api/users",{
       current: current
    }).then(resp => {
       if (resp.data.code === 200){
         console.log(resp)
         this.userList = resp.data.data.list;
         this.myTotal = resp.data.data.total;
         this.myPageSize = resp.data.data.pageSize;
      }
    })
    },
    .el-pagination{
     margin-top: 20px;
    }
  4. 完成底部的页码功能

    在分页组件中添加三个属性,是 @prev-click@current-change@next-click注意,element 框架会自动将这三个方法传入即将跳转转入的页数,我们不用写参数。

    <el-pagination
       background
       layout="prev, pager, next"
       :page-size=myPageSize
       :total=myTotal
       @prev-click="toPage"
       @current-change="toPage"
       @next-click="toPage" />
    toPage(current){
     this.getData(current)
    },

Step3 用户管理的查看功能的实现

需求:点击对应用户的详情按钮,展示出该用户的所有字段信息。

  1. 添加路由信息

    因为要跳转新的页面,所以需要添加路由信息,我们依然是在 main 页面做改变,所以,还是添加子路由,在之前的 user 子路由后面加上我们新的子路由。如:

    children: [
      {
           path: "user",
           component: () => import("../view/UserView.vue"),
      },
      {
           // id 是动态的,所以前面加一个冒号
           path: "user/:id",
           component: () => import("../view/UserDetailView.vue"),
      }
    ]

    注意我们这里是用动态的地址,路径为: /dashboard/user/1,最后面的数字根据不同的用户而不同。所以在路由的 path 中带一个冒号作为动态。

  2. 界面的跳转

    点击某个用户列表的详情按钮,跳转到对应用户的详情页。

    <!--使用 vue 的作用域插槽功能,获取当前用户的 id 作为参数-->
    <template #default="scope">
     <el-button type="primary" @click="view(scope.row.id)">详情</el-button>
     <el-button type="warning">编辑</el-button>
     <el-button type="danger">删除</el-button>
    </template>
    // 跳转到指定 id 的用户信息界面
    view(id) {
     let url = "/dashboard/user/" + id
     // alert(url)
     this.$router.push(url)
    },
  3. 用户详情页的设计

    <el-button type="success" @click="goBack">返回</el-button>
    <el-form :model="userDetail" class="my-form" label-width="130px">
     <el-form-item label="ID">
       <div class="div-item">
         <!-- 写上 &nbsp; 是为了防止后面的数据未空,div 里空值-->
         &nbsp;{{ userDetail.id }}
       </div>
     </el-form-item>

     <el-form-item label="账户">
       <div class="div-item">
         &nbsp;{{ userDetail.loginAct }}
       </div>
     </el-form-item>

     <el-form-item label="密码">
       <div class="div-item">
         <!--密码直接写死-->
         &nbsp;******
       </div>
     </el-form-item>

     <el-form-item label="姓名">
       <div class="div-item">
         &nbsp;{{ userDetail.name }}
       </div>
     </el-form-item>

     <el-form-item label="手机">
       <div class="div-item">
         &nbsp;{{ userDetail.phone }}
       </div>
     </el-form-item>

     <el-form-item label="邮箱">
       <div class="div-item">
         &nbsp;{{ userDetail.email }}
       </div>
     </el-form-item>

     <el-form-item label="账户是否过期">
       <div class="div-item">
         &nbsp;{{ userDetail.accountNoExpired == '1' ? '未过期' : '已过期' }}
       </div>
     </el-form-item>

     <el-form-item label="密码是否过期">
       <div class="div-item">
         &nbsp;{{ userDetail.credentialsNoExpired == '1' ? '未过期' : '已过期' }}
       </div>
     </el-form-item>

     <el-form-item label="账户是否锁定">
       <div class="div-item">
         &nbsp;{{ userDetail.accountNoLocked == '1' ? '未锁定' : '已锁定' }}
       </div>
     </el-form-item>

     <el-form-item label="账户是否启用">
       <div class="div-item">
         &nbsp;{{ userDetail.accountEnabled == '1' ? '启用中' : '未启用' }}
       </div>
     </el-form-item>

     <el-form-item label="账户创建时间">
       <div class="div-item">
         &nbsp;{{ userDetail.createTime }}
       </div>
     </el-form-item>

     <el-form-item label="创建人">
       <div class="div-item">
         &nbsp;{{ userDetail.createByDO.name }}
       </div>
     </el-form-item>

     <el-form-item label="上次账户编辑时间">
       <div class="div-item">
         &nbsp;{{ userDetail.editTime }}
       </div>
     </el-form-item>

     <el-form-item label="编辑人">
       <div class="div-item">
         &nbsp;{{ userDetail.editByDO.name }}
       </div>
     </el-form-item>

     <el-form-item label="上次账户登录时间">
       <div class="div-item">
         &nbsp;{{ userDetail.lastLoginTime }}
       </div>
     </el-form-item>
    </el-form>
    data() {
     return {
       userDetail: {
         id: '',
         loginAct: '',
         loginPwd: '',
         name: '',
         phone: '',
         email: '',
         accountNoExpired: '',
         credentialsNoExpired: '',
         accountNoLocked: '',
         accountEnabled: '',
         createTime: '',
         createBy: '',
         editTime: '',
         editBy: '',
         lastLoginTime: '',
         createByDO: {
           id: '',
           name: '',
        },
         editByDO: {
           id: '',
           name: '',
        },
      },
    }
    },
    mounted() {
     this.getData();
    },
    methods: {
    getData() {
    // 获取 id。这里的 params 后面的 id 的名称,要与动态路由里设置的动态名称一样
    let id = this.$route.params.id
    let url = "/api/user/" + id
    doGet(url, {}).then(resp => {
    if (resp.data.code === 200) {
    // console.log(resp.data.data);
    this.userDetail = resp.data.data;
    }
    })
    },
    goBack() {
    this.$router.go(-1);
    },
    },
    <style scoped>
    /*设置整个表格的样式*/
    .my-form {
    /*设置左边的间距*/
    padding-left: 30px;
    }
    /*设置每个菜单项的值的样式*/
    .div-item {
    /*设置背景色*/
    background-color: azure;
    /*设置宽度比例*/
    width: 100%;
    /*设置左边的间距*/
    padding-left: 15px;
    }
    </style>

Step4 用户管理的新增功能的实现

需求:点击添加用户按钮,输入新用户的信息,新增一个用户。

  1. 页面准备

    <!--这是新增用户的弹窗-->
    <el-dialog v-model="addUserWindows" title="添加用户" width="600" draggable>
     <el-form :model="addUser" label-width="110px">
       <el-form-item label="账号"  prop="loginAct">
         <el-input v-model="addUser.loginAct" />
       </el-form-item>

       <el-form-item label="密码" prop="loginPwd">
         <el-input type="password" v-model="addUser.loginPwd" />
       </el-form-item>

       <el-form-item label="姓名" prop="name">
         <el-input v-model="addUser.name" />
       </el-form-item>

       <el-form-item label="手机" prop="phone">
         <el-input v-model="addUser.phone" />
       </el-form-item>

       <el-form-item label="邮箱" prop="email">
         <el-input v-model="addUser.email" />
       </el-form-item>

       <el-form-item label="账户是否过期" prop="accountNoExpired">
         <el-select v-model="addUser.accountNoExpired" placeholder="请选择" style="width: 100%">
           <el-option label="正常" value="正常"/>
           <el-option label="已过期" value="已过期"/>
         </el-select>
       </el-form-item>

       <el-form-item label="密码是否过期" prop="credentialsNoExpired">
         <el-select v-model="addUser.credentialsNoExpired" placeholder="请选择" style="width: 100%">
           <el-option label="正常" value="正常"/>
           <el-option label="已过期" value="已过期"/>
         </el-select>
       </el-form-item>

       <el-form-item label="账户是否锁定" prop="accountNoLocked">
         <el-select v-model="addUser.accountNoLocked" placeholder="请选择" style="width: 100%">
           <el-option label="正常" value="正常"/>
           <el-option label="已锁定" value="已锁定"/>
         </el-select>
       </el-form-item>

       <el-form-item label="账户是否启用" prop="accountEnabled">
         <el-select v-model="addUser.accountEnabled" placeholder="请选择" style="width: 100%">
           <el-option label="正常" value="正常"/>
           <el-option label="未启用" value="未启用"/>
         </el-select>
       </el-form-item>
     </el-form>
     <template #footer>
       <div class="dialog-footer">
         <el-button @click="addUserWindows = false">取消</el-button>
         <el-button type="primary" @click="addUserSubmit">
          添加
         </el-button>
       </div>
     </template>
    </el-dialog>
    <el-button type="primary" @click="add">添加用户</el-button>
    // 提交新增用户
    addUserSubmit(){
     this.addUserWindows = false
     // ...
    },
    // 新增用户
    add() {
     this.addUserWindows = true
    },
    data() {
     return {
       // 定义 List 对象
       userList: [{}],
       myPageSize: '',
       myTotal: '',
       addUserWindows: false,
       addUser: {
         id: 0,
         loginAct: "",
         loginPwd: "",
         name: "",
         phone: "",
         email: "",
         accountNoExpired: "",
         credentialsNoExpired: "",
         accountNoLocked: "",
         accountEnabled: "",
      },
    }
    },
  2. 添加前端验证

    <el-form :model="addUser" label-width="110px" :rules="addUserRules" ref="addUserRefForm">
    data() {
     return {
       // 定义 List 对象
       userList: [{}],
       myPageSize: '',
       myTotal: '',
       addUserWindows: false,
       addUser: {
         id: 0,
         loginAct: "",
         loginPwd: "",
         name: "",
         phone: "",
         email: "",
         accountNoExpired: "",
         credentialsNoExpired: "",
         accountNoLocked: "",
         accountEnabled: "",
      },
       addUserRules: {
         loginAct: [
          {required: true, message: '请输入账号!', trigger: 'blur'},
        ],
         loginPwd: [
          {required: true, message: '请输入密码!', trigger: 'blur'},
          {min: 6, max: 16, message: '密码的长度在 6-16 之间!', trigger: 'blur'},
        ],
         name: [
          {required: true, message: '请输入姓名!', trigger: 'blur'},
           // 新增正则表达式,表达式的两边要加上斜杠
          {pattern: /^[\u4e00-\u9fa5]{2,}$/, message: '姓名必须全是中文,且至少两个字符!', trigger: 'blur'}
        ],
         phone: [
          {required: true, message: '请输入手机!', trigger: 'blur'},
          {pattern: /^1[3-9]\d{9}$/, message: '请输入正确的中国手机格式!', trigger: 'blur'},
        ],
         email: [
          {required: true, message: '请输入邮箱!', trigger: 'blur'},
          {
             pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/,
             message: '请输入正确的邮箱格式!',
             trigger: 'blur'
          },
        ],
         accountNoExpired: [
          {required: true, message: '请选择账户状态!', trigger: 'blur'},
        ],
         credentialsNoExpired: [
          {required: true, message: '请选择密码状态!', trigger: 'blur'},
        ],
         accountNoLocked: [
          {required: true, message: '请选择账户状态!', trigger: 'blur'},
        ],
         accountEnabled: [
          {required: true, message: '请选择账户状态!', trigger: 'blur'},
        ],
      }
    }
    },
    // 提交新增用户
    addUserSubmit(){
     this.$refs.addUserRefForm.validate((isValid) => {
       if (isValid) {
         // console.log("passed");
         let formData = new FormData();
         // 以键值对的形式写入数据
         formData.append('loginAct', this.addUser.loginAct);
         formData.append('loginPwd', this.addUser.loginPwd);
         formData.append('name', this.addUser.name);
         formData.append('phone', this.addUser.phone);
         formData.append('email', this.addUser.email);
         formData.append('accountNoExpired', this.addUser.accountNoExpired);
         formData.append('credentialsNoExpired', this.addUser.credentialsNoExpired);
         formData.append('accountNoLocked', this.addUser.accountNoLocked);
         formData.append('accountEnabled', this.addUser.accountEnabled);
         // console.log(formData);
         doPost("/api/user", formData).then((resp) => {
           if (resp.data.code === 200){
             messageTip("添加用户成功!","success");
             this.addUserWindows = false;
          }else{
             messageTip("添加用户失败!","error");
          }
        })
      }
    })
    },
  3. 更新成功后实现局部刷新

    • 修改路由

      因为我们想实现仅 dashboard 里的主页面刷新,要去修改 DashboardView 里的对应区域的路由,并注册对应值。

      <!--加上 v-if,控制该区域页面内容是否显示-->
      <router-view v-if="isRouterAlive"/>
      data(){
       return {
         // 其他已注册的值...
           
         // 控制该区域页面内容是否显示
         isRouterAlive: true,
      }
      },
    • 添加 provide

      再与 js 中的 data、methods 平级,添加一个 provide。之后就可以在该路由中刷新,或是子路由中刷新,现在我们的 UserView 页面就是 DashboardView 页面的子路由页面。同理,方法、数据放在这里也可以在子路由中使用。

      data() {
       // ...  
      },
      provide() {
       return {
         reload: () => {
           this.isRouterAlive = false;
           this.$nextTick(function () {
             this.isRouterAlive = true;
          })
        }
      }
      },
      methods: {
       // ...
      }
    • 在要刷新的页面注入并使用

      在要刷新的 vue 页面,即 UserView 中的 js 中的 data、methods 平级,添加一个 inject。导入之前写好的 reload 方法

      data() {
       // ...
      }

      inject:['reload'],
         
      methods: {
       // ...  
      }

      这是 addUserSubmit 方法中的局部,我在这里选择刷新。

      if (resp.data.code === 200) {
       messageTip("添加用户成功!", "success");
       this.addUserWindows = false;
       this.reload();
      } else {
       messageTip("添加用户失败!", "error");
      }

Step5 用户管理的编辑功能的实现

需求:点击对应用户的编辑按钮,展示出该用户的所有字段信息,并且可以修改这些信息。

加上编辑按钮的点击方法:

<el-button type="warning" @click="edit(scope.row.id)">编辑</el-button>
// 编辑指定 id 的用户
edit(id) {
 this.addUserWindows = true;
 this.loadEditData(id);
},
// 编辑时加载对应 id 用户的数据
loadEditData(id) {
 let url = "/api/user/" + id
 doGet(url, {}).then((resp) => {
   if (resp.data.code === 200) {
     // console.log(resp.data.data);
     this.addUser.id = resp.data.data.id;
     this.addUser.loginAct = resp.data.data.loginAct;
     this.addUser.loginPwd = "";
     this.addUser.name = resp.data.data.name;
     this.addUser.phone = resp.data.data.phone;
     this.addUser.email = resp.data.data.email;
     this.addUser.accountNoExpired = resp.data.data.accountNoExpired == 1 ? "正常" : "已过期";
     this.addUser.credentialsNoExpired = resp.data.data.credentialsNoExpired == 1 ? "正常" : "已过期";
     this.addUser.accountNoLocked = resp.data.data.accountNoLocked == 1 ? "正常" : "已锁定";
     this.addUser.accountEnabled = resp.data.data.accountEnabled == 1 ? "正常" : "未启用";
  }
})
},

我们的编辑功能,打算编辑和添加功能共用一个窗口。我们通过 addUser.id 是不是等于 0 来判断是编辑还是添加。

修改弹出窗口:

<el-dialog v-model="addUserWindows" :title="addUser.id>0?'编辑用户':'添加用户'" width="600" draggable>

修改之前的添加提交按钮:

<el-button type="primary" @click="addUserSubmit">
{{ addUser.id > 0 ? '编 辑' : '添 加' }}
</el-button>

修改之前的密码输入框:

<!--编辑时,将密码设置为不验证-->
<el-form-item label="密码" v-if="addUser.id > 0">
 <el-input type="password" v-model="addUser.loginPwd"/>
</el-form-item>
<!--非编辑时-->
<el-form-item label="密码" prop="loginPwd" v-else>
 <el-input type="password" v-model="addUser.loginPwd"/>
</el-form-item>

修改之前的 add 方法:

// 新增用户
add() {
 // 将 addUser 重置为空
 this.addUser = {}
 this.addUserWindows = true;
},

修改之前的 addUserSubmit 方法:

// 提交新增、编辑用户
addUserSubmit() {
 this.$refs.addUserRefForm.validate((isValid) => {
   if (isValid) {
     let formData = new FormData();

     // 以键值对的形式写入数据
     formData.append('loginAct', this.addUser.loginAct);
     formData.append('loginPwd', this.addUser.loginPwd);
     formData.append('name', this.addUser.name);
     formData.append('phone', this.addUser.phone);
     formData.append('email', this.addUser.email);
     formData.append('accountNoExpired', this.addUser.accountNoExpired == "正常" ? '1' : '0');
     formData.append('credentialsNoExpired', this.addUser.credentialsNoExpired == "正常" ? '1' : '0');
     formData.append('accountNoLocked', this.addUser.accountNoLocked == "正常" ? '1' : '0');
     formData.append('accountEnabled', this.addUser.accountEnabled == "正常" ? '1' : '0');

     if (this.addUser.id > 0) {
       // console.log("走这里")
       formData.append('id', this.addUser.id);
       // 将编辑用户代码在此处写
       // doPut("/api/user", formData).then((resp) => {
       doPut("/api/user", formData).then((resp) => {
         // console.log(resp.data.data);
         if (resp.data.code === 200) {
           messageTip("编辑用户成功!", "success");
           this.addUserWindows = false;
           this.reload();
        } else {
           messageTip("编辑用户失败!请检查输入的条件!", "error");
        }
      })
    } else {
       // 将之前的内容(添加用户)的 Post 部分移至 else 分支里
       doPost("/api/user", formData).then((resp) => {
         // console.log(resp.data.data);
         if (resp.data.code === 200) {
           messageTip("添加用户成功!", "success");
           this.addUserWindows = false;
           this.reload();
        } else {
           messageTip("添加用户失败!", "error");
        }
      })
    }
  }
})
},

Step6 用户管理的删除功能的实现

需求:点击对应用户的删除按钮,可以删除该用户。同时,也可以使用批量删除用户功能。

  1. 单个删除

    <el-button type="danger" @click="del(scope.row.id,scope.row.name)">删除</el-button>
    // 删除指定用户
    del(id, name) {
     messageConfirm("确认删除 " + name + " 吗?", "温馨提示").then(() => {
       let url = "/api/user/" + id
       //alert(url)
       doDelete(url, {}).then((resp) => {
         if (resp.data.code === 200) {
           messageTip("已删除" + name, "success")
           this.reload();
        } else {
           messageTip("删除失败!", "error")
        }
      })
    }).catch(() => {
       // 用户点击取消就会触发 catch 里
       messageTip("已取消删除!", "error")
    })
    },
  2. 批量删除

    • 增加点击函数

      <el-button type="danger" @click="batchDel">批量删除</el-button>
    • 在 data 中定义:

      selectedIds: [],
      selectedNames: [],
    • 现在可以完整这个函数了

      改函数是 ele 框架带的,用的时候不用传入参数,在这个函数里会自动传入勾选的数据,以数组的形式。

      // 勾选或者取消勾选时触发该函数
      handleSelectionChange(selectionDataArray) {
       // console.log(selectionDataArray)
       // 清空 Ids、Names 数组
       this.selectedIds = [];
       this.selectedNames = [];
       // 遍历数组
       selectionDataArray.forEach(data => {
         // 遍历数组中的元素,将 id、names 加入统一的数组
         this.selectedIds.push(data.id);
         this.selectedNames.push(data.name);
      })
       // console.log(selectionDataArray)
      },
    • 完成批量删除的函数

      // 批量删除用户
      batchDel() {
       if (this.selectedNames.length == 0) {
         messageTip("请勾选批量删除的用户!", "error")
         return;
      }
       messageConfirm("确认批量删除删除 " + this.selectedNames + " 吗?", "温馨提示").then(() => {
         // 将数组变成字符串,用逗号相隔
         let ids = this.selectedIds.join(",");
         // alert(ids)
         doDelete("/api/users", {ids: ids}).then((resp) => {
           if (resp.data.code === 200) {
             messageTip("已批量删除" + this.selectedNames, "success")
             this.reload();
          } else {
             messageTip("批量删除失败!原因:" + resp.data.msg, "error")
          }
        })
      }).catch(() => {
         // 用户点击取消就会触发 catch 里
         messageTip("已取消批量删除!", "warning")
      })
      },

    五、一个选择的例子

    需求:有一个选择负责人的单选框,单选框里面是数据库中所有的用户,可以选择任意一个用户作为负责人。

    1. 界面实现

      <el-form-item label="负责人">
       <el-select
           v-model="activityQuery.owner"
           placeholder="请选择负责人"
           clearable
           @click="loadOwner">
         <el-option
             v-for="item in ownerOptions"
             :key="item.id"
             :label="item.name"
             :value="item.id"/>
       </el-select>
      </el-form-item>
    2. 数据绑定

      data() {
         return {
        ownerOptions: [],
        }
      }
    3. 方法实现

      methods: {
       // 加载负责人
       loadOwner() {
         doGet("/api/owners", {}).then(resp => {
           if (resp.data.code === 200) {
             // console.log(resp)
             this.ownerOptions = resp.data.data;
          }
        })
      },
      }

后端的开始

由于我主要研究的是 Java 后端方面的开发,但目前开发流程要求程序员必须同时精通前端和后端。所以,在这本手册中,后端方面的手册,我会简略写。前端方面的手册,我会详细写。

一、新建 SpringBoot 项目步骤

Step1 选择项目所需的依赖

  • 自动添加的依赖

    • Spring Boot DevTools

    • Lombok

    • Spring Configuration Processor

    • Spring Web

    • Spring Security

    • MyBatis Framework

    • MySQL Driver

    • Spring Data Redis (Access+Driver)

  • 手动添加的依赖

    <!--目前最新的 pagehelper 依赖  -->
    <dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
       <version>2.1.1</version>
    </dependency>
    <!--目前最新的 jwt 依赖-->
    <dependency>
       <groupId>com.auth0</groupId>
       <artifactId>java-jwt</artifactId>
       <version>4.5.0</version>
    </dependency>
    <!-- 目前最新的 aop 依赖 -->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-aop</artifactId>
       <version>3.5.5</version>
    </dependency>

Step2 创建 application.yml

删除自带的 application.properties 文件,创建 application.yml 文件,文件内容如下:

server:
 # 设置后端 SpringBoot 端口
port: 8089
servlet:
   # 项目路径直接是斜杠
  context-path: /

# 配置数据库连接相关信息
spring:
datasource:
  type: com.zaxxer.hikari.HikariDataSource
  url: jdbc:mysql://localhost:3306/xxxxxxxxx?useUnicode=true&characterEncoding=utf8&useSSL=false
  driver-class-name: com.mysql.cj.jdbc.Driver
  username: root
  password: xxxxxxxxxxxxxxx
  hikari:
    maximum-pool-size: 30
    minimum-idle: 5
    connection-timeout: 5000
    idle-timeout: 600000
    max-lifetime: 18000000
data:
   # 配置 redis 的连接信息
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 1
jackson:
  time-zone: GMT+8
  date-format: yyyy-MM-dd HH:mm:ss

# 指定以下 mapper.xml 文件的位置
mybatis:
mapper-locations: classpath:mapper/*.xml

Step3 修改启动类

package com.sangui;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* @author sangui
*/
@SpringBootApplication
@MapperScan(basePackages = {"com.sangui.mapper"})
public class DlykServerApplication implements CommandLineRunner {

   @Resource
   private RedisTemplate<String, Object> redisTemplate;

   @Override
   public void run(String... args) throws Exception {
       // SpringBoot 项目启动后,把 redisTemplate 这个 Bean 修改一下,修改一下 key 和 value 的序列化方式

       // 设置 key 序列化
       redisTemplate.setKeySerializer(new StringRedisSerializer());

       // 对象映射工具,Java 对象和 json 对象进行相互转化
       ObjectMapper mapper = new ObjectMapper();
       // 设置可见性
       mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
       // 激活类型
       mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING);

       // 设置 value 序列化
       redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(mapper, Object.class));

       // 设置 hashKey 序列化
       redisTemplate.setHashKeySerializer(new StringRedisSerializer());

       // 设置 hashValue 序列化
       redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(mapper, Object.class));

  }

   public static void main(String[] args) {
       SpringApplication.run(DlykServerApplication.class, args);
  }

}

Step4 创建项目的包结构

  • common

  • config

    • filter

    • handler

  • constant

  • manager

  • mapper

  • model

  • query

  • result

  • service

    • impl

  • util

  • web

Step5 完成数据库的逆向工程

使用 Idea 插件 Free MyBatis Tool 自动生成代码。

使用插件生成以下内容:

  • model 实体类

    • 设置该类的包为:com.sangui.model

  • mapper 接口类

    • 设置接口的名字后缀为:Mapper

    • 设置该类的包为:com.sangui.mapper

  • mapper 映射文件

    • 设置该类的包为:mapper

注意:

  • 生成代码之前,取消勾选 Rpository-Annotation(Repository注解),保留前面五个选项

  • 生成代码之后,修改数据库中 JdbcType=tinyint 对应类的属性的数据类型,从 Byte 改为 Boolean

  • model 类中的任何 Boolean 类型的变量,都不要加 is 前缀,从 isXxx 改为 xxx

  • 同时在含有 is_xxx 字段的表对应 mapper映射文件<resuletMap> 中设置从 is_xxx 到 xxx 的映射关系

Step6 创建项目的工具类和其他必要的类

1) Util 类

  1. ~/util/JsonUtils.java

    package com.sangui.util;


    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-30
    * @Description: json 工具类,进行 java 对象与 json 字符串之间的相互转化
    * @Version: 1.0
    */
    public class JsonUtils {
       private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

       /**
        * 把 java 对象转成 json 字符串
        *
        * @param object java 对象
        * @return json 字符串
        */
       public static String toJson(Object object) {
           try {
               return OBJECT_MAPPER.writeValueAsString(object);
          } catch (JsonProcessingException e) {
               throw new RuntimeException(e);
          }
      }

       /**
        * 把 json 字符串转成 java 对象
        *
        * @param json json 字符串
        * @param clazz 想要转的 java 对象 的类型
        * @return java 对象
        * @param <T> java 对象的类型
        */
       public static <T> T toBean(String json, Class<T> clazz) {
           try {
               return OBJECT_MAPPER.readValue(json, clazz);
          } catch (JsonProcessingException e) {
               throw new RuntimeException(e);
          }
      }
    }
  2. ~/util/JwtUtils.java

    package com.sangui.util;


    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.Claim;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.sangui.model.TUser;

    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-02
    * @Description: jwt 工具类
    * @Version: 1.0
    */
    public class JwtUtils {
       /**
        * 自定义的密钥,新项目中,修改自定义密钥
        */
       public static final String SECRET = "mqdqxygyqyklklsys24678";


       /**
        * 生成 jwt,即 token
        * @param userJson tUser 对象的 json
        * @return 返回 jwt,即 token
        */
       public static String createJwt(String userJson) {
           // 组装头数据
           Map<String, Object> header = new HashMap<>();
           header.put("alg", "HS256");
           header.put("typ", "JWT");

           return JWT.create()
                   //头部
                  .withHeader(header)
                   //负载
                  .withClaim("user", userJson)
                   //签名
                  .sign(Algorithm.HMAC256(SECRET));
      }

       /**
        * 验证 jwt
        * @param jwt 验证的 jwt 的字符串
        * @return jwt 是否正确
        */
       public static Boolean verifyJwt(String jwt) {
           try {
               // 使用秘钥创建一个 jwt 验证器对象
               JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();

               //验证 jwt ,如果没有抛出异常,说明验证通过,否则验证不通过
               jwtVerifier.verify(jwt);

               return true;
          } catch (Exception e) {
               e.printStackTrace();
          }
           return false;
      }

       /**
        * 解析 jwt 的数据
        *
        */
       public static void parseJwt(String jwt) {
           try {
               // 使用秘钥创建一个验证器对象
               JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();

               //验证JWT,得到一个解码后的jwt对象
               DecodedJWT decodedJwt = jwtVerifier.verify(jwt);

               // 通过解码后的 jwt 对象,就可以获取里面的负载数据
               Claim nickClaim = decodedJwt.getClaim("nick");
               Claim ageClaim = decodedJwt.getClaim("age");
               Claim phoneClaim = decodedJwt.getClaim("phone");
               Claim birthDayClaim = decodedJwt.getClaim("birthDay");


               String nick = nickClaim.asString();
               int age = ageClaim.asInt();
               String phone = phoneClaim.asString();
               Date birthDay = birthDayClaim.asDate();

               System.out.println(nick + " -- " + age + " -- " + phone + " -- " + birthDay);
          } catch (TokenExpiredException e) {
               e.printStackTrace();
               throw new RuntimeException(e);
          }
      }

       public static TUser parseUserFromJwt(String jwt) {
           try {
               // 使用秘钥创建一个验证器对象
               JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();

               // 验证 jwt,得到一个解码后的 jwt 对象
               DecodedJWT decodedJwt = jwtVerifier.verify(jwt);

               // 通过解码后的 jwt 对象,就可以获取里面的负载数据
               Claim userClaim = decodedJwt.getClaim("user");

               String userJson = userClaim.asString();

               return JsonUtils.toBean(userJson, TUser.class);
          } catch (TokenExpiredException e) {
               e.printStackTrace();
               throw new RuntimeException(e);
          }
      }
    }
  3. ~/util/ResponseUtils.java

    package com.sangui.util;


    import jakarta.servlet.http.HttpServletResponse;

    import java.io.IOException;
    import java.io.PrintWriter;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-30
    * @Description: Response 工具类
    * @Version: 1.0
    */
    public class ResponseUtils {

       /**
        * 使用 response,把结果写出到前端
        *
        * @param response 响应
        * @param result 结果
        */
       public static void write(HttpServletResponse response, String result) {
           response.setContentType("application/json;charset=UTF-8");
           PrintWriter writer = null;
           try {
               writer = response.getWriter();
               writer.write(result);
               writer.flush();
          } catch (IOException e) {
               throw new RuntimeException(e);
          } finally {
               if (writer != null) {
                   writer.close();
              }
          }
      }
    }
  4. ~/util/ResponseUtils.java

    package com.sangui.util;


    import org.springframework.util.ObjectUtils;

    import java.util.function.Consumer;
    import java.util.function.Supplier;


    /**
    * @Author: sangui
    * @CreateTime: 2025-09-09
    * @Description: 缓存工具类
    * @Version: 1.0
    */
    public class CacheUtil {
       /**
        * 带有缓存的查询工具方法
        *
        * @param cacheSelector cacheSelector
        * @param databaseSelector databaseSelector
        * @param cacheSave cacheSave
        * @param <T> <T>
        * @return <T>
        */
       public static <T> T getCacheData(Supplier<T> cacheSelector, Supplier<T> databaseSelector, Consumer<T> cacheSave) {
           // 从 redis 查询
           T data = cacheSelector.get();
           // 如果 redis 没查到
           if (ObjectUtils.isEmpty(data)) {
               // 从数据库查
               data = databaseSelector.get();
               // 数据库查到了数据
               if (!ObjectUtils.isEmpty(data)) {
                   // 把数据放入 redis
                   cacheSave.accept(data);
              }
          }
           // 返回数据
           return data;
      }
    }

2) Result 类

  1. ~/result/CodeEnum.java

    package com.sangui.result;


    import lombok.*;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-30
    * @Description: R 状态的枚举类,返回给前端的 Code 枚举类。包含两个属性,code 和 msg
    * @Version: 1.0
    */
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @RequiredArgsConstructor
    public enum CodeEnum {
       OK(200,"成功"),
       FAIL(500,"失败"),
       TOKEN_IS_EMPTY(901,"登录请求 Token 为空"),
       TOKEN_IS_ERROR(902,"登录请求 Token 有误"),
       TOKEN_IS_EXPIRED(903,"登录请求 Token 已过期"),
       TOKEN_IS_NONE_MATCH(904,"登录请求 Token 不匹配"),
       USER_LOGOUT(200, "退出成功"),
       DATA_ACCESS_EXCEPTION(500,"数据库操作失败"),
       ACCESS_DENIED(500,"权限不足"),
      ;

       /**
        * 结果码
        */
       private int code;

       /**
        * 结果信息
        */
       @NonNull
       private String msg;

    }
  2. ~/result/R.java

    package com.sangui.result;

    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-03
    * @Description: 统一封装 web 层向前端页面返回的结果
    * @Version: 1.0
    */
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class R {
       /**
        * 表示返回的结果码,比如 200 成功,500 失败
        */
       private int code;

       /**
        * 表示返回的结果信息,比如用户登录状态失效了,请求参数格式有误等
        */
       private String msg;

       /**
        * 表示返回的结果数据,数据可能是一个对象,也可以是一个 List 集合等
        */
       private Object data;

       /**
        * 此方法返回给前端的信息为:code:200,msg:成功
        * @return R 实体
        */
       public static R ok() {
           return R.builder()
                  .code(CodeEnum.OK.getCode())
                  .msg(CodeEnum.OK.getMsg())
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:{自定义},msg:{自定义}
        * @param code 结果码
        * @param msg 结果信息
        * @return R 实体
        */
       public static R ok(int code, String msg) {
           return R.builder()
                  .code(code)
                  .msg(msg)
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:200,msg:成功,data:{自定义}
        * @param data 结果数据
        * @return R 实体
        */
       public static R ok(Object data) {
           return R.builder()
                  .code(CodeEnum.OK.getCode())
                  .msg(CodeEnum.OK.getMsg())
                  .data(data)
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:{自定义},msg:{自定义}
        * @param codeEnum Code 枚举类
        * @return R 实体
        */
       public static R ok(CodeEnum codeEnum) {
           return R.builder()
                  .code(codeEnum.getCode())
                  .msg(codeEnum.getMsg())
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:500,msg:失败
        * @return R 实体
        */
       public static R fail() {
           return R.builder()
                  .code(CodeEnum.FAIL.getCode())
                  .msg(CodeEnum.FAIL.getMsg())
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:500,msg:{自定义}
        * @param msg 结果信息
        * @return R 实体
        */
       public static R fail(String msg) {
           return R.builder()
                  .code(CodeEnum.FAIL.getCode())
                  .msg(msg)
                  .build();
      }

       /**
        * 此方法返回给前端的信息为:code:{自定义},msg:{自定义}
        * @param codeEnum Code 枚举类
        * @return R 实体
        */
       public static R fail(CodeEnum codeEnum) {
           return R.builder()
                  .code(codeEnum.getCode())
                  .msg(codeEnum.getMsg())
                  .build();
      }
    }

3) Constant 类

  1. ~/constant/Constants.java

    package com.sangui.constant;


    /**
    * @Author: sangui
    * @CreateTime: 2025-09-02
    * @Description: 常量类
    * @Version: 1.0
    */
    public class Constants {
       // 后端验证登录的 URI 路径
       public static final String LOGIN_URI = "/api/login";
       // 在 user 表中,登录账号的属性名
       public static final String NAME_OF_USERNAME_IN_USER = "loginAct";
       // 在 user 表中,密码的属性名
       public static final String NAME_OF_PASSWORD_IN_USER = "loginPwd";

       // redis 的 key 的命名规范: 项目名:模块名:功能名:唯一业务参数(比如用户 id )
       public static final String REDIS_JWT_KEY = "dlyk:user:login:";

       // redis 中负责人的 key
       public static final String REDIS_OWNER_KEY = "dlyk:user:owner";

       // jwt过期时间 7 天
       public static final Long EXPIRE_TIME = 7 * 24 * 60 * 60L;

       // jwt 过期时间 30 分钟
       public static final Long DEFAULT_EXPIRE_TIME = 30 * 60L;

       //分页时每页显示 10 条数据
       public static final int PAGE_SIZE = 10;

       // 请求 token 的名称
       public static final String TOKEN_NAME = "Authorization";

       public static final String EMPTY = "";

       // 导出 Excel 的接口路径
       public static final String EXPORT_EXCEL_URI = "/api/exportExcel";

       public static final String EXCEL_FILE_NAME = "客户信息数据";
    }

4) Service 类

  1. ~/service/RedisService.java

    package com.sangui.service;


    import java.util.concurrent.TimeUnit;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-02
    * @Description: RedisService
    * @Version: 1.0
    */
    public interface RedisService {
       /**
        * 在 Redis 中存数据
        * @param key Redis 中的 key
        * @param value 存入的值
        */
       void setValue(String key, Object value);

       /**
        * 在 Redis 中取指定 key 的数据
        * @param key Redis 中的 key
        * @return 取到的数据
        */
       Object getValue(String key);

       /**
        * 删除在 Redis 中指定 key 的数据
        * @param key Redis 中的 key
        */
       void removeValue(String key);

       /**
        * 设置 Redis 中指定 key 的自动过期时间
        * @param key Redis 中的 key
        * @param timeOut 设置过期时间的数值,一般默认是秒为单位
        * @param timeUnit 设置 timeOut 的单位,一般是秒,即:TimeUnit.SECONDS
        */
       void expire(String key, Long timeOut, TimeUnit timeUnit);
    }
  2. ~/service/impl/RedisServiceImpl.java

    package com.sangui.service.impl;


    import com.sangui.service.RedisService;
    import jakarta.annotation.Resource;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;

    import java.util.concurrent.TimeUnit;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-02
    * @Description: RedisService 具体的实现类
    * @Version: 1.0
    */
    @Service
    public class RedisServiceImpl implements RedisService {
       @Resource
       private RedisTemplate<String, Object> redisTemplate;

       @Override
       public void setValue(String key, Object value) {
           redisTemplate.opsForValue().set(key, value);
      }

       @Override
       public Object getValue(String key) {
           return redisTemplate.opsForValue().get(key);
      }

       @Override
       public void removeValue(String key) {
           redisTemplate.delete(key);
      }

       @Override
       public void expire(String key, Long timeOut, TimeUnit timeUnit) {
           redisTemplate.expire(key, timeOut, timeUnit);
      }
    }

5) Config 类

  1. ~/config/handler/GlobalExceptionHandler.java

    package com.sangui.config.handler;


    import com.sangui.result.CodeEnum;
    import com.sangui.result.R;
    import org.springframework.dao.DataAccessException;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-08
    * @Description: 统一异常处理类,Controller 发生了异常,统一用该类进行处理
    * @Version: 1.0
    */
    // aop。拦截标注了 @RestController 的controller 中的所有方法
    @RestControllerAdvice
    // aop。拦截标注了 @Controller 的 controller 中的所有方法
    // @ControllerAdvice
    public class GlobalExceptionHandler {
       /**
        * 异常处理的方法(Controller 方法发生了异常,那么就使用该方法来处理)
        * @return 响应结果
        */
       @ExceptionHandler(value = Exception.class)
       public R handException(Exception e) {
           // 在控制台打印异常信息
           e.printStackTrace();
           return R.fail(e.getMessage());
      }
       
       /**
        * 异常的精确匹配,先精确匹配,匹配不到了,就找父类的异常处理
        * @param e 异常
        * @return 响应结果
        */
       @ExceptionHandler(value = DataAccessException.class)
       public R handException3(DataAccessException e) {
           // 在控制台打印异常信息
           e.printStackTrace();
           return R.fail(CodeEnum.DATA_ACCESS_EXCEPTION);
      }

       /**
        * 权限不足的异常处理
        * @param e 异常
        * @return 响应结果
        */
       @ExceptionHandler(value = AccessDeniedException.class)
       public R handException(AccessDeniedException e) {
           // 在控制台打印异常信息
           e.printStackTrace();
           return R.fail(CodeEnum.ACCESS_DENIED);
      }
    }

6) Common 类

  1. ~/common/DataScope.java

    package com.sangui.common;


    import java.lang.annotation.*;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-08
    * @Description: 数据范围的注解
    * @Version: 1.0
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataScope {

       // 要在 sql 语句的末尾添加一个过滤条件
       // select * from t_user (管理员)
       // select * from t_user tu where tu.id = 2 (普通用户:于嫣)

       // select * from t_activity (管理员)
       // select * from t_activity ta where ta.owner_id = 2 (普通用户:于嫣)

       /**
        * 表的别名
        */
       public String tableAlias() default "";

       /**
        * 表的字段名
        */
       public String tableField() default "";
    }

7) Manager 类

  1. ~/manager/RedisManager.java

    package com.sangui.manager;


    import jakarta.annotation.Resource;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;

    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-09
    * @Description: RedisManager
    * @Version: 1.0
    */
    @Component
    public class RedisManager {

       @Resource
       private RedisTemplate<String, Object> redisTemplate;

       public Object getValue(String key) {
           // string
           // list
           // hash
           // set
           // zset
           // 获取列表(正序)
           List<Object> result = redisTemplate.opsForList().range(key, 0, -1);
           // 反转列表以实现倒序
           if (result != null) {
               Collections.reverse(result);
          }
           return result;
      }

       public <T> Object setValue(String key,  Collection<T> data) {
           // string
           // list
           // hash
           // set
           // zset
           Object[] t  = new Object[data.size()];
           data.toArray(t);
           return redisTemplate.opsForList().leftPushAll(key, t);
      }
    }

二、一个使用 SpringSecurity,实现登录模块的例子

需求:使用 SpringSecurity 框架,满足只有登录上账号之后,才能访问系统。使用 jwt 作为是否登录的验证。同时,完成登录页的记住我的 7 天免密码登录功能。

Step1 修改用于登录模块的 user 表的信息

1) 修改 TUser 的 mapper 接口

在生成的 TUserMapper 接口中新增按用户名查找用户的方法,示例如下:(自行更改可能不一样的变量名)

TUser selectByLoginAct(String loginAct);

2) 修改 TUser 的 mapper 映射文件

同时在生成的 mapper映射文件 中新增对应 id 的 SQL 语句,示例如下:(自行更改可能不一样的变量名)

<select id="selectByLoginAct" parameterType="java.lang.String" resultMap="BaseResultMap">
  select
   <include refid="Base_Column_List" />
  from t_user
  where login_act = #{loginAct,jdbcType=BIGINT}
</select>

3) 修改 TUser 的实体类

  1. 类中加入两个属性,示例如下:

    // 角色 List
    private List<String> roleList;
    // 权限标识符 List
    private List<String> permissionList;
  2. 实现 UserDetails 接口

  3. 重写 UserDetails 接口的七个方法,示例如下:

    // 实现 UserDetails 的七个方法
    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
       List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();

       // 角色
       if (!ObjectUtils.isEmpty(this.getRoleList())){
           this.getRoleList().forEach(role -> {
               list.add(new SimpleGrantedAuthority(role));
          });
      }

       // 权限标识符
       if (!ObjectUtils.isEmpty(this.getPermissionList())){
           this.getPermissionList().forEach(permission -> {
               list.add(new SimpleGrantedAuthority(permission));
          });
      }
       return list;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
       return this.getLoginPwd();
    }

    @JsonIgnore
    @Override
    public String getUsername() {
       return this.getLoginAct();
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
       return this.getAccountNoExpired() == 1;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
       return this.getAccountNoLocked() == 1;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
       return this.getCredentialsNoExpired() == 1;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
       return this.getAccountEnabled() == 1;
    }

Step 2 创建项目所需的类

1) Config 类

  1. ~/config/handler/MyAuthenticationSuccessHandler.java

    package com.sangui.config.handler;


    import com.sangui.constant.Constants;
    import com.sangui.model.TUser;
    import com.sangui.result.R;
    import com.sangui.service.RedisService;
    import com.sangui.util.JsonUtils;
    import com.sangui.util.JwtUtils;
    import com.sangui.util.ResponseUtils;
    import jakarta.annotation.Resource;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;

    import java.io.IOException;
    import java.util.concurrent.TimeUnit;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-31
    * @Description: tUser 登录成功的处理器
    * @Version: 1.0
    */
    @Component
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
       @Resource
       private RedisService redisService;

       @Override
       public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
           TUser tUser = (TUser) authentication.getPrincipal();

           // 把 tUser 对象转成 json 作为负载数据放入 jwt
           String userJson = JsonUtils.toJson(tUser);
           String jwt = JwtUtils.createJwt(userJson);

           // 在 Redis 中放入此 jwt
           redisService.setValue(Constants.REDIS_JWT_KEY + tUser.getId(),jwt);

           // 设置 jwt 的过期时间(如果选择了记住我,过期时间是 7 天,否则是 30 min)
           String rememberMe = request.getParameter("rememberMe");
           if (Boolean.parseBoolean(rememberMe)){
               redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(),Constants.EXPIRE_TIME, TimeUnit.SECONDS);
          }else {
               redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(),Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
          }

           // 登录成功的统一结果
           R result = R.ok(jwt);

           // 把 R 对象转成 json
           String resultJson = JsonUtils.toJson(result);

           // 把 R 以 json 返回给前端
           ResponseUtils.write(response, resultJson);
      }
    }
  2. ~/config/handler/MyAuthenticationFailureHandler.java

    package com.sangui.config.handler;


    import com.sangui.result.R;
    import com.sangui.util.JsonUtils;
    import com.sangui.util.ResponseUtils;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.stereotype.Component;

    import java.io.IOException;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-31
    * @Description: tUser 登录失败的处理器
    * @Version: 1.0
    */
    @Component
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

       @Override
       public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
           // 登录失败,执行该方法,在该方法中返回 json 给前端,就行了
           // 登录失败的统一结果
           R result = R.fail(exception.getMessage());

           // 把 R 对象转成 json
           String resultJson = JsonUtils.toJson(result);

           // 把 R 以json返回给前端
           ResponseUtils.write(response, resultJson);
      }
    }
  3. ~/config/filter/TokenVerifyFilter.java

    package com.sangui.config.filter;


    import com.sangui.constant.Constants;
    import com.sangui.model.TUser;
    import com.sangui.result.CodeEnum;
    import com.sangui.result.R;
    import com.sangui.service.RedisService;
    import com.sangui.util.JsonUtils;
    import com.sangui.util.JwtUtils;
    import com.sangui.util.ResponseUtils;
    import jakarta.annotation.Resource;
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;

    import java.io.IOException;
    import java.util.concurrent.TimeUnit;

    /**
    * @Author: sangui
    * @CreateTime: 2025-09-02
    * @Description: TokenVerifyFilter
    * @Version: 1.0
    */
    @Component
    public class TokenVerifyFilter extends OncePerRequestFilter {

       @Resource
       private RedisService redisService;

       // SpringBoot 框架的 IoC 容器中已经创建好了该线程池,可以注入直接使用
       @Resource
       private ThreadPoolTaskExecutor threadPoolTaskExecutor;

       @Override
       protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
           // 如果是登录请求,此时还没有生成 jwt,那不需要对登录请求进行 jwt 验证
           if (request.getRequestURI().equals(Constants.LOGIN_URI)) {
               // 验证 jwt 通过了 ,让 Filter 链继续执行,也就是继续执行下一个 Filter
               filterChain.doFilter(request, response);
          } else {
               String token = null;
               if (request.getRequestURI().equals(Constants.EXPORT_EXCEL_URI)) {
                   // 从请求路径的参数中获取 token
                   token = request.getParameter("Authorization");
              } else {
                   // 其他请求都是从请求头中获取 token
                   token = request.getHeader("Authorization");
              }

               if (!StringUtils.hasText(token)) {
                   // token 验证未通过的统一结果
                   R result = R.fail(CodeEnum.TOKEN_IS_EMPTY);
                   // 把 R 对象转成 json
                   String resultJson = JsonUtils.toJson(result);
                   // 把 R 以 json 返回给前端
                   ResponseUtils.write(response, resultJson);
                   return;
              }

               // 验证 token 有没有被篡改过
               if (!JwtUtils.verifyJwt(token)) {
                   // token 验证未通过统一结果
                   R result = R.fail(CodeEnum.TOKEN_IS_ERROR);
                   // 把 R 对象转成 json
                   String resultJson = JsonUtils.toJson(result);
                   // 把 R 以 json 返回给前端
                   ResponseUtils.write(response, resultJson);
                   return;
              }

               TUser tUser = JwtUtils.parseUserFromJwt(token);
               String redisToken = (String) redisService.getValue(Constants.REDIS_JWT_KEY + tUser.getId());

               if (!StringUtils.hasText(redisToken)) {
                   // token 验证未通过统一结果
                   R result = R.fail(CodeEnum.TOKEN_IS_EXPIRED);

                   // 把 R 对象转成 json
                   String resultJson = JsonUtils.toJson(result);

                   // 把 R 以 json返回给前端
                   ResponseUtils.write(response, resultJson);

                   return;
              }

               if (!token.equals(redisToken)) {
                   // token 验证未通过的统一结果
                   R result = R.fail(CodeEnum.TOKEN_IS_NONE_MATCH);

                   // 把 R 对象转成 json
                   String resultJson = JsonUtils.toJson(result);

                   // 把 R 以 json 返回给前端
                   ResponseUtils.write(response, resultJson);
                   return;
              }

               // jwt 验证通过了,那么在 SpringSecurity 的上下文环境中要设置一下,设置当前这个人是登录过的,你后续不要再拦截他了
               UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(tUser, tUser.getLoginPwd(), tUser.getAuthorities());
               SecurityContextHolder.getContext().setAuthentication(authenticationToken);

               // 刷新一下 token(异步处理)
               // 异步处理(更好的方式,使用线程池去执行)
               threadPoolTaskExecutor.execute(() -> {
                   // 刷新 token
                   String rememberMe = request.getHeader("rememberMe");
                   if (Boolean.parseBoolean(rememberMe)) {
                       redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                  } else {
                       redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                  }
              });

               // 验证 jwt 通过了 ,让 Filter 链继续执行,也就是继续执行下一个 Filter
               filterChain.doFilter(request, response);
          }
      }
    }
  4. ~/config/SecurityConfig.java

    package com.sangui.config;


    import com.sangui.config.filter.TokenVerifyFilter;
    import com.sangui.config.handler.MyAuthenticationFailureHandler;
    import com.sangui.config.handler.MyAuthenticationSuccessHandler;
    import com.sangui.constant.Constants;
    import jakarta.annotation.Resource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.authentication.logout.LogoutFilter;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

    import java.util.List;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-31
    * @Description: SpringSecurity 配置文件
    * @Version: 1.0
    */
    @Configuration
    public class SecurityConfig {
       @Resource
       private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

       @Resource
       private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

       @Bean
       public PasswordEncoder passwordEncoder(){
           return new BCryptPasswordEncoder();
      }

       @Resource
       private TokenVerifyFilter tokenVerifyFilter;


       @Bean
       public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource corsConfigurationSource) throws Exception {
           // 禁用跨站请求伪造
           return httpSecurity.formLogin((formLogin)->{
                       formLogin.loginProcessingUrl(Constants.LOGIN_URI)
                              .usernameParameter(Constants.NAME_OF_USERNAME_IN_USER)
                              .passwordParameter(Constants.NAME_OF_PASSWORD_IN_USER)
                              .successHandler(myAuthenticationSuccessHandler)
                              .failureHandler(myAuthenticationFailureHandler);
                  })
                  .authorizeHttpRequests((authorize)->{
                       // 任何请求都需要登录后才能访问,除了 "/api/login"
                       authorize.requestMatchers(Constants.LOGIN_URI).permitAll()
                              .anyRequest().authenticated();
                  })
                   // 方法引用 禁用跨站请求伪造
                  .csrf(AbstractHttpConfigurer::disable)
                   // 支持跨域请求
                  .cors((cors) ->{
                       cors.configurationSource(corsConfigurationSource);
                  })
                  .addFilterBefore(tokenVerifyFilter, LogoutFilter.class)
                  .build();
      }

       @Bean
       public CorsConfigurationSource corsConfigurationSource(){
           CorsConfiguration corsConfiguration = new CorsConfiguration();
           // 允许任何来源,http://localhost:8080
           corsConfiguration.setAllowedOrigins(List.of("*"));
           // 运行任何方式,post, get, delete, put
           corsConfiguration.setAllowedMethods(List.of("*"));
           // 设置运行的请求头
           corsConfiguration.setAllowedHeaders(List.of("*"));

           UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
           source.registerCorsConfiguration("/**",corsConfiguration);
           return source;
      }
    }

2) Service 类

  1. ~/service/UserService.java

    package com.sangui.service;


    import org.springframework.security.core.userdetails.UserDetailsService;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-31
    * @Description: UserService
    * @Version: 1.0
    */
    public interface UserService extends UserDetailsService {
    }
  2. ~/service/impl/UserServiceImpl.java

    package com.sangui.service.impl;


    import com.sangui.mapper.TUserMapper;
    import com.sangui.model.TUser;
    import com.sangui.service.UserService;
    import jakarta.annotation.Resource;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;

    /**
    * @Author: sangui
    * @CreateTime: 2025-08-31
    * @Description: UserServiceImpl
    * @Version: 1.0
    */
    @Service
    public class UserServiceImpl implements UserService {
       @Resource
       TUserMapper userMapper;

       @Override
       public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
           TUser tUser = userMapper.selectByLoginAct(username);
           if (tUser == null) {
               throw new UsernameNotFoundException("登录账号不存在!");
          }
           
           // 更新上次登录时间
           tUser.setLastLoginTime(new Date());
           userMapper.updateByPrimaryKey(tUser);
           
           return tUser;
      }
    }

Step3 实现免密登录

前端给后端发送一个 GET 请求,请求路径是 /api/login/free,参数为空,后端响应给前端判断当前登录用户是否可以免密登录。

package com.sangui.web;


import com.sangui.model.TUser;
import com.sangui.result.R;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author: sangui
* @CreateTime: 2025-09-02
* @Description: UserController
* @Version: 1.0
*/
@RestController
public class UserController {
   /**
    * 判断是否可以免密登录
    * @return 判断结果
    */
   @GetMapping("/api/login/free")
   public R freeLogin(){
       // TokenVerifyFilter 会自动验证,这里不需要验证
       return  R.ok();
  }
}

Step4 给用户加上权限信息

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   TUser tUser = userMapper.selectByLoginAct(username);
   if (tUser == null) {
       throw new UsernameNotFoundException("登录账号不存在!");
  }

   
   tUser.setLastLoginTime(new Date());
   userMapper.updateByPrimaryKey(tUser);
   
   // 查询一下当前用户的角色信息
   List<TRole> tRoleList = tRoleMapper.selectByUserId(tUser.getId());
   // 字符串的角色列表
   List<String> stringRoleList = new ArrayList<>();
   tRoleList.forEach(tRole -> {
       stringRoleList.add(tRole.getRole());
  });
   // 设置用户的角色
   tUser.setRoleList(stringRoleList);
   
   return tUser;
}
<select id="selectByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
  select t_role.*
  from t_role
            left join t_user_role
                      on t_role.id = t_user_role.role_id
            left join t_user
                      on t_user_role.user_id = t_user.id
  where t_user.id = #{userId,jdbcType=INTEGER}
</select>

三、一个获取当前登录用户的例子

需求:前端给后端发送一个 GET 请求,请求路径是 /api/login/info,参数为空,后端响应给前端当前登录用户的数据。

/**
* 获取登录人信息
* @param authentication 注入的 SpringSecurity 的信息
* @return 包含当前登录人 tUser 对象信息的响应
*/
@GetMapping("/api/login/info")
public R getLoginInfo(Authentication authentication){
   TUser tUser = (TUser) authentication.getPrincipal();
   return R.ok(tUser);
}

四、一个响应前端退出登录的例子

需求:前端给后端发送一个 GET 请求,请求路径是 /api/logout,参数为空,后端清除相关的登录信息。

这里使用 SpringSecurity 提供的退出登录功能就好,不用自己写 Controller。

Step1 新建成功退出登录的处理器

~/config/handler/MyLogoutSuccessHandler.java,文件如下:

package com.sangui.config.handler;


import com.sangui.constant.Constants;
import com.sangui.model.TUser;
import com.sangui.result.CodeEnum;
import com.sangui.result.R;
import com.sangui.service.RedisService;
import com.sangui.util.JsonUtils;
import com.sangui.util.ResponseUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
* @Author: sangui
* @CreateTime: 2025-09-03
* @Description: 成功退出登录的处理器
* @Version: 1.0
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

   @Resource
   private RedisService redisService;

   /**
    * 退出成功,执行该方法,在该方法中返回 json 给前端,就行了
    * @param request request
    * @param response response
    * @param authentication authentication
    * @throws IOException IO 异常
    * @throws ServletException Servlet 异常
    */
   @Override
   public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       // 取出 authentication 里的 tUser
       TUser tUser = (TUser)authentication.getPrincipal();

       // 删除一下 Redis 中用户的 jwt
       redisService.removeValue(Constants.REDIS_JWT_KEY + tUser.getId());

       // 退出成功的统一结果
       R result = R.ok(CodeEnum.USER_LOGOUT);

       // 把 R 对象转成 json
       String resultJson = JsonUtils.toJson(result);

       // 把 R 以 json 返回给前端
       ResponseUtils.write(response, resultJson);
  }
}

Step2 在 SpringSecurity 的配置类中配置我们刚刚写的处理器

// 注入到我们的 Ioc 容器
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;

在 build 之前加入这样的退出登录代码:

.logout((logout) -> {
   logout.logoutUrl("/api/logout").logoutSuccessHandler(myLogoutSuccessHandler);
})
.build();

五、一个获取全部用户信息的例子

需求:前端给后端发送一个 GET 请求,请求路径是 /api/users,有一个参数 current,代表的是查询指定页数的用户信息,后端返回这些信息。

Step1 Controller 层

/**
* 获取全部用户的信息
* @param current 当前页数
* @return 当前页数的用户信息
*/
@GetMapping("/api/users")
public R getUsers(@RequestParam(value = "current",required = false)Integer current){
   if (current == null){
       current = 1;
  }
   PageInfo<TUser> pageInfo = userService.getUsersByPage(current);

   return R.ok(pageInfo);
}

响应给前端 pagehelper 格式的用户信息。

Step2 Service 层

  1. UserServce 接口

    /**
    * 根据页数查找用户信息
    * @param current 要查找的页数
    * @return 用户信息
    */
    PageInfo<TUser> getUsersByPage(Integer current);
  2. UserServiceImpl 实现类

    @Override
    public PageInfo<TUser> getUsersByPage(Integer current) {
       // 1. 设置 PageHelper
       PageHelper.startPage(current, Constants.PAGE_SIZE);
       // 2. 查询
       List<TUser> list = userMapper.selectUserByPage();
       // 3. 封装分页数据到 PageInfo
       return new PageInfo<>(list);
    }

Step3 Mapper 层

  1. TUserMapper 接口

    List<TUser> selectUserByPage();
  2. TUserMapper 映射文件

    <select id="selectUserByPage" resultMap="BaseResultMap">
      select
       <include refid="Base_Column_List"/>
      from t_user
    </select>

六、一个获取指定某个用户信息的例子

需求:前端给后端发送一个 GET 请求,请求路径是 /api/user/xx,参数为空,代表的是查询 id 为 xx 的指定用户的用户信息,后端返回这些信息。

这里需要注意的是,这里涉及到多表查询。

Step1 Controller 层

/**
* 响应给前端指定 id 的用户信息
* @param id 用户 id
* @return 用户信息
*/
@GetMapping("/api/user/{id}")
public R getUserDetail(@PathVariable("id")Integer id){
   TUser tUser = userService.getUserDetailById(id);
   return R.ok(tUser);
}

Step2 Service 层

  1. UserServce 接口

    /**
    * 根据 id 查询指定 id 的用户详情
    * @param id 用户 id
    * @return 用户对象
    */
    TUser getUserDetailById(Integer id);
  2. UserServiceImpl 实现类

    @Override
    public TUser getUserDetailById(Integer id) {
       return userMapper.selectByIdWithCreateAndEditUserName(id);
    }

Step3 Mapper 层

  1. TUserMapper 接口

    TUser selectByIdWithCreateAndEditUserName(Integer id);
  2. TUserMapper 映射文件

    <resultMap id="UserDetailMap" type="com.sangui.model.TUser">
       <id column="id" jdbcType="INTEGER" property="id"/>
       <result column="login_act" jdbcType="VARCHAR" property="loginAct"/>
       <result column="login_pwd" jdbcType="VARCHAR" property="loginPwd"/>
       <result column="name" jdbcType="VARCHAR" property="name"/>
       <result column="phone" jdbcType="VARCHAR" property="phone"/>
       <result column="email" jdbcType="VARCHAR" property="email"/>
       <result column="account_no_expired" jdbcType="INTEGER" property="accountNoExpired"/>
       <result column="credentials_no_expired" jdbcType="INTEGER" property="credentialsNoExpired"/>
       <result column="account_no_locked" jdbcType="INTEGER" property="accountNoLocked"/>
       <result column="account_enabled" jdbcType="INTEGER" property="accountEnabled"/>
       <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
       <result column="create_by" jdbcType="INTEGER" property="createBy"/>
       <result column="edit_time" jdbcType="TIMESTAMP" property="editTime"/>
       <result column="edit_by" jdbcType="INTEGER" property="editBy"/>
       <result column="last_login_time" jdbcType="TIMESTAMP" property="lastLoginTime"/>
       <!--一对一关联-->
       <association property="createByDO" javaType="com.sangui.model.TUser">
           <id column="createById" jdbcType="INTEGER" property="id"/>
           <result column="createByName" jdbcType="VARCHAR" property="name"/>
       </association>
       <association property="editByDO" javaType="com.sangui.model.TUser">
           <id column="editById" jdbcType="INTEGER" property="id"/>
           <result column="editByName" jdbcType="VARCHAR" property="name"/>
       </association>
    </resultMap>

    <select id="selectByIdWithCreateAndEditUserName" parameterType="java.lang.Integer" resultMap="UserDetailMap">
      select u1.*, u2.id createById, u2.name createByName, u3.id editById, u3.name editByName
      from t_user u1
                left join t_user u2
                          on u1.crea te_by = u2.id
                left join t_user u3
                          on u1.edit_by = u3.id
      where u1.id = #{id,jdbcType=INTEGER}
    </select>
  3. 额外步骤

    最后,还要在 TUser 类中加上两个一对一关联的属性

    // 两个一对一关联的属性
    private TUser createByDO;
    private TUser editByDO;

七、一个新增用户的例子

需求:前端给后端发送一个 POST 请求,请求路径是 /api/user,参数为新增的用户信息,后端新增一个用户并存储这些信息。

Step1 Query 层

package com.sangui.query;


import lombok.Data;

/**
* @Author: sangui
* @CreateTime: 2025-09-06
* @Description: 前端返回的 TUser 的对象
* @Version: 1.0
*/
@Data
public class UserQuery {
   private Integer id;
   private String loginAct;
   private String loginPwd;
   private String name;
   private String phone;
   private String email;
   private Integer accountNoExpired;
   private Integer credentialsNoExpired;
   private Integer accountNoLocked;
   private Integer accountEnabled;
}

Step2 Controller 层

/**
* 新增用户
* @param userQuery 前端传过来的用户信息
* @return 响应前端 o 不 ok
*/
@PostMapping("/api/user")
public R addUser(UserQuery userQuery,Authentication authentication) {
   int count = userService.addUser(userQuery,authentication);
   return count >= 1 ? R.ok() : R.fail();
}

Step3 Service 层

  1. UserServce 接口

    /**
    * 新增用户
    * @param userQuery userQuery 前端传过来的用户信息
    * @param authentication 用于获取创建人信息
    * @return 数据库改变条数
    */
    int addUser(UserQuery userQuery, Authentication authentication);
  2. UserServiceImpl 实现类

    @Resource
    PasswordEncoder passwordEncoder;

    @Override
    public int addUser(UserQuery userQuery,Authentication authentication) {
       TUser tUser = new TUser();
       BeanUtils.copyProperties(userQuery, tUser);

       // 添加创建人 id
       TUser createByDO = (TUser) authentication.getPrincipal();
       Integer createBy = createByDO.getId();
       tUser.setCreateBy(createBy);

       // 添加创建时间
       tUser.setCreateTime(new Date());

       // 加密原文密码
       tUser.setLoginPwd(passwordEncoder.encode(userQuery.getLoginPwd()));

       return userMapper.insertSelective(tUser);
    }

最后,Mapper 层不需要写,我们在 Service 层中用的是自动生成的 Mapper 层代码。

八、一个编辑用户的例子

需求:前端给后端发送一个 POST 请求,请求路径是 /api/user,参数为 UserQuery 的 form 表单,要求根据 UserQuery 编辑对应 id 的用户信息。

Step1 Controller 层

/**
* 编辑用户
* @param userQuery 前端传过来的用户信息
* @return 响应前端 o 不 ok
*/
@PutMapping("/api/user")
public R editUser(UserQuery userQuery,Authentication authentication) {
   int count = userService.editUser(userQuery,authentication);
   return count >= 1 ? R.ok() : R.fail();
}

Step2 Service 层

  1. UserServce 接口

    /**
    * 编辑用户
    * @param userQuery userQuery 前端传过来的用户信息
    * @param authentication 用于获取编辑人信息
    * @return 数据库改变条数
    */
    int editUser(UserQuery userQuery, Authentication authentication);
  2. UserServiceImpl 实现类

    @Override
    public int editUser(UserQuery userQuery, Authentication authentication) {
       TUser tUser = new TUser();
       BeanUtils.copyProperties(userQuery, tUser);

       // 添加修改人 id
       TUser createByDO = (TUser) authentication.getPrincipal();
       Integer editBy = createByDO.getId();
       tUser.setEditBy(editBy);

       // 添加修改时间
       tUser.setEditTime(new Date());

       // 加密
       if (tUser.getLoginPwd().isEmpty()) {
           tUser.setLoginPwd(null);
      }else if (tUser.getLoginPwd().length() < 6 || tUser.getLoginPwd().length() > 16) {
           return 0;
      }else{
           tUser.setLoginPwd(passwordEncoder.encode(tUser.getLoginPwd()));
      }

       return userMapper.updateByPrimaryKeySelective(tUser);
    }

最后,Mapper 层不需要写,我们在 Service 层中用的是自动生成的 Mapper 层代码。

九、一个删除用户的例子

需求:前端给后端发送一个 DELETE 请求,请求路径是 /api/user/xx,参数为空,要求删除 id 为 xx 的用户。同时还有一个批量删除的需求,请求路径是 /api/users,参数为 id 数组,类似 "2,4,5",就是要求删除 id 为 2、4 和 5 的用户。

Step1 Controller 层

/**
* 删除用户
* @param id 前端传过来的指定 id 的用户
* @return 响应前端 o 不 ok
*/
@DeleteMapping("/api/user/{id}")
public R delUser(@PathVariable("id") Integer id) {
   int count = userService.delUserById(id);
   return count >= 1 ? R.ok() : R.fail();
}

/**
* 批量删除用户
* @param ids id 字符串,类似 "2,4,5"
* @return 响应前端 o 不 ok
*/
@DeleteMapping("/api/users")
public R batchDelUser(@RequestParam(value = "ids", required = false) String ids) {
   int count = userService.delUserByIds(ids);
   // System.out.println(ids);
   int len = ids.split(",").length;
   return count >= len ? R.ok() : R.fail();
}

Step2 Service 层

  1. UserServce 接口

    /**
    * 删除指定 id 的用户
    * @param id id
    * @return 数据库改变条数
    */
    int delUserById(Integer id);

    /**
    * 批量删除用户
    * @param ids id 字符串,类似 "2,4,5"
    * @return 数据库改变条数
    */
    int delUserByIds(String ids);
  2. UserServiceImpl 实现类

    @Override
    public int delUserById(Integer id) {
       return userMapper.deleteByPrimaryKey(id);
    }

    @Override
    public int delUserByIds(String ids) {
       return userMapper.deleteByIds(ids);
    }

Step3 Mapper 层

  1. TUserMapper 接口

    int deleteByIds(String ids);
  2. TUserMapper 映射文件

    使用 SQL 语句中的 in 关键词,实现批量删除,注意这里注入是 $,而不是 #,因为这是字符串。

    <delete id="deleteByIds" parameterType="java.lang.String">
      delete
      from t_user
      where id in (${id,jdbcType=INTEGER})
    </delete>

十、一个权限 aop 的实现的例子

Step1 具体的切面类

package com.sangui.aspect;


import com.sangui.common.DataScope;
import com.sangui.constant.Constants;
import com.sangui.model.TUser;
import com.sangui.util.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.List;

/**
* @Author: sangui
* @CreateTime: 2025-09-08
* @Description:
* @Version: 1.0
*/
@Aspect
@Component
public class DataScopeAspect {

   // aspectJ 实现 aop

   // 切入点,切在注解上
   @Pointcut(value = "@annotation(com.sangui.common.DataScope)")
   private void pointCut() {

  }

   @Around(value = "pointCut()")
   public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
       MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

       // 拿到方法上的注解
       DataScope dataScope = methodSignature.getMethod().getDeclaredAnnotation(DataScope.class);

       String tableAlias = dataScope.tableAlias();
       String tableField = dataScope.tableField();

       // 在 spring web 容器中,可以拿到当前请求的 request 对象
       HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

       String token = request.getHeader(Constants.TOKEN_NAME);
       // 从 token 中解析出该用户是管理员还是普通用户
       TUser tUser = JwtUtils.parseUserFromJwt(token);

       // 拿到用户的角色
       List<String> roleList = tUser.getRoleList();
       // System.out.println("roleList:" + roleList);
       Object[] args = joinPoint.getArgs();

       // 不包含 admin 角色,只查当前用户自己的数据,否则查所有数据
       if (!roleList.contains("admin")) {
           // 拿方法的第一个参数

           if (args.length > 0 && args[0] instanceof String) {
               String filterSql = (String) args[0];
               // 假设原 filterSql 可能已有条件,因此追加而非覆盖
               // 如果原设计是覆盖,可改为直接赋值;但根据描述,建议追加以支持其他过滤
               filterSql += " and " + tableAlias + "." + tableField + " = " + tUser.getId();
               args[0] = filterSql;
               // System.out.println("filterSql:" + filterSql);
          }
      }

       // System.out.println("目标方法执行之前....");
       Object result = joinPoint.proceed(args);
       // System.out.println("目标方法执行之后....");
       return result;
  }
}

Step2 加入切面

package com.sangui.mapper;

import com.sangui.common.DataScope;
import com.sangui.model.TUser;
import org.springframework.security.core.Authentication;

import java.util.List;

/**
* @author sangui
*/
public interface TUserMapper {
   int deleteByPrimaryKey(Integer id);

   int insert(TUser record);

   int insertSelective(TUser record);

   TUser selectByPrimaryKey(Integer id);

   int updateByPrimaryKeySelective(TUser record);

   int updateByPrimaryKey(TUser record);

   TUser selectByLoginAct(String loginAct);

   // 加入切面,并在方法中加入参数
   @DataScope(tableAlias = "tu",tableField = "id")
   List<TUser> selectUserByPage(String filterSql);

   TUser selectByIdWithCreateAndEditUserName(Integer id);

   int deleteByIds(String ids);
}

同时,还要修改之前的 Service 方法,把方法参数加上。

修改 mapper 映射文件,加上写好的表别名,where 条件:

<select id="selectUserByPage" resultMap="BaseResultMap">
  select
   <include refid="Base_Column_List"/>
  from t_user tu
   <where>
      ${filterSql}
   </where>
</select>
  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录