第一章:项目概述环境搭建。
软件开发整体介绍
软件开发流程:1.需求分析(有需求规格说明书,产品原型)2.设计(ui设计(用户界面),3.数据库设计,接口设计(就是看某个功能的请求方式之类的,数据返回方式啊等等))。4.编码:项目代码,单元测试。5.测试:编写测试用例,测试文档。6.上线运维,软件环境安装和配置。
角色分工
每个角色进行的工作不同:
我们是做编码的。那就需要接触不同的环境:
软件环境
项目介绍
为餐饮行业定制的一款产品。
功能架构:体现项目中的业务功能模块。
产品原型
一般有产品经理来做,一般都是静态的html页面。
我们要先去看产品原型才能进行开发。我们要仔细查看产品原型,才能理解产品业务最终开发出来。
线下自行查看技术原型,也就是每个页面,好好理解。
技术选型
看项目中用到的技术:
本项目使用到的技术:
里面新技术后面会讲解。
开发环境搭建
我们的主要工作就是后端的,前端只需要代码就可以啦。我们只需要使用nginx运行前端代码就可以啦。我们会搭建好后端的环境,在这个环境基础上完成业务代码就可以啦。
前端环境搭建
只需要打开资料day1的内容,
把这个文件复制到一个没有中文文件名的文件夹下面运行就可以啦。
里面已经部署好前端的页面啦。启动之后访问:
localhost
就可以访问到页面啦,因为访问的接口为80,所以上面的链接就不需要单独指定接口了。
到这里前端的环境 就搭建好啦。
后端环境搭建
后端基于maven进行项目构建,并进行分模块开发。
在资料的:
把这个项目导入idea里面:
对于这个结构熟悉一下就可以啦。
看图:需要了解这个pojo,右边表格其实都是pojo,只是功能不同,需要细分实体类这样可以分清楚每个pojo是做什么的。
公司大部分不会让你从零开始创建项目的,大部分都是在已有的代码基础上开发的。
总结:把后端项目的基本结构进行啦简单的了解,知道每个目录存什么的。
git环境搭建
开发前进行git环境搭建,进行版本控制。
使用git进行版本控制的步骤:
这个文件是设置git的忽略文件,不需要git进行版本控制的文件。
接下来进行版本控制:
1.创建本地仓库:
点击之后选中当前项目的根目录结构:
点击之后idea出现
这个图标就表示仓库创建好啦。
然后把代码提交到本地:
点击这个按钮,然后选中
书写标记:
点击commit就可以上传到本地库啦。
接下啦在gitee上面创建远程库,创建远程库之后点击这个就可以啦。
如果是第一次提交远程库,你还需要关联远程库。
点击define remote就可以啦。
这样git环境就配置完成啦。
总结:本小节就是在本地新建本地仓库,创建远程库,把代码提交到远程库就算完成啦。之后我们只需要基于git的环境提交推送代码就可以啦。
数据库环境搭建
通过数据库语句创建出数据库表结构。
这里的md文件讲解啦数据库表结构,需要我们线下去查看。(md文件对应的是sky中创建的表代表什么)
把sql文件导入到数据库中,运行就生成数据啦。
到这里数据库就弄好啦
前后端联调
在这里使用登录功能来测试:
浏览器是前段的内容,而后面的就是后端的东西。
1.启动nginx,打开登录界面。
2.启动SkyApplication
(这里会出现密码错误的问题,需要你自己修改链接数据库的密码)
登录界面点击登录就可以登录进去啦。
查看代码解析:网页点击登录就进入到
EmployeeController类中,把数据提交到
login方法中,查看里面的代码就知道数据是如何流通的。
这里需要看代码数据如何传回去的,看看源代码就知道啦
可以查看:前端的请求地址为:
http://localhost/api/employee/login
而后端的请求路径是:
它们两个的请求地址不一样,是由nginx反向代理来联调的。
它的访问流程:
反向代理的好处:
在实际公司开发中,前端不能直接访问后端,他们是分布在一个局域网的,让后端来访问,增加安全性,负载均衡就是可以让多个后端进行访问。
nginx反向代理配置方法
在安装nginx的文件夹下寻找:
nginx.conf文件,在这里配置。
在server标签里面设置:
只要接收到请求字段里有api的,就交给它处理 ,proxy_pass把请求路径api前面的字符串交换成proxy_pass后面列出来的字符串,
location /api/ {
proxy_pass http://localhost:8080/admin/;
}
后端的路径就修改啦。
在配置文件中同样使用proxy_pass来交换字段,上边的upstream webservers字段里面放的就是要均衡的主机的主机号,
类似这个,weight代表权值,表示权值90
在主机后面可以放的规则有:
这个简单了解就可以啦。
完善登录功能
加密登录密码,使用md5的方式来加密。
操作方法:1.修改数据库的密码,改成铭文加密后的密文。
2.修改java代码,前端提交的密码加密后再与数据库的密文进行比较。比较对啦就算密码输入正确。
密文加密需要使用到
DigestUtils工具类来加密。
具体的操作:
去数据库修改密码,再去图下这个类这里加密password就可以啦。
导入接口文档
实际开发中接口设计非常漫长。需要前后端不断去开会来讨论才能确定。
前后段分离开发的流程:
定制接口,然后前后端并行开发,开发完之后进行联调看是否能连接成功,最后测试。
导入接口文档使用yapi,在浏览器搜索yapi官网登录进去,创建两个项目,用户端接口和管理端接口,然后导入对应的json文件就可以啦。
swagger接口文档
它可以帮助后端生成接口文档,并且可以进行在线接口测试。
可以生成属于你后端的代码的接口文档(不是前后端讨论成的),它可以生成一个独立的页面,你在电脑里访问那个页面就可以看到一个接口文档,可以再那个页面进行测试,观看接口。
使用方法:
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>${knife4j}</version> </dependency>
第二步:
再配置类WebMvcConfiguration中添加下面的,这个书写的方式是固定的,只是一些信息不同而已,复制粘贴进去就可以啦。
@Bean
public Docket docket() {
// 生成接口文档的一系列信息,信息自定义。
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
//
Docket docket = new Docket(DocumentationType.SWAGGER_2)
// 放入接口文档的信息
.apiInfo(apiInfo)
.select()
// 指定生成接口需要扫描的包,他会扫描com.sky.controller这个包,最后帮我们生成接口文档
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
第三步:设置静态资源隐射,没有这个就生成不了页面。
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始创建静态资源隐射");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
yapi与swagger区别:
swgger的注解
它的作用就是生成接口文档的时候增加可读性
分别有:
具体的使用的用例:就是用来生命
生成的注解是在页面显示的:员工相干接口,登录,退出,在这里就可以看到效果啦。还有参数说明。增加啦接口文档的可读性。
9.11总结:今天学习到nginx反向代理,了解到它的作用,以及设置它的方法。完善登录功能学习到把前端传过来的密码进行加密过后再与数据库的加密密码进行对比,这样就完成登录功能的密码加密操作。导入接口文档,知道啦开发的流程,1.前后端开发人员开会设计接口文档,设计好接口文档可以使用yapi进行导入接口文档,前后端人员再进行并行开发,再进行前后端联调(这里就需要用到swagger生成后端的接口文档,也可以进行简单的接口测试它比postman好用),又学到怎么能设置swagger来生成接口文档,设置的格式基本一样。还有它的常用注解,就为啦增加swagger生成的接口文档的可读性。
开发
上边已经把环境给配置好啦。接下来就是开发功能的操作啦。
新增员工
企业开发具体的流程:
1.需求分析:根据产品原型来熟悉业务功能。
想一下这些表单项输入有什么限制。
分析之后的结果:
2.设计接口:
讨论访问路径,请求方式,接口描述等,后端响应回前端的数据。
设定项目开发的约定:对于访问路径为了方便区分管理端和用户端:
3.数据库设计:确定对哪个表进行操作。设计每个字段有什么约束等。
以上就是对于新增员工功能的需求分析与设计的内容,1.先看产品原型了解每个功能输入数据的格式,或者约束。2.设计接口文档,设计访问路径,项目约束,请求方式,响应数据格式等。3.设计数据库表结构,确定每个字段有什么约束等。
新增员工代码开发
1.根据接口文档设计 dto类来封装前端传输过来的数据。
DTO只封装有用的数据,可以过滤掉敏感数据。
接口如果需要的参数很少并且不需要做数据就可以直接传参不需要进行封装。
1.首先在 EmployeeController 类中创建方法:
@PostMapping
@ApiOperation("新增员工")
// 如果是json数据就用@RequestBody注解,使用这个注解可以解析出前端返回来的json中的数据,具体解析去spirngmvc中查看
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);//输出接受到的数据
// 我们去调用employeeService的方法
employeeService.save(employeeDTO);
return null;
}
2.在EmployeeService类中添加该方法。
3.在该实现类中实现这个方法。
/**
*
* 新增员工
* @param employeeDTO
*/
@Override
public void save(EmployeeDTO employeeDTO) {
// 把参数传给实体类Employee
Employee employee=new Employee();
// 把对象属性拷贝到实体类中,把dto类中的数据拷贝到实体类中,前提是两边的字段名是一样的。
BeanUtils.copyProperties(employeeDTO,employee);
// 因为除啦需要传导前端的数据,实体类中还有其他参数需要我们手动设置
//设置账号状态,1表示正常,0表示锁定,我们为了规范一点就是用到项目中设计的常量来规定。
employee.setStatus(StatusConstant.ENABLE);
//设置密码,默认123456,在添加密码保存到数据库的密码的是加密的。所以需要进行加密操作,PasswordConstant.DEFAULT_PASSWORD是项目中设置的常量,后面的getBytes把代码设置成字节数组
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
// 设置当前记录的创建和修改实践
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id,因为现在无法获取id,我们之后再改善,可以使用TODO来记录
//TODO后期需要改为当前登录用户的id
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
//到这里就把封装好啦数据,就调用持久层把数据插入。
employeeMapper.insert(employee);
}
上面已经把数据封装好啦,就去持久层那边插入书写代码插入数据。
// 扩展持久层的插入方法,插入员工数据
@Insert("insert into employee (name,username,password,phone,sex,id_number,create_time,update_time,create_user,update_user,status)"+
"values" +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);
这里代码出现问题啦,复制粘贴代码进去就可以啦截图的是错误的。
最后设置返回值结果:
我在这里遇见啦一点问题:java: 程序包sun.security.util不存在。那时候的jdk版本是11,应该改成jdk1.8的版本。
这样就可以啦
功能测试
上面已经把代码开发好啦,现在进行功能测试。
功能测试方法有两种:1。使用接口文档的方式来测试。2.前后端联调方式。
因为是并行开发的,后端开发完前端不一定开发好,我们测试就比较多的使用第一种方式来测试。
使用调试的方式启动项目,进入接口文档。设计请求参数:
在图中位置放置断点:
点击发送参数;
这个是因为jwt令牌效验没有通过,调试过后发现token为null
所以我们的方法没有被执行到。
问题解决:在接口文档那里设置正确的请求头。使用全局的方式设置值。
我们通过登录功能获取正确的请求头值:
在这里添加全局参数设置就可以啦。
这时候我们的新增员工的请求头路径就是:
接下来就可以重新发送请求啦。下面是接收到请求的参数的结果。
可以去查看数据库那里已经可以添加员工啦。这个功能测试就完成啦。
接下来去前端页面进行数据添加看看。就可以啦。
目前到这里没有出现问题。
代码完善
程序问题:1.录入用户名已存在,抛出异常后没有处理(因为用户名在数据库中设置为唯一的)。2.新增员工时,创建人id和修改人id设置为啦固定值。
解决问题一:在全局处理器GlobalExceptionHandler中添加处理异常的方法。
具体方法如下:
/**
* 处理sql异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username'异常信息
// 通过字段获取异常信息看看是否和上边的一样
String message=ex.getMessage();
//查看字段message是否包含括号里面的字段
if (message.contains("Duplicate")){
//通过空格来分割字段,就形成多个数组,使用数组变量来获取数据
String[] split = message.split("");
String username=split[2];
//返回提示信息给前端,username+常量信息账号已存在
String msg=username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
去接口文档处检查就会得到结果:
第二个问题:我们因该要动态获取当前用户的id,然后设置id。
把基于jwt的认证方式的流程搞清楚才能知道怎么获取。
流程就是用户认证提交信息认证通过之后生成jwt令牌返沪给前端,前端保存jwtToken,在前端请求后端接口的时候每次请求都在请求头中携带JWT Token拦截器进行拦截并验证,验证通过就执行业务代码,不通过返回错误信息。
我们只需要在验证token的那个地方获取用户id就可以啦。我们可以查看以下代码段:
claims添加啦用户id,然后把claims添加到token里。
之后我们只需要在jwt校验的时候解析出来就可以了。
我们取到id之后需要传给service的 save方法,需要使用到ThreadLocal,对于这个类我们进行理解:
线程本地变量(Thread Local Variables,简称TLV)是一种特殊的局部变量,它为每个并发执行的线程提供了一个独立的副本。这种变量的作用域仅限于当前线程,这意味着每当一个新的线程启动时,它的线程本地变量会自动初始化一个新的值,而不会影响其他线程的副本。这在需要为每个线程维护单独数据的情况下非常有用,例如跟踪单线程环境下的状态,可以避免竞态条件和数据共享带来的复杂性。
线程本地变量通常用于存储那些不需要同步的数据,因为它们对每个线程都是私有的。在Java中,java.lang.ThreadLocal
类提供了创建和管理线程本地变量的功能。
简单的来说就是有并发的线程,使用到这个类每创建一个线程就会为那个线程复制一个变量的副本,你在这个线程修改这个变量只会i修改本线程的变量,不会影响到其他线程的变量。
那么问题来了,用户发送的每次请求数据流通是不是单独的线程呢?(确定是单线程的时候id就是一样的)
我们需要到数据流通的那几个位置加上
System.out.println("当前线程的id:"+ Thread.currentThread().getId());查看就可以啦。
在前端发送请求的之后控制台打出:
说明他们是在同一个线程的。
我们可以把用户id存到ThreaLocal提供的存储空间里,这样子在save方法中就可以获取id啦。
在项目中使用通过工具类的方式包装它的一些方法,我们使用的时候直接使用工具类的方式来调用这些方法就可以啦。
下面是包装方法
调用baseContext来设置id
在save方法中把值传进去就可以啦。
在这里也需要自己测试一下,测试一下id是否获取成功。
添加员工之后查看数据库就可以得到这样的结果:
最后我们的完善成功需要上传gitee上面进行更新。
总结
在第一个功能开发阶段个人比较激动,在这途中发现效率低啦不少,毕竟没有做过需要用脑袋想一下,思考它的过程,在这途中我知道需求分析是最重要的,需求分析完之后需要进行数据的封装,设计接口,先进行数据的封装:使用到dto。在这里清晰的理解到后端只需要考虑数据的传输,也就是如何接受前端传过来的数据,以及如何反馈数据给前端。在项目中可以使用全局常量来设置经常使用到的值,这里有一个大概的框架,现在表现层添加方法接受数据再调用逻辑层的方法,再去逻辑层sevice添加该方法进行逻辑操作(看数据具体提娜佳到哪里,怎么传),逻辑层调用数据层的方法,最后数据层添加该方法对数据库进行操作,返回数据到上一次,上一层再把数据返回到控制层最后把结果返回给前端显示。
功能测试就查看上边的流程是否是想要的结果,测试方式有:1.前后端联调。2.使用swagger来模拟前端发出的响应。在测试途中学习到可以增加断点进行代码调试,出现问题或者数据传输到哪里就可以通过断点看到,具体再哪里出现问题。
可以通过异常信息查看是什么原因导致的,就更好排除错误。
我在这里知道:1.请求头如何通过jwt令牌的校验,并且学会如何设置统一的Token通过jwt校验 。而且了解到jwt校验流程。
在代码完善阶段解决啦两个问题,1.如何自动处理一些错误(在全局异常处理器中,复制异常信息,创建处理异常的方法)2.如何动态的获取请求头中的信息。(需要了解jwt工作流程,看在哪里会有该信息,然后再那个地方使用Thread Local的方法对id进行传输。详细看上方)。
员工分页查询
1.需求分析设计
通过上方的产品原型可以分析出规则:
1.每一页展示10条用户信息。
2.可以通过搜索栏查询到用户的信息。
3.可以根据页码展示员工信息。
查询方式是搜索的,所以我们的请求方式应该是get请求的方式。
我们需要传什么样的参数:1.页码。2.员工姓名。3.每页记录数。
需求分析出来的图:
代码开发只需要去看代码
1.在EmployeeController中添加方法
/**
* 分页查询
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
//在参数中接收前段提交过来的数据(这个在文档里面去看),EmployeePageQueryDTO是前面封装啦的前段传过来的dto。因为这个不是json数据所有就不需要像上面一样
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询:参数:{}",employeePageQueryDTO);
//调用这个方法进行实际的查询,
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
2.在empoyeeservice添加方法
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
3。在它的实现类里面从写该方法
/**基于数据库的分页,dto里面已经把页码数和记录数给传过来啦。所以数据库语句只需要动态的把
* 分页查询
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10;我们需要动态的计算limit后面的数,这里需要用到一个插件
//这个方法动态的计算啦page,和size
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
//把持久层查询出来的数据付给page
//TODO 无法查询到数据
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total=page.getTotal();
List<Employee> records=page.getResult();
return new PageResult(total,records);
}
4.在持久层设计sql语句(并在mapper文件里填上对应的sql语句)
/**这里需要用到动态sql,就把sql注入到映射文件里面去,在这里需要下载一个mybatisx插件
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name!=null and name!=''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
看代码注解。与上面的步骤类似。就是在书写sql语句的时候使用到一个
PageHelper.startPage的插件。他在你sql语句后面动态添加limit
状态码为200表示你的代码没问题,你的前端提交的数据有问题。
代码完善
对日期类型进行格式的设置。
设置方法:
1.在这个类里面扩展消息转换器
2.在里面从写方法:
/**
* 从写的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要消息转换器设置一个对象转换器,它可以讲java对象序列化为json对象
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到框架的容器中,加上一个索引0,表示converter转换器放在第一个使用。
converters.add(0,converter);
}
里面的格式就在
这里设置的。
完善之后就提交到git上。
启用禁用员工账号
需求分析和设计
代码开发
1.在EmployeeController类中添加方法
/**
* 启用禁用员工账号
* @param status
* @param id
* @return
*/
//请求方式为post方式,@PathVariable Integer status,Long id通过这个方式获取前端请求过来的数据。
//因为如果是查询的数据,就需要通过泛型来包装数据的类型。这里不是查询的数据,所以不用泛型。
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用员工账号:{},{}",status,id);
employeeService.startOrStop(status,id);
return Result.success();
}
2.在EmployeeController类中调用EmployeeService的方法
3.在EmployeeService具体实现这个方法。
/**我们需要进行的操作就是修改员工表的状态。
* 启用禁用员工账号
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
//我们传实体对象过去就可以啦,因为Employee有一个@Builder解释器,所以可以直接进行创建对象然后把id,status传进去。
Employee employee = Employee.builder().id(id).status(status).build();
//调用持久层的方法
employeeMapper.update(employee);
}
4.调用持久层的方法,编写持久层的sql语句,进行数据操作,代码就完成啦。
<!-- 类型为Employee,因为在配置文件里面导入包啦,直接写类型就可以-->
<!-- 参数类型可以不指定具体包名, 因为在配置文件中我们已经指定了扫描的包为com.sky.entity-->
<update id="update" parameterType="Employee">
update employee
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
<if test="status != null">status = #{status}</if>
</set>
where id = #{id}
</update>
到这里代码就开发完成啦。
代码测试
总结:本节学习启用禁用员工账号,我们需要设置它状态值,获取到前端发送过来的数据中的状态值和id,就可以根据这个状态值对数据库表的数据进行修改并且返回给前端,其中使用到
@Builder注释。个人认为比较困难的点在于前面的设计模块,给出设计规则之后,再去设计代码,代码的设计流程大致就那几个,都只是对数据的处理。还有持久层的sql语句书写需要认真学习学习。
编辑员工
设计分析
产品原型:
由图可知点击修改按钮会弹出下面的图。
1.我们需要根据id查询要修改的员工信息
2.再去进行修改
接口文档:
代码开发
EmployeeController
/**
* 根据id查询员工信息
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
EmployeeServiceImpl
/**
* 查询员工
*/
@Override
public Employee getById(Long id) {
Employee employee= employeeMapper.getById(id);
// 在查询到密码之后把密码改成******返回给前端就可以增加安全性
employee.setPassword("******");
return employee;
}
EmployeeMapper
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
进行前后端联调测试