个人项目
用到的知识: 增删改查, 分页, 高级查询, Vue + elementUI, Hutool工具包, 登录功能, session跨域问题解决, 登录拦截…
一、后端项目构建
1.后端环境搭建
-
构建项目:
个人项目是一个基于Springboot + Vue的前后端分离的项目
-
建父项目, 父项目是一个空项目, 子模块有Vue构建的子模块, 不需要继承父项目pom文件的jar包依赖;
-
构建子模块, 子模块暂时包括后端的Springboot模块 blog-server 和后台管理前端模块 blog-view-admin 以及用户前端模块 blog-view-user ;
-
-
建包:
项目基于SpringMVC架构, 首先根据三层架构建立基本包结构
- 首先根据系统需求分析数据实体, 进行数据库表设计
- 构建包层次结构, 实体类domain,mapper层,service层,controller层, 分别加上注解注入Spring容器交给容器管理
- 其中domain的字段最好以驼峰命名与数据库的蛇形命名字段对应
- mapper层的xml文件要构建在resources文件夹下, 根路径和mapper相同, 同时在启动类上方加上包扫描
@MapperScan("cn.tjw.mapper")
- service层主要写业务逻辑代码, 由控制层调用service层, 再由service层调用mapper层, 可以看作controller层是在service层之上的,service层在mapper层之上, mapper层再通过xml配置的SQL语句进行数据库增删改查操作
- 构建好基本的三层架构之后先在application.yml文件中配置数据库基本配置和日志级别和端口号配置等, 导入依赖包之后可以随时连接数据库
- 添加构建过程可能需要的包, 比如config包(拦截器配置类, 跨域配置,
@EnableSwagger2
注解的api文档配置类), Intercepter包, util包或者一些自定义异常类, AOP切面类等等
-
导入项目依赖&配置application.yml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.tjw</groupId> <artifactId>blog-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>blog-server</name> <description>blog-server</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> // 统一版本 <parent> <!--该maven工程管理着我们springboot相关的jar包和插件--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <dependencies> <!-- 支持web环境,不用写版本号 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据库的jar包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <!-- SpringBoot2.2.5的版本中Mysql是mysql8.0的,需要手动导入8以下的版本。我们电脑中安装的数据库是mysql5.x的,mysql8.0启动包都不一样,所以需要手动导入5.x的版本。避免链接数据库出现问题 --> <!-- <version>5.1.38</version>--> </dependency> <!-- mybatis与springboot的整合包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <!-- druid连接池(德鲁伊-阿里巴巴的)--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- pagehelper后端分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.10</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- 后端处理JSON的依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <!--swagger2的核心包--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!--swagger2的ui界面包 底层用的thymeleaf--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!--hutool依赖--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.22</version> </dependency> <!--poi依赖(生成Excel时需要用到此依赖包)--> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> </dependencies> </project>
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource # 连接池指定 springboot2.02版本默认使用HikariCP 此处要替换成Druid driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:///blog?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true username: root password: 123456 druid: #企业开发中可能会用到的配置 initial-size: 5 # 初始化时建立物理连接的个数 min-idle: 5 # 最小连接池连接数量,最小空闲数量 max-active: 20 # 最大连接池连接数量,最大活跃连接数 max-wait: 60000 # 配置获取连接等待超时的时间 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 testWhileIdle: true testOnBorrow: true testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 stat-view-servlet: # 德鲁伊连接池,内置提供一个web版的性能监控配置 allow: 0.0.0.0 # 允许哪些IP访问druid监控界面,多个IP以逗号分隔 login-username: admin # 设置登录帐号 login-password: 123456 # 设置登录密码 reset-enable: false # 是否允许重置数据 # url-pattern: /database/* # 默认访问根路径是:/druid/;也可以自定义设置 # mybatis配置 mybatis: configuration: map-underscore-to-camel-case: true #开启驼峰字段转换,自动映射时将有下划线的字段的数据映射给驼峰字段 type-aliases-package: cn.tjw.domain #扫描实体类所在的包 logging: level: cn.itsource: trace server: port: 8080 servlet: context-path: /
2.增删改查
- 先写基础的列表展示—查询所有, 返回一个实体类对象的List
- 先写mapper层, 根据什么查询,返回什么类型, 用到哪些字段
- 再写service层和controller层
- 在service层注入mapper层接口, 在controller层注入service层接口
- 业务逻辑都写在service层方法, mapper层只负责访问数据库, controller层只负责调用业务方法
- 编写代码和测试需要穿插进行, 使用Postman进行接口调试, 以免发生错误难以定位
Restful风格:
GET:获取资源
POST:添加资源【也可以用于更新资源】
PUT:更新资源【也可以用于添加资源】
DELETE:用来删除资源
PATCH:用来做批量操作
注意:get请求是将参数放在地址栏。post,put,patch是将参数放在请求体【requestBody】。一般传一个参数可以用get,delete,多个参数用post,put,patch
前端传输数据 | 后端匹配请求 + 接收数据 |
---|---|
GET http://localhost/dept/1 | @GetMapping("/dept/{id}") + @PathVariable("id") Long id |
Post http://localhost/dept - 对象:{} | @PostMapping("/dept") + @RequestBody Dept dept |
复杂表单数据【文件上传】name="file" | @PostMapping("/dept") + @RequestPart("file") MultipartFile file |
@PathVarible:从路径上去接收参数 /user/1
@RequestPart:接收二进制数据,文件上传
@RequestBody:接收json数据
@RequestParam:按照参数名接收 ?key=value
路径参数区别:
@PathVariable:
- 用于从URL路径中获取参数值
- 用于Spring MVC和Spring Boot
- 通常用于RESTful风格的URL,参数值会直接嵌入到URL路径中,而不是作为查询字符串参数
@RequestParam:
- 用于从HTTP请求的查询字符串中获取参数值
- 通常用于传递键值对参数,这些参数会作为查询字符串的一部分附加到URL中
- 可以用于Spring框架中的Spring MVC和Spring Boot等,以及Servlet API中
@PathParam:
- 用于JAX-RS(Java API for RESTful Web Services)
3.分页查询
查询工具类
@Data
public class SysUserQuery {
//分页查询参数:当前页
private Integer currentPage = 1; //默认值可以防止空指针问题
//分页查询参数:每页显示的条数
private Integer pageSize = 5;
private String keyword;
}
service层分页查询
@Override
public List<SysUser> queryPage(SysUserQuery sysUserQuery) {
//PageHelper的写法:开启分页
PageHelper.startPage(sysUserQuery.getCurrentPage(), sysUserQuery.getPageSize());
return sysUserMapper.queryPage(sysUserQuery);
}
controller层调用
//分页查询 + 高级查询
@PostMapping
public PageInfo<SysUser> queryPage(@RequestBody SysUserQuery sysUserQuery){
// 前端传过来的参数包含currentPage和pageSize
List<SysUser> sysUsers = sysUserService.queryPage(sysUserQuery);
return new PageInfo<>(sysUsers);
}
前端代码
methods: {
//1.获取用户列表数据
getTableData() {
let para = {
"currentPage": this.currentPage,
"pageSize": this.pageSize,
"keyword": this.filters.keyword
};
this.listLoading = true; //显示加载圈
//分页查询
this.$http.post("/user",para).then(result => {
this.totals = result.data.total;
this.tableDatas = result.data.list;
this.listLoading = false; //关闭加载圈
}).catch(result => {
this.$message.error("获取分页数据失败!!!");
});
},
//2.选择第几页时触发
handleCurrentChange(currentPage) {
this.currentPage = currentPage;
this.getTableData();
},
//3.选择每页显示记录条数
handleSizeChange(pageSize) {
this.pageSize = pageSize
this.getTableData();
},
二、前端项目准备
1.前端Vue脚手架
-
构建项目:
- 拷贝集成ElementUI代码
- 在config/index.js中修改端口号
- npm install命令安装依赖
- 拷贝node_modules
一些常用的前端UI组件可以去官网复制—组件 | Element
-
运行:
1. 后端项目直接运行启动类
2. 前端项目在IDEA的Terminal栏位中进入到前端项目目录,然后使用npm run dev启动
跨域问题解决
axios.defaults.withCredentials = true; //当前请求为跨域类型时,是否在请求中协带cookie
package cn.itsource.basic.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 GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写/,否则cookie就无法使用了
config.addAllowedOrigin("http://127.0.0.1:8081");
config.addAllowedOrigin("http://localhost:8081");
config.addAllowedOrigin("http://127.0.0.1:80");
config.addAllowedOrigin("http://localhost:80");
config.addAllowedOrigin("http://127.0.0.1");
config.addAllowedOrigin("http://localhost");
//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);
}
}
2.批量删除
-
主要调整前端代码:
//批量删除 delBatch() { this.$confirm('确定删除这些数据吗?', '请确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { var ids = this.sels.map(item => item.id); this.$http.patch("/user", ids, {headers: { 'Content-Type': 'application/json' }}).then(result => { if (result.data.success) { this.$message({ type: 'success', message: result.data.message }); //如果删除是最后一页的所有数据,并且还不能是第一页,回到上一页 var totalPage = Math.ceil(this.totals / this.pageSize);//总页数 if (this.currentPage == totalPage && (this.totals - this.sels.length) % this.pageSize == 0 && this.currentPage != 1) { this.currentPage = this.currentPage - 1; } this.getTableData(); } else { this.$message({ type: 'error', message: result.data.message }); } }); }).catch(() => { this.$message({ type: 'info', message: '已取消操作' }); }); },
-
后端实现:
控制层//批量删除用户信息 @PatchMapping public AjaxResult patchDel(@RequestBody Long[] ids){ try { System.out.println("尝试批量删除"); sysUserService.patchDel(ids); return AjaxResult.me(); } catch (Exception e) { e.printStackTrace(); return AjaxResult.me().setSuccess(false).setMessage("系统繁忙,请稍后重试"); } }
业务层
@Override public void patchDel(Long[] ids) { sysUserMapper.patchDel(ids); }
mapper层
// 这个account对应的是形参里面的String account @Select("select * from sys_user where user_name=#{account}") SysUser findByUsername(String account);
3.Hutool工具包
1. 获取随机数和随机字符串:RandomUtil
2. 字符串判空:StrUtil
3. Excel导入导出:ExcelUtil
4. 字符串的加密:SecureUtil
5. 日期时间的处理:DateUtil
6. IO流的处理:IoUtil
7. 文件处理:FileUtil
8. 资源文件处理:ResourceUtil
9. Json处理:JsonUtil
10. Http请求:HttpUtil
11. JWT处理:JWTUtil
三、登录功能
1.登录接口
-
新建一个LoginController类, 接收前端参数
//登录接口 @PostMapping("/login") public AjaxResult login(@RequestBody Map<String,String> map, HttpSession session){ try { System.out.println("后端校验密码"); SysUser dbUser = sysUserService.login(map); //将登录成功之后将登录信息放在Session中:1.用于做登录拦截,登录拦截就从session中获取登录信息来判断当前操作人是否登录过 session.setAttribute(SysConstant.USER_IN_SESSION,dbUser); return AjaxResult.me().setResultObj(dbUser); //Service有业务异常这里就要捕捉业务异常,业务异常要抛给前端显示出来 } catch (BusinessException e) { //e.printStackTrace(); //业务异常是不需要打印的,自己抛的 return AjaxResult.me().setSuccess(false).setMessage(e.getMessage()); } catch (Exception e) { //系统异常500,如果不是业务异常就是系统异常了 e.printStackTrace(); return AjaxResult.me().setSuccess(false).setMessage("系统异常,稍后重试"); } }
// 自定义结果类 @Data public class AjaxResult { //接口状态(true:成功;false:失败) private Boolean success = true; //返回前端的提示信息(成功) private String message = "操作成功"; //存储返回给前端的数据 private Object resultObj; // private String token; private String realName; private String avatar; //无参构造 public AjaxResult(){} //有参构造 public AjaxResult(Boolean success, String message) { this.success = success; this.message = message; } //链式语法改造 public static AjaxResult me(){ return new AjaxResult(); } public AjaxResult setSuccess(Boolean success) { this.success = success; return this; } public AjaxResult setMessage(String message) { this.message = message; return this; } public AjaxResult setResultObj(Object resultObj) { this.resultObj = resultObj; return this; } }
// 自定义异常类 public class BusinessException extends RuntimeException{ public BusinessException() { } public BusinessException(String message) { super(message); } }
-
调用service层, 执行密码校验逻辑
@Override public SysUser login(Map<String, String> map) { String account = map.get("userName"); String checkPass = map.get("password"); System.out.println(checkPass); //一:校验 - 空值校验 if(StrUtil.isEmptyIfStr(account)){ throw new BusinessException("账号不能为空"); } if(StrUtil.isEmptyIfStr(checkPass)){ throw new BusinessException("密码不能为空"); } //二:校验 - 用户名校验 SysUser dbSysUser = sysUserMapper.findByUsername(account); if(dbSysUser == null){ throw new BusinessException("用户名错误"); } //三:校验 - 密码校验 String md5Pwd = checkPass; // String md5Pwd = SecureUtil.md5(checkPass); if(!md5Pwd.equals(dbSysUser.getPassword())){ throw new BusinessException("密码错误"); } return dbSysUser; }
-
调用mapper层方法查出用户对象或密码
// 这个account对应的是形参里面的 @Select("select * from sys_user where user_name=#{account}") SysUser findByUsername(String account);
四、登录拦截
1.编写拦截器类
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 在执行目标方法之前,先执行此方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在session中获取登录用户
Object loginObj = request.getSession().getAttribute(SysConstant.USER_IN_SESSION);
if(loginObj == null){
System.out.println(loginObj + "----用户session信息");
//由于这里是前后端分离项目:所以后端项目没有页面,无法直接重定向到登录页面。并且前端发送的请求基本都是异步请求,所以拦截器中可以统一给前端[管理端或用户端]一个响应,让前端去跳转到登录页面
//设置编码格式,否则中文会乱码
response.setCharacterEncoding("utf-8");
//将数据输出到前端,只要是false或noLogin,就要跳转到登录页面
response.getWriter().println("{\"success\":false,\"message\":\"noLogin\"}");
System.out.println("你被我拦截了");
return false;//并且拦截
}
//放行
return true;
}
}
2.配置拦截路径
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
/**
* 注册我自定义的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //设置自定义拦截器作用与所有请求
.excludePathPatterns("/login"); //排除哪些请求不需要做拦截
}
}
3.前端拦截器
前端axios后置拦截器
//axios的响应/后置拦截器 - 发送axios请求之后响应的结果最先到这里来,然后才会到回调函数then中
axios.interceptors.response.use(res => {
//如果想要的数据中success为false,或者message为noLogin,就说明后端需要跳转到登录页面
if(res.data.success === false && res.data.message === "noLogin"){
localStorage.removeItem("realName");
localStorage.removeItem("avatar");
//说明没有登录
router.push({path: '/login'});
}
return res;
}, err => {
// 在请求错误时要做的事儿
Promise.reject(err)
});
前端路由拦截器
router.beforeEach((to, from, next) => {
if (to.path == '/login') {
next();
}else{
let realName = localStorage.getItem('realName');
if (realName) {
next();
} else {
next({path: '/login'});//跳转到login
}
}
})
-
this.$http.post("/login", loginParams).then(res=>{ if(res.data.success){ var userData = res.data.resultObj; alert(userData.realName + "我才是真实名称"); //把token存储起来 localStorage.setItem("U-TOKEN",userData.token); localStorage.setItem("realName",userData.realName); localStorage.setItem("userPic",userData.avatar); var sysUser = res.data; //把登录名和用户头像保存起来 localStorage.setItem("realName",userData.realName); localStorage.setItem("avatar",userData.avatar); //修改登录成功后跳转到首页 this.$router.push({ path: '/echarts' }); this.logining = true; return;