title:神盾局特工管理系统
功能:系统管理,查询用户、增删改查。
项目特点:简单简单蛋蛋蛋
B站视频地址:https://www.bilibili.com/video/BV1dG4y1T7yp
作者原帖:https://blog.csdn.net/m0_37613503/article/details/132610271
一、项目概述
- 目标:理解前后端,具备独立搭建前后端分离项目的能力。
- 开发模式:前端+后端+数据库。
- 技术栈:
- 前端:vue+vuex+ElementUI+Axios+vue-element-admin说明:前端框架、全局状态管理框架、前端UI框架、前端HTTP框架、项目脚手架。
- 后端:springBoot+Mybatis+Mybatis-plus+Redis 说明:容器+MVC框架、ORM框架、MyBatis增强工具、非关系型数据库。
二、数据库xdb
-
创建数据库:xdb
-
创建用户表、角色表、菜单表、用户角色映射表、角色菜单映射表
三、前端笔记
(1)node环境
-
node要求16.12.0版本以下,否则和vue-admin-template不兼容。
-
可以使用nvm降低node版本,https://juejin.cn/post/7094576504243224612
-
可以通过nvm install xxx的操作,安装不同版本的node,nvm ls查看本地已经安装过的node版本,再nvm use xxx就可以快速切换node版本啦
-
注意:不用配置环境变量,注意新版本的node文件夹中应该包含的文件。
-
设置npm的全局安装路径和缓存路径的。
npm config set prefix “您想创建文件的global”
npm config set cache “您想创建文件的cache”。
(2)下载vue-admin-template
(3)项目初始化
- npm install下载moudle
- npm run dev运行项目
(4)修改文字、图片、菜单、导航栏
(5)增加选项卡,标签栏导航——代码基本固定的
-
npm install下载moudle
- install卡住问题
第一种方案、首先检查npm代理,是否已经使用国内镜像
// 执行以下命令查看是否为国内镜像npm config get registry
出现如下所示,表明已经为国内镜像无需再修改
如果不是则换成国内镜像,执行以下命令
npm config set registry=https://registry.npmmirror.com
原文链接:https://blog.csdn.net/shi450561200/article/details/134354871
-
npm run dev运行项目
(6)梳理项目结构
├── build # 构建相关
├── mock # 项目mock 模拟数据
├── plop-templates # 基本模板
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter
│ ├── icons # 项目所有 svg icons
│ ├── lang # 国际化 language
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json
(7)修改一些配置项
- 修改前端项目部署端口→vue.config.js中修改
- lintaOnSave的修改→在vue.config.js中,改成false;
- 是否打开默认浏览器:devServer→在vue.config.js中,open的value值改成true;
- mock模拟数据的服务也在vue.config.js中,在后端搭起后要关掉。
(8)添加标签栏导航
- 官方具体网址:快捷导航(标签栏导航) | vue-element-admin (panjiachen.github.io)
- 具体过程:
- 首先在@/layout/components/AppMain.vue中添加
- 首先在@/layout/components/AppMain.vue中添加
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
cachedViews() {
return this.$store.state.tagsView.cachedViews
}
2. 复制vue-element-admin项目中的文件到相应的目录中
@/layout/components/TagsView
@/store/modules/tagsView.js
@/store/modules/permission.js
3. 修改vuex中store文件夹下的getters.js
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
permission_routes: state => state.permission.routes
4. 将tagsView放到全局状态管理器中,在store的index.js中引入tagsView
5. 在布局中导出tagsView,在文件@layout\components\index.js新增
export { default as TagsView } from './TagsView'
6. 在布局中使用tagsView
7. Affix 固钉
遇到一个报错:/deep/深度选择器报错(样式穿透的一种方法),是node版本过高的原因,可以改为::v-deep
(9)梳理登录接口
- 后端的响应数据
{"code":20000,"data":{"token":"admin-token"}}
{
"code": 20000,
"data": {
"roles": [
"admin"
],
"introduction": "I am a super administrator",
"avatar": "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
"name": "Super Admin"
}
}
{"code":20000,"data":"success"}
四、后端笔记
(1)项目初始化
1. 创建springboot项目:2.7.8
2. 添加pom依赖
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3. yml
server:
port: 9999
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///xdb
logging:
level:
com.fangfang: debug
4. idea版本太低与低版本的jdk和springboot不兼容
重装idea新版:[https://blog.csdn.net/bobby102/article/details/136325397](https://blog.csdn.net/bobby102/article/details/136325397)
5. **完成之后运行一下**
(2)使用MyBatis-plus代码生成器
- 在该目录下建立一个代码生成器类CodeGenerator,运行代码快速生成基础包:控制器、实体类、mapper、和service等等
public static void main(String[] args) {
//代码生成器,可以复用。下次只需要改前面自定义的module名字、mapper路径以及表名。
String url = "jdbc:mysql:///xdb";
String username = "root";
String password = "root";
String author = "fangfang";
String outputDir = "D:\\Code\\IdeaProjects\\Xdun\\x-admin\\src\\main\\java";
String basePackage = "com.fangfang";
String moduleName = "sys";
String mapperLocation = "D:\\Code\\IdeaProjects\\Xdun\\x-admin\\src\\main\\resources\\mapper\\" + moduleName;
String tableName = "x_user,x_menu,x_role,x_role_menu,x_user_role";
String tablePrefix = "x_";
FastAutoGenerator.create(url, username, password)
.globalConfig(builder -> {
builder.author(author) // 设置作者
// 不开启Swagger模式、不覆盖
// .enableSwagger() // 开启 swagger 模式
// .fileOverride() // 覆盖已生成文件
.outputDir(outputDir); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent(basePackage) // 设置父包名
.moduleName(moduleName) // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, mapperLocation)); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude(tableName) // 设置需要生成的表名
.addTablePrefix(tablePrefix); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
- 最后记得在入口文件扫描mapper
![](https://secure2.wostatic.cn/static/eeqmHxT2u3pcRjbvZm1FBd/image.png)
@MapperScan("com.fangfang.*.mapper")
- 运行入口文件 测试一下有没有问题(一直没测试失败的原因好像是:没有添加数据源)
(2)测试
- 使用自带的测试类测试UserMapper是否能使用
package com.fangfang;
import com.fangfang.sys.entity.User;
import com.fangfang.sys.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class XAdminApplicationTests {
// 注入资源
@Resource
UserMapper userMapper;
@Test
void contextLoads() {
List<User> users = userMapper.selectList(null);
for (User user : users) {
System.out.println(user);
}
// users.forEach(System.out::println);
}
}
- controller暴露接口,测试service
**//@Controller
//@Controller 注解会默认输出视图,而前后端对接需要输出json对象,应该使用 @RestController**
@RestController
@RequestMapping("/user")
public class UserController {
// 暴露接口,测试service
@Autowired
private IUserService userService;
@GetMapping("/all")
public List<User> getAll() {
return userService.list();
}
}
另一种方法:
@RequestMapping("/user")
public class UserController {
// 暴露接口,测试service
@Autowired
private IUserService userService;
@GetMapping("/all")
@ResponseBody
//可以使用ResponseBody来将返回的视图变成json对象
public List<User> getAll() {
return userService.list();
}
}
(3)设置公共响应类,统一返回数据格式。(目的:方便与前端对接)
- 设置公共响应类
package com.fangfang.common.vo;
//vo:value object 值对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
//生成数据的getter/setter方法
@NoArgsConstructor
//无参构造
@AllArgsConstructor
//全参构造
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success() {
return new Result<T>(20000, "success", null);
}
public static <T> Result<T> success(T data) {
return new Result<T>(20000, "success", data);
}
public static <T> Result<T> success(String message) {
return new Result<T>(20000, message, null);
}
public static <T> Result<T> success(String message, T data) {
return new Result<T>(20000, message, data);
}
public static <T> Result<T> fail() {
return new Result<T>(20001, "fail", null);
}
public static <T> Result<T> fail(String message) {
return new Result<T>(20001, message, null);
}
public static <T> Result<T> fail(Integer code) {
return new Result<T>(code, "fail", null);
}
public static <T> Result<T> fail(Integer code, String message) {
return new Result<T>(code, message, null);
}
}
- 使用公共响应类返回统一的数据(同理还有另一种方法)
//@Controller
//@Controller 注解会默认输出试图,而前后端对接需要输出json对象,应该使用 @RestController
@RestController
@RequestMapping("/user")
public class UserController {
// 暴露接口,测试service
@Autowired
private IUserService userService;
@GetMapping("/all")
public Result<List<User>> getAll() {
return Result.success("查询成功",userService.list());
}
}
(4)登录相关接口
- 登录(用到postman/redis/redis desktop manager工具)
- controller 新增登录接口类
// 新增登录接口,注意@RequestBody注解,转换成json字符串
@PostMapping("/login")
public Result<Map<String,Object>> login(@RequestBody User user) {
Map<String,Object> data = userService.login(user);
if (data!=null){
return Result.success(data);
}
return Result.fail(20002,"用户名或密码错误");
}
2. service 生成登录接口
public interface IUserService extends IService<User> {
// 生成登录接口
Map<String, Object> login(User user);
}
3. service用户登录对应的实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
// 注入redis对应配置对象
@Autowired
private RedisTemplate redisTemplate;
// 用户登录对应的实现类
@Override
public Map<String, Object> login(User user) {
// 根据用户名密码查询
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,user.getUsername());
wrapper.eq(User::getPassword,user.getPassword());
User loginUser = this.baseMapper.selectOne(wrapper);
// 结果不为空,则生成token,并将用户信息存入redis
if (loginUser!=null){
// 暂时用uuid,终极方案是jwt
String key = "user" + UUID.randomUUID();
// 存入redis
loginUser.setPassword(null);
redisTemplate.opsForValue().set(key,loginUser,30, TimeUnit.MINUTES);
// 返回数据
Map<String, Object> data = new HashMap<>();
data.put("token",key);
return data;
}
return null;
}
}
4. redis配置存储token信息
@Configuration
public class MyRedisConfig {
// 注入连接工厂
@Resource
private RedisConnectionFactory factory;
// 配置bean
@Bean
public RedisTemplate redisTemplate(){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(factory);
// 序列化处理,键值对
redisTemplate.setKeySerializer(new StringRedisSerializer());
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
// 对象映射:对特定数据如日期,时区做一些设置,反序列化就会简单
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
om.setTimeZone(TimeZone.getDefault());
om.configure(MapperFeature.USE_ANNOTATIONS, false);
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
serializer.setObjectMapper(om);
return redisTemplate;
}
}
5. postman和Redis Desktop manager测试redis和接口
- 获取用户信息(逻辑:根据携带的token从redis中获取用户信息,然后返回给浏览器)
- controller层
@GetMapping("/info")
@ResponseBody
public Result<Map<String,Object>> getUserInfo(@RequestParam("token") String token) {
// 根据token获取用户信息,redis
Map<String, Object> data = userService.getUserInfo(token);
if(data!=null){
return Result.success(data);
}
return Result.fail(20003,"用户信息获取失败");
}
2. service层 (接口和实现类)
@Override
public Map<String, Object> getUserInfo(String token) {
Object obj = redisTemplate.opsForValue().get(token);
if(obj != null) {
// 导入alibaba fastjson包,反序列化
User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class);
HashMap<String, Object> data = new HashMap<>();
data.put("name", loginUser.getUsername());
data.put("avatar", loginUser.getAvatar());
// 角色需要关联查询,使用userId——>得到roleId,在userMapper写入sql查询
List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
data.put("roles",roleList);
return data;
}
return null;
}
<!-- fastjson -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.51</version>
</dependency>
3. mapper.xml添加,在userMapper接口添加方法
<select id="getRoleNameByUserId" parameterType="Integer" resultType="String">
select
b.role_name
from x_user_role a,x_role b
where a.role_id=b.role_id
and a.user_id= #{userId}
</select>
public interface UserMapper extends BaseMapper<User> {
public List<String> getRoleNameByUserId(Integer userId);
}
4. 测试postman
- 注销(将token从redis中删除)
- controller层
@PostMapping("/logout")
@ResponseBody // 炒鸡重要
public Result<?> logout(@RequestHeader("X-Token") String token) {
userService.logout(token);
return Result.success("注销成功");
}
2. service层
public void logout(String token) {
redisTemplate.delete(token);
}
404报错,没有返回正确的响应消息,但是token会被删除。
// 说明没有返回正确的值,是由于
// @Controller 注解会默认输出试图,而前后端对接需要输出json对象,
// 应该在类开头使用 @RestController注解
// 或者在方法前面加上 @ResponseBody
(5)跨域处理
-
No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
这个错误与 CORS(跨域资源共享) 策略有关。这是一种由浏览器实施的安全机制,用来限制网页向不同域名的资源发出请求,除非目标域明确允许跨域请求。
原因:当你的网页(来源域)试图向不同域、协议或端口的资源发出请求时,服务器响应中没有包含
Access-Control-Allow-Origin
头信息,而浏览器需要这个头信息来允许跨域请求,因此报错。 -
使用nginx反向代理或者cors 跨域问题 | vue-element-admin (panjiachen.github.io)
前端修改api的url值,以及开发配置中修改baseApi
后端在config中新建MyCorsConfig类
package com.fangfang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class MyCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://localhost:8888"); //这里填写请求的前端服务器
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允许的头信息
config.addAllowedHeader("*");
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
测试前后端对接:能够对接上。完成登录查询功能。
(6)用户管理页面布局(前端代码)
- 首先布局user.vue页面,包括搜索栏,内容列表,分页组件
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="20">
<el-input v-model="searchModel.username" placeholder="用户名" />
<el-input v-model="searchModel.phone" placeholder="电话" />
<el-button type="primary" round icon="el-icon-search">查询</el-button>
</el-col>
<el-col :span="4" align="right">
<el-button type="primary" icon="el-icon-plus" circle />
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table
:data="userList"
stripe
style="width: 100%"
>
<el-table-column
type="index"
label="#"
width="180"
/>
<el-table-column
prop="id"
label="用户ID"
width="180"
/>
<el-table-column
prop="username"
label="用户名"
width="180"
/>
<el-table-column
prop="phone"
label="电话"
width="180"
/>
<!-- 剩下宽度给电子邮件 -->
<el-table-column
prop="email"
label="电子邮件"
/>
<el-table-column
label="操作"
width="180"
/>
</el-table>
</el-card>
<!-- 底部pagination分页栏 -->
<el-pagination
:current-page="searchModel.pageSize"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
export default {
data() {
return {
total: 0,
searchModel: {
pageNo: 1,
pageSize: 10
},
userList: []
}
},
methods: {
handleSizeChange() {
},
handleCurrentChange() {
}
}
}
</script>
<style scoped>
#search .el-input {
width: 200px;
margin-right: 20px;
}
</style>
- app.vue中全局样式保证页面美观
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
.app-main{
padding:10px;
}
.el-card{
margin-bottom: 10px;
}
</style>
- 在main.js中修改下element-ui的语言
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
(7)用户管理接口
接口 | 说明 |
---|---|
查询用户列表 | 分页查询 |
新增用户 | |
根据id查询用户 | |
修改用户 | |
删除用户 | 逻辑删除 |
7.1 查询用户列表
(1)controller
@GetMapping("/list")
@ResponseBody
public Result<?> getUserList(@RequestParam(value = "username", required = false) String username,
@RequestParam(value = "phone", required = false) String phone,
@RequestParam("pageNo") Long pageNo,
@RequestParam("pageSize") Long pageSize) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasLength(username), User::getUsername, username);
wrapper.eq(StringUtils.hasLength(phone), User::getPhone, phone);
// baomidou里的Page
// 还需要分页拦截器的配置 https://baomidou.com/plugins/
Page<User> page = new Page<>(pageNo, pageSize);
userService.page(page, wrapper);
Map<String, Object> data = new HashMap<>();
data.put("total", page.getTotal());
data.put("rows", page.getRecords());
System.out.println(data);
return Result.success(data);
}
(2)分页拦截器 插件主体 | MyBatis-Plus (baomidou.com)
package com.fangfang.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MpConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
(3)前后端对接
1. 前端api文件下定义userManager.js
import request from '@/utils/request'
export default {
getUserList(searchModel) {
return request({
url: 'user/list',
method: 'get',
params: {
pageNo: searchModel.pageNo,
pageSize: searchModel.pageSize,
username: searchModel.username,
phone: searchModel.phone
}
})
}
}
2. 前端user.vue使用api,处理分页序号
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="20">
<el-input v-model="searchModel.username" placeholder="用户名" clearable />
<el-input v-model="searchModel.phone" placeholder="电话" clearable />
<el-button type="primary" round icon="el-icon-search" @click="getUserList">查询</el-button>
</el-col>
<el-col :span="4" align="right">
<el-button type="primary" icon="el-icon-plus" circle />
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table
:data="userList"
stripe
style="width: 100%"
>
<el-table-column
label="#"
width="180"
>
<!-- 作用域插槽 -->
<template slot-scope="scope">
<!-- (pageNo-1)*pageSize + index +1 -->
{{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
prop="id"
label="用户ID"
width="180"
/>
<el-table-column
prop="username"
label="用户名"
width="180"
/>
<el-table-column
prop="phone"
label="电话"
width="180"
/>
<!-- 剩下宽度给电子邮件 -->
<el-table-column
prop="email"
label="电子邮件"
/>
<el-table-column
label="操作"
width="180"
/>
</el-table>
</el-card>
<!-- 底部pagination分页栏 -->
<el-pagination
:current-page="searchModel.pageSize"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import userApi from '../../api/userManager'
export default {
data() {
return {
total: 0,
searchModel: {
pageNo: 1,
pageSize: 10
},
userList: []
}
},
created() {
this.getUserList()
},
methods: {
handleSizeChange(pageSize) {
this.searchModel.pageSize = pageSize
this.getUserList()
},
handleCurrentChange(pageNo) {
this.searchModel.pageNo = pageNo
this.getUserList()
},
getUserList() {
userApi.getUserList(this.searchModel).then(res => {
this.userList = res.data.rows
this.total = res.data.total
})
}
}
}
</script>
<style scoped>
#search .el-input {
width: 200px;
margin-right: 20px;
}
</style>
7.2 新增用户
(1)后端
@PostMapping
@ResponseBody
public Result<?> addUser(@RequestBody User user) {
userService.save(user);
return Result.success("新增用户成功");
}
(2)前端+前后端对接
API接口
addUser(user) {
return request({
url: '/user',
method: 'post',
data: user
})
}
}
涉及到用户编辑新增对话框,属性绑定,点击事件,表单常规验证自定义验证,调用接口,注意保存用户之后的事件顺序
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="20">
<el-input v-model="searchModel.username" placeholder="用户名" clearable />
<el-input v-model="searchModel.phone" placeholder="电话" clearable />
<el-button type="primary" round icon="el-icon-search" @click="getUserList">查询</el-button>
</el-col>
<el-col :span="4" align="right">
<el-button type="primary" icon="el-icon-plus" circle @click="openEditUI" />
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table
:data="userList"
stripe
style="width: 100%"
>
<el-table-column
label="#"
width="180"
>
<!-- 作用域插槽 -->
<template slot-scope="scope">
<!-- (pageNo-1)*pageSize + index +1 -->
{{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
prop="id"
label="用户ID"
width="180"
/>
<el-table-column
prop="username"
label="用户名"
width="180"
/>
<el-table-column
prop="phone"
label="电话"
width="180"
/>
<el-table-column
prop="status"
label="用户状态"
width="180"
>
<template slot-scope="scope">
<el-tag v-if="scope.row.status===1">正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<!-- 剩下宽度给电子邮件 -->
<el-table-column
prop="email"
label="电子邮件"
/>
<el-table-column
label="操作"
width="180"
/>
</el-table>
</el-card>
<!-- 底部pagination分页栏 -->
<el-pagination
:current-page="searchModel.pageSize"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<!-- 用户编辑新增对话框 -->
<el-dialog :title="title" :visible.sync="dialogFormVisible" @close="clearForm">
<el-form ref="userFormRef" :model="userForm" :rules="rules">
<el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
<el-input v-model="userForm.username" autocomplete="off" />
</el-form-item>
<el-form-item label="登录密码" prop="password" :label-width="formLabelWidth">
<el-input v-model="userForm.password" type="password" autocomplete="off" />
</el-form-item>
<el-form-item label="联系电话" :label-width="formLabelWidth">
<el-input v-model="userForm.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="用户状态" :label-width="formLabelWidth">
<el-switch
v-model="userForm.status"
:active-value="1"
inactive-value="0"
/>
</el-form-item>
<el-form-item label="电子邮件" prop="email" :label-width="formLabelWidth">
<el-input v-model="userForm.email" autocomplete="off" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveUser">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import userApi from '../../api/userManager'
export default {
data() {
return {
formLabelWidth: '20%',
userForm: {},
dialogFormVisible: false,
title: '',
total: 0,
searchModel: {
pageNo: 1,
pageSize: 10
},
userList: [],
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入登录初始密码', trigger: 'blur' },
{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入电子邮件', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
}
},
created() {
this.getUserList()
},
methods: {
handleSizeChange(pageSize) {
this.searchModel.pageSize = pageSize
this.getUserList()
},
handleCurrentChange(pageNo) {
this.searchModel.pageNo = pageNo
this.getUserList()
},
getUserList() {
userApi.getUserList(this.searchModel).then(res => {
this.userList = res.data.rows
this.total = res.data.total
})
},
openEditUI() {
this.title = '新增用户'
this.dialogFormVisible = true
},
clearForm() {
this.userForm = {}
// 关闭对话框清除验证
this.$refs.userFormRef.clearValidate()
},
saveUser() {
// 1. 触发表单验证
this.$refs.userFormRef.validate((valid) => {
if (valid) {
// 2.提交请求给后台
userApi.addUser(this.userForm).then(res => {
// 成功提示
this.$message({
message: res.message,
type: 'success'
})
// 关闭对话框
this.dialogFormVisible = false
// 刷新表格
this.getUserList()
})
} else {
console.log('error submit!!')
return false
}
})
// 3. 关闭对话框
this.dialogFormVisible = false
}
}
}
</script>
<style scoped>
#search .el-input {
width: 200px;
margin-right: 20px;
}
.el-dialog .el-input {
width: 80%;
}
</style>
(3)密码加密处理,用BCryptPasswordEncoder,涉及登录逻辑改动
1. 添加依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
2. 启动类配置一个Bean,返回一个密码控制类
// 启动类配置一个Bean,返回一个密码控制类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3. userController修改添加用户接口,密码加密
@PostMapping
@ResponseBody
public Result<?> addUser(@RequestBody User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userService.save(user);
return Result.success("新增用户成功");
}
4. 修改登录逻辑
@Override
public Map<String, Object> login(User user) {
// 根据用户名查询
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, user.getUsername());
User loginUser = this.baseMapper.selectOne(wrapper);
// 结果不为空,并且密码和传入匹配,则生成token,并将用户信息存入redis
if (loginUser != null && passwordEncoder.matches(user.getPassword(), loginUser.getPassword())) {
String key = loginUser.getUsername()+ UUID.randomUUID();
loginUser.setPassword(null);
redisTemplate.opsForValue().set(key,loginUser,30, TimeUnit.MINUTES);
HashMap<String, Object> data = new HashMap<>();
data.put("token", key);
return data;
}
return null;
}
7.3 修改用户
此处不提供密码更新,自行扩展,可以去实现前端右上角菜单的个人信息功能
1. 后端添加updateUser接口,getUserById接口
@PutMapping
@ResponseBody
public Result<?> updateUser(@RequestBody User user) {
user.setPassword(null);
// updateById方法默认:如果该字段为空不更新,password不更新
userService.updateById(user);
return Result.success("修改用户成功");
}
@GetMapping("/{id}")
@ResponseBody
public Result<User> getUserById(@PathVariable("id") Integer id) {
return Result.success(userService.getById(id));
}
2. 前端API文档,代码复用
getUserById(id) {
return request({
// url:'url' + id
url: `/user/${id}`,
method: 'get'
})
},
saveUser(user) {
if (user.id == null && user.id === undefined) {
return this.addUser(user)
}
return this.updateUser(user)
},
updateUser(user) {
return request({
url: '/user',
method: 'put',
data: user
})
}
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="20">
<el-input v-model="searchModel.username" placeholder="用户名" clearable />
<el-input v-model="searchModel.phone" placeholder="电话" clearable />
<el-button type="primary" round icon="el-icon-search" @click="getUserList">查询</el-button>
</el-col>
<el-col :span="4" align="right">
<el-button type="primary" icon="el-icon-plus" circle @click="openEditUI(null)" />
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table
:data="userList"
stripe
style="width: 100%"
>
<el-table-column
label="#"
width="180"
>
<!-- 作用域插槽 -->
<template slot-scope="scope">
<!-- (pageNo-1)*pageSize + index +1 -->
{{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
prop="id"
label="用户ID"
width="180"
/>
<el-table-column
prop="username"
label="用户名"
width="180"
/>
<el-table-column
prop="phone"
label="电话"
width="180"
/>
<el-table-column
prop="status"
label="用户状态"
width="180"
>
<template slot-scope="scope">
<el-tag v-if="scope.row.status===1">正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<!-- 剩下宽度给电子邮件 -->
<el-table-column
prop="email"
label="电子邮件"
/>
<el-table-column
label="操作"
width="180"
>
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini" circle @click="openEditUI(scope.row.id)" />
<el-button type="danger" icon="el-icon-delete" size="mini" circle />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 底部pagination分页栏 -->
<el-pagination
:current-page="searchModel.pageSize"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<!-- 用户编辑新增对话框 -->
<el-dialog :title="title" :visible.sync="dialogFormVisible" @close="clearForm">
<el-form ref="userFormRef" :model="userForm" :rules="rules">
<el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
<el-input v-model="userForm.username" autocomplete="off" />
</el-form-item>
<el-form-item v-if="userForm.id==null || userForm.id==undefined" label="登录密码" prop="password" :label-width="formLabelWidth">
<el-input v-model="userForm.password" type="password" autocomplete="off" />
</el-form-item>
<el-form-item label="联系电话" :label-width="formLabelWidth">
<el-input v-model="userForm.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="用户状态" :label-width="formLabelWidth">
<el-switch
v-model="userForm.status"
:active-value="1"
inactive-value="0"
/>
</el-form-item>
<el-form-item label="电子邮件" prop="email" :label-width="formLabelWidth">
<el-input v-model="userForm.email" autocomplete="off" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveUser">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import userApi from '../../api/userManager'
export default {
data() {
return {
formLabelWidth: '20%',
userForm: {},
dialogFormVisible: false,
title: '',
total: 0,
searchModel: {
pageNo: 1,
pageSize: 10
},
userList: [],
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入登录初始密码', trigger: 'blur' },
{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入电子邮件', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
}
},
created() {
this.getUserList()
},
methods: {
handleSizeChange(pageSize) {
this.searchModel.pageSize = pageSize
this.getUserList()
},
handleCurrentChange(pageNo) {
this.searchModel.pageNo = pageNo
this.getUserList()
},
getUserList() {
userApi.getUserList(this.searchModel).then(res => {
this.userList = res.data.rows
this.total = res.data.total
})
},
openEditUI(id) {
if (id == null) {
this.title = '新增用户'
} else {
this.title = '修改用户'
// 根据id查询用户数据
userApi.getUserById(id).then(res => {
this.userForm = res.data
})
}
this.dialogFormVisible = true
},
clearForm() {
this.userForm = {}
// 关闭对话框清除验证
this.$refs.userFormRef.clearValidate()
},
saveUser() {
// 1. 触发表单验证
this.$refs.userFormRef.validate((valid) => {
if (valid) {
// 2.提交请求给后台
userApi.saveUser(this.userForm).then(res => {
// 成功提示
this.$message({
message: res.message,
type: 'success'
})
// 关闭对话框
this.dialogFormVisible = false
// 刷新表格
this.getUserList()
})
} else {
console.log('error submit!!')
return false
}
})
// 3. 关闭对话框
this.dialogFormVisible = false
}
}
}
</script>
<style scoped>
#search .el-input {
width: 200px;
margin-right: 20px;
}
.el-dialog .el-input {
width: 80%;
}
</style>
7.4 删除用户
处理业务的时候涉及到物理删除和逻辑删除
方式一:物理删除:
@DeleteMapping("/{id}")
@ResponseBody
public Result<User> deleteUserById(@PathVariable("id") Integer id){
userService.removeById(id);
return Result.success("删除用户成功");
}
方式二:利用MyBatisPlus做逻辑删除处理(userController.java不变)
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
前端添加API,user.vue
deleteUserById(id) {
return request({
// url:'url' + id
url: `/user/${id}`,
method: 'delete'
})
}
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="20">
<el-input v-model="searchModel.username" placeholder="用户名" clearable />
<el-input v-model="searchModel.phone" placeholder="电话" clearable />
<el-button type="primary" round icon="el-icon-search" @click="getUserList">查询</el-button>
</el-col>
<el-col :span="4" align="right">
<el-button type="primary" icon="el-icon-plus" circle @click="openEditUI(null)" />
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table
:data="userList"
stripe
style="width: 100%"
>
<el-table-column
label="#"
width="180"
>
<!-- 作用域插槽 -->
<template slot-scope="scope">
<!-- (pageNo-1)*pageSize + index +1 -->
{{ (searchModel.pageNo - 1) * searchModel.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
prop="id"
label="用户ID"
width="180"
/>
<el-table-column
prop="username"
label="用户名"
width="180"
/>
<el-table-column
prop="phone"
label="电话"
width="180"
/>
<el-table-column
prop="status"
label="用户状态"
width="180"
>
<template slot-scope="scope">
<el-tag v-if="scope.row.status===1">正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<!-- 剩下宽度给电子邮件 -->
<el-table-column
prop="email"
label="电子邮件"
/>
<el-table-column
label="操作"
width="180"
>
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini" circle @click="openEditUI(scope.row.id)" />
<el-button type="danger" icon="el-icon-delete" size="mini" circle @click="deleteUser(scope.row)" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 底部pagination分页栏 -->
<el-pagination
:current-page="searchModel.pageSize"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<!-- 用户编辑新增对话框 -->
<el-dialog :title="title" :visible.sync="dialogFormVisible" @close="clearForm">
<el-form ref="userFormRef" :model="userForm" :rules="rules">
<el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
<el-input v-model="userForm.username" autocomplete="off" />
</el-form-item>
<el-form-item v-if="userForm.id==null || userForm.id==undefined" label="登录密码" prop="password" :label-width="formLabelWidth">
<el-input v-model="userForm.password" type="password" autocomplete="off" />
</el-form-item>
<el-form-item label="联系电话" :label-width="formLabelWidth">
<el-input v-model="userForm.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="用户状态" :label-width="formLabelWidth">
<el-switch
v-model="userForm.status"
:active-value="1"
inactive-value="0"
/>
</el-form-item>
<el-form-item label="电子邮件" prop="email" :label-width="formLabelWidth">
<el-input v-model="userForm.email" autocomplete="off" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveUser">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import userApi from '../../api/userManager'
export default {
data() {
return {
formLabelWidth: '20%',
userForm: {},
dialogFormVisible: false,
title: '',
total: 0,
searchModel: {
pageNo: 1,
pageSize: 10
},
userList: [],
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入登录初始密码', trigger: 'blur' },
{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入电子邮件', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
}
},
created() {
this.getUserList()
},
methods: {
handleSizeChange(pageSize) {
this.searchModel.pageSize = pageSize
this.getUserList()
},
handleCurrentChange(pageNo) {
this.searchModel.pageNo = pageNo
this.getUserList()
},
getUserList() {
userApi.getUserList(this.searchModel).then(res => {
this.userList = res.data.rows
this.total = res.data.total
})
},
openEditUI(id) {
if (id == null) {
this.title = '新增用户'
} else {
this.title = '修改用户'
// 根据id查询用户数据
userApi.getUserById(id).then(res => {
this.userForm = res.data
})
}
this.dialogFormVisible = true
},
clearForm() {
this.userForm = {}
// 关闭对话框清除验证
this.$refs.userFormRef.clearValidate()
},
saveUser() {
// 1. 触发表单验证
this.$refs.userFormRef.validate((valid) => {
if (valid) {
// 2.提交请求给后台
userApi.saveUser(this.userForm).then(res => {
// 成功提示
this.$message({
message: res.message,
type: 'success'
})
// 关闭对话框
this.dialogFormVisible = false
// 刷新表格
this.getUserList()
})
} else {
console.log('error submit!!')
return false
}
})
// 3. 关闭对话框
this.dialogFormVisible = false
},
deleteUser(user) {
this.$confirm(`您确认删除用户 ${user.username}`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userApi.deleteUserById(user.id).then(res => {
this.$message({
type: 'success',
message: res.message
})
this.getUserList()
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
}
}
}
</script>
<style scoped>
#search .el-input {
width: 200px;
margin-right: 20px;
}
.el-dialog .el-input {
width: 80%;
}
</style>
完结撒花!!!代码在: