项目开发流程(前端 · 后端)(终版)
适合需要把“能跑的原型”工程化的开发者:登录鉴权、JWT + Redis 会话、增删改查、分页、权限切面、缓存策略等常见模块都有实战代码。
本版在初版基础上精简并补充了生产级的注意点(序列化、异常处理、缓存回退、数据隔离等),更容易拿来即用。 阅读建议:先按“快速启动”把前后端连通,再按模块查阅需要的实现与代码片段。
前端开始:
由于我主要研究的是 Java 后端方面的开发,但目前开发流程要求程序员必须同时精通前端和后端。所以,在这本手册中,后端方面的手册,我会简略写。前端方面的手册,我会详细写。
一、新建 SpringBoot 项目步骤
Step1 用 Vite 脚手架工具创建 Vue 项目
在需要创建项目目录的 Dos 窗口输入以下内容,通过 vite 获取最新版本的 Vue 项目结构:
npm create vite@latestStep2 键入该 Vue 项目的名称
继续在原 Dos 窗口输入该项目名称,如:
my-vue-projectStep3 选择 Vue 项目
Vite 工具还可以创建除了 Vue 项目之外的其他项目,这里选择 Vue 项目。
Step4 选择项目语言
有 TypeScript 和 JavaScript 等语言供选择,推荐选择 TypeScript。
Step5 安装项目依赖
此时,Vue 的项目已经创建完成。
cd 进入刚刚创建的项目,在新的目录输入以下命令:
npm installStep6 启动项目
继续在项目里输入以下命令以启动此 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 --savevue-router:(路由组件)
npm install vue-router --saveaxios:(axios 异步请求)
npm install axios --savexxx:
最后:
编辑 ~\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 调整项目文件
可删除
~\src\style.css可删除
~\src\components目录及里面的所有文件编辑
~\index.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>编辑
~\src\App.vue文件中的所有内容,变成如下内容:<template>
<!--渲染路由地址所对应的页面组件-->
<router-view/>
</template>编辑
~/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,
}
})创建
~/src/view/LoginVue.vue这里的
view文件夹需额外创建。文件内容为:
<template>
<h1>Hello,这里是系统的首页!</h1>
</template>
<script>
import {defineComponent} from 'vue'
export default defineComponent({
name: "LoginVue",
})
</script>
<style scoped>
</style>创建
~/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数组里面的内容,每增加一个页面,就新增一个路由
创建
~/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',
}
)
}创建
~/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 在官网找合适的表单组件
我找的是:
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 添加表单的前端验证
为表单添加规则字段
:rules为字段添加属性字段
prop注册上述两步的字段
添加详细的验证规则
<!--为需要添加验证的表单,添加 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 添加表单的提交验证
为需要添加登录验证的表单,添加 ref
在提交方法中验证输入框的合法性(共用上一步写的验证)
选择数据,发送请求
根据请求的响应,做出不同的选择......
若响应信息为做出登录请求,则进入系统主页,这里要入路由模块
<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 使用合适的布局组件
在官网找合适的布局组件
我找的是:,具体是里面的:

找到想要的复制到自己的程序中
复制所有可以用到的组件,组件都是在
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>修改成自己所需要的
先写好一切写死的东西
<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);
}
Step2 使用合适的侧导航栏目组件
在官网找合适的侧导航栏组件
我找的是:,具体是这样的:

找到想要的复制到自己的程序中
复制所有可以用到的组件,组件都是在
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>修改成自己所需要的侧导航栏
先搭建好基础的侧导航栏架子
<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>在对应地方加入图标
在这里找合适的图标:,粘贴进程序即可,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-aside的width设置成变量,如:<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 完成顶部导航条设计
顶部导航条的设计只有一个要求,就是,显示当前用户的用户姓名,然后点击可以查看我的资料、修改密码、退出登录。
查看我的资料和修改密码暂时不实现。先实现动态显示用户姓名和退出登录功能。
设计下拉框
选择一个合适的下拉栏,并作出自己所需要的效果,如:
<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;
}动态显示用户姓名功能实现
<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;
})
},
},退出登录功能实现
<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 用户管理的结构的设计
开启 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 里写下这个路径。
使用子路由
在之前 dashboard 这个路由的基础之上,添加我们用户页的路由
{
path: "/dashboard",
component: () => import("../view/DashboardView.vue"),
// 子路由,子路由可以是多个,所以是数组
children: [
{
// 子路由里面的每一个组件,都是和路由一样,path 和 component
// 唯一的区别是,子路由的 path 不能带斜杠
path: "user",
component: () => import("../view/UserView.vue"),
}
]
},将我们的需要渲染子路由的地方,即 dashboard 的主页面上写上
<router-view/><!--主区域开始-->
<el-main>
<router-view/>
</el-main>
<!--主区域结束-->
这样,我们的 UserVier.vue 就运行在 dashboard 页面的主页面里了。
Step2 用户管理的界面的实现
界面上组件的先完成堆砌
<!--两个按钮-->
<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>加载后端的数据
使用 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;
}
})
},完成底部的页码样式
在表格的下方加入页码栏,如:
<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;
}完成底部的页码功能
在分页组件中添加三个属性,是
@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 用户管理的查看功能的实现
需求:点击对应用户的详情按钮,展示出该用户的所有字段信息。
添加路由信息
因为要跳转新的页面,所以需要添加路由信息,我们依然是在 main 页面做改变,所以,还是添加子路由,在之前的 user 子路由后面加上我们新的子路由。如:
children: [
{
path: "user",
component: () => import("../view/UserView.vue"),
},
{
// id 是动态的,所以前面加一个冒号
path: "user/:id",
component: () => import("../view/UserDetailView.vue"),
}
]注意我们这里是用动态的地址,路径为:
/dashboard/user/1,最后面的数字根据不同的用户而不同。所以在路由的 path 中带一个冒号作为动态。界面的跳转
点击某个用户列表的详情按钮,跳转到对应用户的详情页。
<!--使用 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)
},用户详情页的设计
<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">
<!-- 写上 是为了防止后面的数据未空,div 里空值-->
{{ userDetail.id }}
</div>
</el-form-item>
<el-form-item label="账户">
<div class="div-item">
{{ userDetail.loginAct }}
</div>
</el-form-item>
<el-form-item label="密码">
<div class="div-item">
<!--密码直接写死-->
******
</div>
</el-form-item>
<el-form-item label="姓名">
<div class="div-item">
{{ userDetail.name }}
</div>
</el-form-item>
<el-form-item label="手机">
<div class="div-item">
{{ userDetail.phone }}
</div>
</el-form-item>
<el-form-item label="邮箱">
<div class="div-item">
{{ userDetail.email }}
</div>
</el-form-item>
<el-form-item label="账户是否过期">
<div class="div-item">
{{ userDetail.accountNoExpired == '1' ? '未过期' : '已过期' }}
</div>
</el-form-item>
<el-form-item label="密码是否过期">
<div class="div-item">
{{ userDetail.credentialsNoExpired == '1' ? '未过期' : '已过期' }}
</div>
</el-form-item>
<el-form-item label="账户是否锁定">
<div class="div-item">
{{ userDetail.accountNoLocked == '1' ? '未锁定' : '已锁定' }}
</div>
</el-form-item>
<el-form-item label="账户是否启用">
<div class="div-item">
{{ userDetail.accountEnabled == '1' ? '启用中' : '未启用' }}
</div>
</el-form-item>
<el-form-item label="账户创建时间">
<div class="div-item">
{{ userDetail.createTime }}
</div>
</el-form-item>
<el-form-item label="创建人">
<div class="div-item">
{{ userDetail.createByDO.name }}
</div>
</el-form-item>
<el-form-item label="上次账户编辑时间">
<div class="div-item">
{{ userDetail.editTime }}
</div>
</el-form-item>
<el-form-item label="编辑人">
<div class="div-item">
{{ userDetail.editByDO.name }}
</div>
</el-form-item>
<el-form-item label="上次账户登录时间">
<div class="div-item">
{{ 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 用户管理的新增功能的实现
需求:点击添加用户按钮,输入新用户的信息,新增一个用户。
页面准备
<!--这是新增用户的弹窗-->
<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: "",
},
}
},添加前端验证
<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");
}
})
}
})
},更新成功后实现局部刷新
修改路由
因为我们想实现仅 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 用户管理的删除功能的实现
需求:点击对应用户的删除按钮,可以删除该用户。同时,也可以使用批量删除用户功能。
单个删除
<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")
})
},批量删除
增加点击函数
<el-button type="danger" ="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")
})
},
五、一个选择的例子
需求:有一个选择负责人的单选框,单选框里面是数据库中所有的用户,可以选择任意一个用户作为负责人。
界面实现
<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>数据绑定
data() {
return {
ownerOptions: [],
}
}方法实现
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 端口
port8089
servlet
# 项目路径直接是斜杠
context-path/
# 配置数据库连接相关信息
spring
datasource
typecom.zaxxer.hikari.HikariDataSource
urljdbcmysql//localhost3306/xxxxxxxxx?useUnicode=true&characterEncoding=utf8&useSSL=false
driver-class-namecom.mysql.cj.jdbc.Driver
usernameroot
passwordxxxxxxxxxxxxxxx
hikari
maximum-pool-size30
minimum-idle5
connection-timeout5000
idle-timeout600000
max-lifetime18000000
data
# 配置 redis 的连接信息
redis
host127.0.0.1
port6379
password
database1
jackson
time-zoneGMT+8
date-formatyyyy-MM-dd HHmmss
# 指定以下 mapper.xml 文件的位置
mybatis
mapper-locationsclasspathmapper/*.xmlStep3 修改启动类
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
*/
(basePackages = {"com.sangui.mapper"})
public class DlykServerApplication implements CommandLineRunner {
private RedisTemplate<String, Object> redisTemplate;
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 类
~/util/JsonUtils.javapackage 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);
}
}
}~/util/JwtUtils.javapackage 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);
}
}
}~/util/ResponseUtils.javapackage 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();
}
}
}
}~/util/ResponseUtils.javapackage 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 类
~/result/CodeEnum.javapackage com.sangui.result;
import lombok.*;
/**
* @Author: sangui
* @CreateTime: 2025-08-30
* @Description: R 状态的枚举类,返回给前端的 Code 枚举类。包含两个属性,code 和 msg
* @Version: 1.0
*/
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;
/**
* 结果信息
*/
private String msg;
}~/result/R.javapackage 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
*/
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 类
~/constant/Constants.javapackage 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 类
~/service/RedisService.javapackage 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);
}~/service/impl/RedisServiceImpl.javapackage 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
*/
public class RedisServiceImpl implements RedisService {
private RedisTemplate<String, Object> redisTemplate;
public void setValue(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public Object getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
public void removeValue(String key) {
redisTemplate.delete(key);
}
public void expire(String key, Long timeOut, TimeUnit timeUnit) {
redisTemplate.expire(key, timeOut, timeUnit);
}
}
5) Config 类
~/config/handler/GlobalExceptionHandler.javapackage 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 中的所有方法
// aop。拦截标注了 @Controller 的 controller 中的所有方法
// @ControllerAdvice
public class GlobalExceptionHandler {
/**
* 异常处理的方法(Controller 方法发生了异常,那么就使用该方法来处理)
* @return 响应结果
*/
(value = Exception.class)
public R handException(Exception e) {
// 在控制台打印异常信息
e.printStackTrace();
return R.fail(e.getMessage());
}
/**
* 异常的精确匹配,先精确匹配,匹配不到了,就找父类的异常处理
* @param e 异常
* @return 响应结果
*/
(value = DataAccessException.class)
public R handException3(DataAccessException e) {
// 在控制台打印异常信息
e.printStackTrace();
return R.fail(CodeEnum.DATA_ACCESS_EXCEPTION);
}
/**
* 权限不足的异常处理
* @param e 异常
* @return 响应结果
*/
(value = AccessDeniedException.class)
public R handException(AccessDeniedException e) {
// 在控制台打印异常信息
e.printStackTrace();
return R.fail(CodeEnum.ACCESS_DENIED);
}
}
6) Common 类
~/common/DataScope.javapackage com.sangui.common;
import java.lang.annotation.*;
/**
* @Author: sangui
* @CreateTime: 2025-09-08
* @Description: 数据范围的注解
* @Version: 1.0
*/
(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
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 类
~/manager/RedisManager.javapackage 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
*/
public class RedisManager {
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 的实体类
类中加入两个属性,示例如下:
// 角色 List
private List<String> roleList;
// 权限标识符 List
private List<String> permissionList;实现 UserDetails 接口
重写 UserDetails 接口的七个方法,示例如下:
// 实现 UserDetails 的七个方法
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;
}
public String getPassword() {
return this.getLoginPwd();
}
public String getUsername() {
return this.getLoginAct();
}
public boolean isAccountNonExpired() {
return this.getAccountNoExpired() == 1;
}
public boolean isAccountNonLocked() {
return this.getAccountNoLocked() == 1;
}
public boolean isCredentialsNonExpired() {
return this.getCredentialsNoExpired() == 1;
}
public boolean isEnabled() {
return this.getAccountEnabled() == 1;
}
Step 2 创建项目所需的类
1) Config 类
~/config/handler/MyAuthenticationSuccessHandler.javapackage 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
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedisService redisService;
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);
}
}~/config/handler/MyAuthenticationFailureHandler.javapackage 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
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
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);
}
}~/config/filter/TokenVerifyFilter.javapackage 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
*/
public class TokenVerifyFilter extends OncePerRequestFilter {
private RedisService redisService;
// SpringBoot 框架的 IoC 容器中已经创建好了该线程池,可以注入直接使用
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
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);
}
}
}~/config/SecurityConfig.javapackage 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
*/
public class SecurityConfig {
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
private TokenVerifyFilter tokenVerifyFilter;
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();
}
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 类
~/service/UserService.javapackage 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 {
}~/service/impl/UserServiceImpl.javapackage 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
*/
public class UserServiceImpl implements UserService {
TUserMapper userMapper;
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
*/
public class UserController {
/**
* 判断是否可以免密登录
* @return 判断结果
*/
("/api/login/free")
public R freeLogin(){
// TokenVerifyFilter 会自动验证,这里不需要验证
return R.ok();
}
}Step4 给用户加上权限信息
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 对象信息的响应
*/
("/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
*/
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
private RedisService redisService;
/**
* 退出成功,执行该方法,在该方法中返回 json 给前端,就行了
* @param request request
* @param response response
* @param authentication authentication
* @throws IOException IO 异常
* @throws ServletException Servlet 异常
*/
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 当前页数的用户信息
*/
("/api/users")
public R getUsers((value = "current",required = false)Integer current){
if (current == null){
current = 1;
}
PageInfo<TUser> pageInfo = userService.getUsersByPage(current);
return R.ok(pageInfo);
}响应给前端 pagehelper 格式的用户信息。
Step2 Service 层
UserServce 接口
/**
* 根据页数查找用户信息
* @param current 要查找的页数
* @return 用户信息
*/
PageInfo<TUser> getUsersByPage(Integer current);UserServiceImpl 实现类
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 层
TUserMapper 接口
List<TUser> selectUserByPage();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 用户信息
*/
("/api/user/{id}")
public R getUserDetail(("id")Integer id){
TUser tUser = userService.getUserDetailById(id);
return R.ok(tUser);
}Step2 Service 层
UserServce 接口
/**
* 根据 id 查询指定 id 的用户详情
* @param id 用户 id
* @return 用户对象
*/
TUser getUserDetailById(Integer id);UserServiceImpl 实现类
public TUser getUserDetailById(Integer id) {
return userMapper.selectByIdWithCreateAndEditUserName(id);
}
Step3 Mapper 层
TUserMapper 接口
TUser selectByIdWithCreateAndEditUserName(Integer id);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>额外步骤
最后,还要在 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
*/
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
*/
("/api/user")
public R addUser(UserQuery userQuery,Authentication authentication) {
int count = userService.addUser(userQuery,authentication);
return count >= 1 ? R.ok() : R.fail();
}Step3 Service 层
UserServce 接口
/**
* 新增用户
* @param userQuery userQuery 前端传过来的用户信息
* @param authentication 用于获取创建人信息
* @return 数据库改变条数
*/
int addUser(UserQuery userQuery, Authentication authentication);UserServiceImpl 实现类
PasswordEncoder passwordEncoder;
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
*/
("/api/user")
public R editUser(UserQuery userQuery,Authentication authentication) {
int count = userService.editUser(userQuery,authentication);
return count >= 1 ? R.ok() : R.fail();
}Step2 Service 层
UserServce 接口
/**
* 编辑用户
* @param userQuery userQuery 前端传过来的用户信息
* @param authentication 用于获取编辑人信息
* @return 数据库改变条数
*/
int editUser(UserQuery userQuery, Authentication authentication);UserServiceImpl 实现类
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
*/
("/api/user/{id}")
public R delUser(("id") Integer id) {
int count = userService.delUserById(id);
return count >= 1 ? R.ok() : R.fail();
}
/**
* 批量删除用户
* @param ids id 字符串,类似 "2,4,5"
* @return 响应前端 o 不 ok
*/
("/api/users")
public R batchDelUser((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 层
UserServce 接口
/**
* 删除指定 id 的用户
* @param id id
* @return 数据库改变条数
*/
int delUserById(Integer id);
/**
* 批量删除用户
* @param ids id 字符串,类似 "2,4,5"
* @return 数据库改变条数
*/
int delUserByIds(String ids);UserServiceImpl 实现类
public int delUserById(Integer id) {
return userMapper.deleteByPrimaryKey(id);
}
public int delUserByIds(String ids) {
return userMapper.deleteByIds(ids);
}
Step3 Mapper 层
TUserMapper 接口
int deleteByIds(String ids);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
*/
public class DataScopeAspect {
// aspectJ 实现 aop
// 切入点,切在注解上
(value = "@annotation(com.sangui.common.DataScope)")
private void pointCut() {
}
(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);
// 加入切面,并在方法中加入参数
(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>- 微信
- 赶快加我聊天吧

- 赶快加我聊天吧
