【苍穹外卖】项目实战Day02
? 本文由 程序喵正在路上 原创,CSDN首发!
? 系列专栏:苍穹外卖项目实战
? 首发时间:2024年5月4日
? 欢迎关注?点赞?收藏?留言?
目录
- 新增员工
- 需求分析和设计
- 代码开发
- 功能测试
- 代码完善
- 员工分页查询
- 需求分析和设计
- 代码开发
- 功能测试
- 代码完善
- 启用禁用员工账号
- 需求分析和设计
- 代码开发
- 功能测试
- 编辑员工
- 需求分析和设计
- 代码开发
- 功能测试
- 导入分类模块功能代码
- 需求分析和设计
- 代码导入
- 功能测试
新增员工
需求分析和设计
需求分析
首先,我们来到新增员工的页面原型,进行需求分析。因为页面原型比较直观,便于我们去了解业务。
页面原型上注明了新增员工时需要录入的信息,接下来我们需要思考一下这些表单项是否需要某些限制,还是可以随便录入?
思考结果:
- 账号必须是唯一的
- 手机号为合法的 11 位手机号码
- 身份证号为合法的 18 位身份证号码
- 注意到表单项中没有 “密码” ,这里我们约定,新增员工时会默认员工密码为 123456,后续员工可以自行修改密码
设计
接口设计
因为我们是前后端分离开发,所以我们必须先设计好接口后,才能进行前后端的开发。
在接口设计中,我们需要考虑请求的路径以及请求的类型;同时,我们需要定义前端需要传给后端的参数,以及后端返回给前端的数据格式。由于本项目分为管理端和用户端,所以统一规定:
- 管理端发出的请求,统一使用
/admin
作为前缀 - 用户端发出的请求,统一使用
/user
作为前缀
- 管理端发出的请求,统一使用
数据库设计
employee表:
代码开发
根据新增员工接口设计对应的DTO:
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据。
在 EmployeeController 中创建新增员工方法,接收前端提交的参数:
/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工接口")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
在 EmployeeService 接口中声明新增员工方法:
/**
* 新增员工
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);
在 EmployeeServiceImpl 中实现新增员工方法:
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
//定义员工对象
Employee employee = new Employee();
//对象属性拷贝: 将employeeDTO的属性拷贝给employee
BeanUtils.copyProperties(employeeDTO, employee);
//账号状态默认为1, 正常状态
employee.setStatus(StatusConstant.ENABLE);
//默认密码为123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id
//由于仅凭当前的技术还无法获取这两个id, 所以我们先给它一个固定值
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
employeeMapper.insert(employee);
}
在 EmployeeMapper 中声明 insert 方法:
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into employee" +
"(name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
"VALUES " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
void insert(Employee employee);
功能测试
功能测试方式:
- 通过接口文档测试
- 通过前后端联调测试
注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
通过接口文档测试
启动服务,来到接口文档页面,查看新增员工接口:
点击调试,填写请求参数,点击发送后,程序好像没什么反应,而是直接返回响应码 401。HTTP状态码 401 表示未经授权,表示请求需要进行身份验证,但请求没有提供所需的凭据:
其实我们写的代码没有问题,只是因为我们的项目中写了一个拦截器,在收到请求后,都会被其拦截,拦截器会先从请求头中获取 jwt 令牌并校验令牌,通过校验才可以进行后续操作;否则直接返回。
因为刚才我们并没有传令牌给服务端,所以请求失败了。
但是每一次我们发送请求测试都要传递令牌,其实是很麻烦的,所以我们的想法是在接口文档中统一添加一个令牌,这样在后续调试都会自动将令牌提交给服务端。
ps:在项目的配置文件中,我们设置了 jwt 令牌的有效时间为 2 小时。
首先,我们需要先获取一个令牌,很简单,我们通过员工登录这个接口即可获取,将其复制:
然后,我们点击全局参数设置,添加参数:
参数名为 token 是因为我们在配置文件中配置了:
添加后,刷新一下页面,可以看到请求头部分多了一个参数,点击发送,成功:
通过前后端联调测试
打开前端页面,添加员工:
成功:
代码完善
程序存在的问题:
- 录入的用户名已存在,抛出异常后没有处理
- 新增员工时,创建人id和修改人id的设置
针对第一个问题,可以通过全局异常处理器来处理:
在新增员工的时候,如果输入的用户名已存在,会触发下面的异常:
我们通过项目中的全局异常处理器来捕获这个异常并进行处理:
/**
* 捕获sql异常
*
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
log.info("异常信息:{}", ex.getMessage()); //即 Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if (message.contains("Duplicate entry")) {
String[] split = message.split(" ");
String username = split[2]; //获取传进来的用户名
return Result.error(username + MessageConstant.ALREADY_EXITS);
}
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
重启项目,再次调试,结果如下:
针对第二个问题,我们需要通过某种方式来动态获取当前登录员工的 id:
程序的执行流程:
员工登录成功后会生成 JWT 令牌并响应给前端:
后续请求中,前端会携带 JWT 令牌,通过 JWT 令牌可以解析出当前登录员工 id:
获取员工 id 这一步还是比较简单的,现在的问题是,解析出登录员工 id 后,如何将其传递给 Service 的 save 方法?
解决方法——使用ThreadLocal
- ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。
- ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
注意:客户端发送的每次请求,后端的 Tomcat 服务器都会分配一个单独的线程来处理请求,所以这里我们才能使用 ThreadLocal 来处理。
ThreadLocal 常用方法:
public void set(T value)
:设置当前线程的线程局部变量的值public T get()
:返回当前线程所对应的线程局部变量的值public void remove()
:移除当前线程的线程局部变量
初始工程中已经封装了 ThreadLocal 操作的工具类,方便我们后续使用:
完善代码
在拦截器中解析出当前登录员工 id,并放入线程局部变量中:
在 Service 中获取线程局部变量中的值:
到此,新增员工的业务代码就开发完了,我们将添加的代码推送到远程仓库中:
员工分页查询
需求分析和设计
产品原型:
业务规则:
- 根据页码展示员工信息
- 每页展示 10 条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
接口设计:
代码开发
根据分页查询接口设计对应的 DTO,这个 DTO 在初始工程中已经定义:
请求参数格式为 Query,不是 json
后面所有的分页查询,统一都封装成 PageResult 对象:
员工信息分页查询后端返回的对象类型为:
Result<PageResult>
根据接口定义创建分页查询方法:
/**
* 员工分页查询
*
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
//不是json格式的数据, 不能加@RequestBody
log.info("员工分页查询,参数:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
在 EmployeeService 接口中声明 pageQuery 方法:
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在 EmployeeServiceImpl 中实现 pageQuery 方法:
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
注意:此处使用了 mybatis 的分页插件 PageHelper 来简化分页代码的开发,底层基于 mybatis 的拦截器实现。
所需依赖已添加:
在 EmployeeMapper 中声明 pageQuery 方法:
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在 EmployeeMapper.xml 中编写SQL:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper">
<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>
# PageHelper插件会给我们动态添加limit参数
order by create_time desc
</select>
</mapper>
ps:PageHelper 底层也是通过 ThreadLocal 来实现的。
功能测试
可以通过接口文档进行测试,也可以进行前后端联调测试,测试过程中发现最后操作时间字段展示有问题,就是时间的格式好像不太对,如下:
来到前端页面,我们看到时间的展示也不是我们想要的效果:
代码完善
解决方式:
- 方式一:在属性上加入注解,对日期进行格式化
- 方式二:在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式化处理
方式一:给每个属性都加上注解
方式二:
重写 Spring MVC 框架的消息转换器,使用我们自己写的对象转换器,以后关于时间的对象都会自动格式化,无需我们再去进行格式化
/**
* 扩Spring MVC框架的消息转换器
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
//JacksonObjectMapper在com.sky.json中已存在
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入容器中,并设置为第一位
converters.add(0, converter);
}
自己写的对象转换器,项目中已提供:
效果:
如果你想具体到秒,可以在对象转换器 JacksonObjectMapper 中修改。
启用禁用员工账号
需求分析和设计
产品原型:
业务规则:
- 可以对状态为 “启用” 的员工账号进行 “禁用” 操作
- 可以对状态为 “禁用” 的员工账号进行 “启用” 操作
- 状态为 “禁用” 的员工账号不能登录系统
接口设计:
代码开发
根据接口设计中的请求参数形式,对应在 EmployeeController 中创建启用禁用员工账号的方法:
/**
* 启动禁用员工账号
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启动禁用员工账号")
public Result startOrStop(@PathVariable Integer status, Long id) {
//由于变量名相同, 所以注解中不用指定使用哪个路径参数
log.info("启动禁用员工账号:{}, {}", status, id);
employeeService.startOrStop(status, id);
return Result.success();
}
在 EmployeeService 接口中声明启用禁用员工账号的业务方法:
/**
* 启动禁用员工账号
*
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:
/**
* 启动禁用员工账号
*
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
//方式一:可以直接new一个Employee, 再设置id和status
//方式二:因为Employee类中已经添加了@Builder注解,所以可以使用builder
Employee employee = Employee.builder()
.id(id)
.status(status)
.build();
employeeMapper.update(employee);
}
在 EmployeeMapper 接口中声明 update 方法:
/**
* 根据id修改员工信息
*
* @param employee
*/
void update(Employee employee);
在 EmployeeMapper.xml 中编写 SQL:
<!-- 参数类型可以不指定具体包名, 因为在配置文件中我们已经指定了扫描的包为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 查询员工信息
编辑员工信息
代码开发
根据 id 查询员工信息接口开发
在 EmployeeController 中创建 getById 方法:
/**
* 根据id查询员工
*
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工")
public Result<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工:{}", id);
return Result.success(employeeService.getById(id));
}
在 EmployeeService 接口中声明 getById 方法:
/**
* 根据id查询员工
*
* @param id
* @return
*/
Employee getById(Long id);
在 EmployeeServiceImpl 中实现 getById 方法:
/**
* 根据id查询员工
*
* @param id
* @return
*/
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****"); //设置一下密码, 防止他人通过开发者控制台获取密码
return employee;
}
在 EmployeeMapper 接口中声明 getById 方法:
/**
* 根据id查询员工信息
*
* @param id
* @return
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
编辑员工信息接口开发
在 EmployeeController 中创建 update 方法:
/**
* 编辑员工信息
*
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO) {
log.info("编辑员工:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
在 EmployeeService 接口中声明 update 方法:
/**
* 根据id修改员工信息
*
* @param employeeDTO
*/
void update(EmployeeDTO employeeDTO);
在 EmployeeServiceImpl 中实现 update 方法:
/**
* 根据id修改员工信息
*
* @param employeeDTO
*/
@Override
public void update(EmployeeDTO employeeDTO) {
//拷贝对象属性
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
//设置修改人和修改时间
employee.setUpdateUser(BaseContext.getCurrentId());
employee.setUpdateTime(LocalDateTime.now());
//使用前面开发好的update方法
employeeMapper.update(employee);
}
功能测试
通过 Swagger 接口文档进行测试,通过后再前后端联调测试即可
导入分类模块功能代码
需求分析和设计
产品原型:
业务规则:
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为 “禁用”
接口设计:
- 新增分类
- 分类分页查询
- 根据 id 删除分类
- 修改分类
- 启用禁用分类
- 根据类型查询分类
数据库设计 (category表):
代码导入
从技术层面上来看,分类管理模块和员工管理模块其实是非常类似的。除了操作的表不一样,实际上都是对单个表的增删改查,技术难度是差不多的,所以关于分类管理的代码我们就不再细讲,直接导入即可,小伙伴有不懂的地方也可以在评论区提出来。
在第二天的资料中找到分类管理的代码,导入即可
导入的时候可以按照从后往前的顺序导入,这样代码不会报错
导入完,手动编译一下,导入的类有时候不会自动编译:
需要稍微注意一下的是删除分类这个功能,就是我们在删除的时候需要考虑到该分类中是否包含菜品或套餐。
功能测试
直接进行前后端联调测试即可
还没有评论,来说两句吧...