【苍穹外卖】项目实战Day02

向右看齐 2024-05-06 09:56 125阅读 0赞

? 本文由 程序喵正在路上 原创,CSDN首发!
? 系列专栏:苍穹外卖项目实战
? 首发时间:2024年5月4日
? 欢迎关注?点赞?收藏?留言?

目录

  • 新增员工
    • 需求分析和设计
    • 代码开发
    • 功能测试
    • 代码完善
  • 员工分页查询
    • 需求分析和设计
    • 代码开发
    • 功能测试
    • 代码完善
  • 启用禁用员工账号
    • 需求分析和设计
    • 代码开发
    • 功能测试
  • 编辑员工
    • 需求分析和设计
    • 代码开发
    • 功能测试
  • 导入分类模块功能代码
    • 需求分析和设计
    • 代码导入
    • 功能测试

新增员工

需求分析和设计

需求分析

首先,我们来到新增员工的页面原型,进行需求分析。因为页面原型比较直观,便于我们去了解业务。

在这里插入图片描述

页面原型上注明了新增员工时需要录入的信息,接下来我们需要思考一下这些表单项是否需要某些限制,还是可以随便录入?

思考结果:

  • 账号必须是唯一的
  • 手机号为合法的 11 位手机号码
  • 身份证号为合法的 18 位身份证号码
  • 注意到表单项中没有 “密码” ,这里我们约定,新增员工时会默认员工密码为 123456,后续员工可以自行修改密码

设计

  1. 接口设计

    因为我们是前后端分离开发,所以我们必须先设计好接口后,才能进行前后端的开发。
    在这里插入图片描述
    在接口设计中,我们需要考虑请求的路径以及请求的类型;同时,我们需要定义前端需要传给后端的参数,以及后端返回给前端的数据格式。

    由于本项目分为管理端和用户端,所以统一规定:

    • 管理端发出的请求,统一使用 /admin 作为前缀
    • 用户端发出的请求,统一使用 /user 作为前缀
  2. 数据库设计

    employee表:
    在这里插入图片描述

代码开发

根据新增员工接口设计对应的DTO

在这里插入图片描述

注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据。

EmployeeController 中创建新增员工方法,接收前端提交的参数:

  1. /**
  2. * 新增员工
  3. * @param employeeDTO
  4. * @return
  5. */
  6. @PostMapping
  7. @ApiOperation("新增员工接口")
  8. public Result save(@RequestBody EmployeeDTO employeeDTO) {
  9. log.info("新增员工:{}", employeeDTO);
  10. employeeService.save(employeeDTO);
  11. return Result.success();
  12. }

EmployeeService 接口中声明新增员工方法:

  1. /**
  2. * 新增员工
  3. * @param employeeDTO
  4. */
  5. void save(EmployeeDTO employeeDTO);

EmployeeServiceImpl 中实现新增员工方法:

  1. /**
  2. * 新增员工
  3. *
  4. * @param employeeDTO
  5. */
  6. public void save(EmployeeDTO employeeDTO) {
  7. //定义员工对象
  8. Employee employee = new Employee();
  9. //对象属性拷贝: 将employeeDTO的属性拷贝给employee
  10. BeanUtils.copyProperties(employeeDTO, employee);
  11. //账号状态默认为1, 正常状态
  12. employee.setStatus(StatusConstant.ENABLE);
  13. //默认密码为123456
  14. employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
  15. //设置当前记录的创建时间和修改时间
  16. employee.setCreateTime(LocalDateTime.now());
  17. employee.setUpdateTime(LocalDateTime.now());
  18. //设置当前记录创建人id和修改人id
  19. //由于仅凭当前的技术还无法获取这两个id, 所以我们先给它一个固定值
  20. employee.setCreateUser(10L);
  21. employee.setUpdateUser(10L);
  22. employeeMapper.insert(employee);
  23. }

EmployeeMapper 中声明 insert 方法:

  1. /**
  2. * 插入员工数据
  3. * @param employee
  4. */
  5. @Insert("insert into employee" +
  6. "(name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
  7. "VALUES " +
  8. "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
  9. void insert(Employee employee);

功能测试

功能测试方式:

  • 通过接口文档测试
  • 通过前后端联调测试

注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主

通过接口文档测试

启动服务,来到接口文档页面,查看新增员工接口:

在这里插入图片描述

点击调试,填写请求参数,点击发送后,程序好像没什么反应,而是直接返回响应码 401。HTTP状态码 401 表示未经授权,表示请求需要进行身份验证,但请求没有提供所需的凭据:

在这里插入图片描述

其实我们写的代码没有问题,只是因为我们的项目中写了一个拦截器,在收到请求后,都会被其拦截,拦截器会先从请求头中获取 jwt 令牌并校验令牌,通过校验才可以进行后续操作;否则直接返回。

因为刚才我们并没有传令牌给服务端,所以请求失败了。

但是每一次我们发送请求测试都要传递令牌,其实是很麻烦的,所以我们的想法是在接口文档中统一添加一个令牌,这样在后续调试都会自动将令牌提交给服务端。

ps:在项目的配置文件中,我们设置了 jwt 令牌的有效时间为 2 小时。

首先,我们需要先获取一个令牌,很简单,我们通过员工登录这个接口即可获取,将其复制:

在这里插入图片描述

然后,我们点击全局参数设置,添加参数:

在这里插入图片描述

参数名为 token 是因为我们在配置文件中配置了:

在这里插入图片描述

添加后,刷新一下页面,可以看到请求头部分多了一个参数,点击发送,成功:

在这里插入图片描述

在这里插入图片描述

通过前后端联调测试

打开前端页面,添加员工:

在这里插入图片描述

成功:

在这里插入图片描述

在这里插入图片描述

代码完善

程序存在的问题:

  1. 录入的用户名已存在,抛出异常后没有处理
  2. 新增员工时,创建人id和修改人id的设置

针对第一个问题,可以通过全局异常处理器来处理:

在新增员工的时候,如果输入的用户名已存在,会触发下面的异常:

在这里插入图片描述

我们通过项目中的全局异常处理器来捕获这个异常并进行处理:

在这里插入图片描述

  1. /**
  2. * 捕获sql异常
  3. *
  4. * @param ex
  5. * @return
  6. */
  7. @ExceptionHandler
  8. public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
  9. log.info("异常信息:{}", ex.getMessage()); //即 Duplicate entry 'zhangsan' for key 'employee.idx_username'
  10. String message = ex.getMessage();
  11. if (message.contains("Duplicate entry")) {
  12. String[] split = message.split(" ");
  13. String username = split[2]; //获取传进来的用户名
  14. return Result.error(username + MessageConstant.ALREADY_EXITS);
  15. }
  16. return Result.error(MessageConstant.UNKNOWN_ERROR);
  17. }

重启项目,再次调试,结果如下:

在这里插入图片描述

针对第二个问题,我们需要通过某种方式来动态获取当前登录员工的 id

程序的执行流程:

在这里插入图片描述
员工登录成功后会生成 JWT 令牌并响应给前端:

在这里插入图片描述

后续请求中,前端会携带 JWT 令牌,通过 JWT 令牌可以解析出当前登录员工 id

在这里插入图片描述

获取员工 id 这一步还是比较简单的,现在的问题是,解析出登录员工 id 后,如何将其传递给 Servicesave 方法?

解决方法——使用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>

    在这里插入图片描述

  1. 根据接口定义创建分页查询方法:

    1. /**
    2. * 员工分页查询
    3. *
    4. * @param employeePageQueryDTO
    5. * @return
    6. */
    7. @GetMapping("/page")
    8. @ApiOperation("员工分页查询")
    9. public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
    10. //不是json格式的数据, 不能加@RequestBody
    11. log.info("员工分页查询,参数:{}", employeePageQueryDTO);
    12. PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
    13. return Result.success(pageResult);
    14. }
  2. EmployeeService 接口中声明 pageQuery 方法:

    1. /**
    2. * 分页查询
    3. *
    4. * @param employeePageQueryDTO
    5. * @return
    6. */
    7. PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
  3. EmployeeServiceImpl 中实现 pageQuery 方法:

    1. /**
    2. * 分页查询
    3. *
    4. * @param employeePageQueryDTO
    5. * @return
    6. */
    7. public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    8. //开始分页查询
    9. PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
    10. Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
    11. return new PageResult(page.getTotal(), page.getResult());
    12. }

    注意:此处使用了 mybatis 的分页插件 PageHelper 来简化分页代码的开发,底层基于 mybatis 的拦截器实现。

    所需依赖已添加:

    在这里插入图片描述

  4. EmployeeMapper 中声明 pageQuery 方法:

    1. /**
    2. * 分页查询
    3. *
    4. * @param employeePageQueryDTO
    5. * @return
    6. */
    7. Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
  5. EmployeeMapper.xml 中编写SQL

    1. <?xml version="1.0" encoding="UTF-8" ?>
    2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    4. <mapper namespace="com.sky.mapper.EmployeeMapper">
    5. <select id="pageQuery" resultType="com.sky.entity.Employee">
    6. select * from employee
    7. <where>
    8. <if test="name != null and name != ''">
    9. and name like concat('%', #{name}, '%')
    10. </if>
    11. </where>
    12. # PageHelper插件会给我们动态添加limit参数
    13. order by create_time desc
    14. </select>
    15. </mapper>

ps:PageHelper 底层也是通过 ThreadLocal 来实现的。

在这里插入图片描述

功能测试

可以通过接口文档进行测试,也可以进行前后端联调测试,测试过程中发现最后操作时间字段展示有问题,就是时间的格式好像不太对,如下:

在这里插入图片描述

来到前端页面,我们看到时间的展示也不是我们想要的效果:

在这里插入图片描述

代码完善

解决方式:

  • 方式一:在属性上加入注解,对日期进行格式化
  • 方式二:在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式化处理

方式一:给每个属性都加上注解

在这里插入图片描述

方式二:

重写 Spring MVC 框架的消息转换器,使用我们自己写的对象转换器,以后关于时间的对象都会自动格式化,无需我们再去进行格式化

在这里插入图片描述

  1. /**
  2. * 扩Spring MVC框架的消息转换器
  3. *
  4. * @param converters
  5. */
  6. @Override
  7. protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
  8. log.info("扩展消息转换器...");
  9. //创建一个消息转换器对象
  10. MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  11. //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
  12. //JacksonObjectMapper在com.sky.json中已存在
  13. converter.setObjectMapper(new JacksonObjectMapper());
  14. //将自己的消息转换器加入容器中,并设置为第一位
  15. converters.add(0, converter);
  16. }

自己写的对象转换器,项目中已提供:

在这里插入图片描述

效果:

在这里插入图片描述

在这里插入图片描述

如果你想具体到秒,可以在对象转换器 JacksonObjectMapper 中修改。

启用禁用员工账号

需求分析和设计

产品原型:

在这里插入图片描述

业务规则:

  • 可以对状态为 “启用” 的员工账号进行 “禁用” 操作
  • 可以对状态为 “禁用” 的员工账号进行 “启用” 操作
  • 状态为 “禁用” 的员工账号不能登录系统

接口设计:

在这里插入图片描述

代码开发

根据接口设计中的请求参数形式,对应在 EmployeeController 中创建启用禁用员工账号的方法:

  1. /**
  2. * 启动禁用员工账号
  3. *
  4. * @param status
  5. * @param id
  6. * @return
  7. */
  8. @PostMapping("/status/{status}")
  9. @ApiOperation("启动禁用员工账号")
  10. public Result startOrStop(@PathVariable Integer status, Long id) {
  11. //由于变量名相同, 所以注解中不用指定使用哪个路径参数
  12. log.info("启动禁用员工账号:{}, {}", status, id);
  13. employeeService.startOrStop(status, id);
  14. return Result.success();
  15. }

EmployeeService 接口中声明启用禁用员工账号的业务方法:

  1. /**
  2. * 启动禁用员工账号
  3. *
  4. * @param status
  5. * @param id
  6. */
  7. void startOrStop(Integer status, Long id);

EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:

  1. /**
  2. * 启动禁用员工账号
  3. *
  4. * @param status
  5. * @param id
  6. */
  7. @Override
  8. public void startOrStop(Integer status, Long id) {
  9. //方式一:可以直接new一个Employee, 再设置id和status
  10. //方式二:因为Employee类中已经添加了@Builder注解,所以可以使用builder
  11. Employee employee = Employee.builder()
  12. .id(id)
  13. .status(status)
  14. .build();
  15. employeeMapper.update(employee);
  16. }

EmployeeMapper 接口中声明 update 方法:

  1. /**
  2. * 根据id修改员工信息
  3. *
  4. * @param employee
  5. */
  6. void update(Employee employee);

EmployeeMapper.xml 中编写 SQL

  1. <!-- 参数类型可以不指定具体包名, 因为在配置文件中我们已经指定了扫描的包为com.sky.entity-->
  2. <update id="update" parameterType="Employee">
  3. update employee
  4. <set>
  5. <if test="username != null">username = #{username},</if>
  6. <if test="name != null">name = #{name},</if>
  7. <if test="password != null">password = #{password},</if>
  8. <if test="phone != null">phone = #{phone},</if>
  9. <if test="sex != null">sex = #{sex},</if>
  10. <if test="idNumber != null">id_Number = #{idNumber},</if>
  11. <if test="updateTime != null">update_Time = #{updateTime},</if>
  12. <if test="updateUser != null">update_User = #{updateUser},</if>
  13. <if test="status != null">status = #{status}</if>
  14. </set>
  15. where id = #{id}
  16. </update>

功能测试

可以通过接口文档进行测试,最后完成前后端联调测试即可

在这里插入图片描述

在这里插入图片描述

编辑员工

需求分析和设计

产品原型:

在这里插入图片描述

点击修改按钮会弹出界面:

在这里插入图片描述

接口设计:

编辑员工功能涉及到两个接口:

  • 根据 id 查询员工信息
    在这里插入图片描述
  • 编辑员工信息

    在这里插入图片描述

代码开发

根据 id 查询员工信息接口开发

EmployeeController 中创建 getById 方法:

  1. /**
  2. * 根据id查询员工
  3. *
  4. * @param id
  5. * @return
  6. */
  7. @GetMapping("/{id}")
  8. @ApiOperation("根据id查询员工")
  9. public Result<Employee> getById(@PathVariable Long id) {
  10. log.info("根据id查询员工:{}", id);
  11. return Result.success(employeeService.getById(id));
  12. }

EmployeeService 接口中声明 getById 方法:

  1. /**
  2. * 根据id查询员工
  3. *
  4. * @param id
  5. * @return
  6. */
  7. Employee getById(Long id);

EmployeeServiceImpl 中实现 getById 方法:

  1. /**
  2. * 根据id查询员工
  3. *
  4. * @param id
  5. * @return
  6. */
  7. @Override
  8. public Employee getById(Long id) {
  9. Employee employee = employeeMapper.getById(id);
  10. employee.setPassword("****"); //设置一下密码, 防止他人通过开发者控制台获取密码
  11. return employee;
  12. }

EmployeeMapper 接口中声明 getById 方法:

  1. /**
  2. * 根据id查询员工信息
  3. *
  4. * @param id
  5. * @return
  6. */
  7. @Select("select * from employee where id = #{id}")
  8. Employee getById(Long id);

编辑员工信息接口开发

EmployeeController 中创建 update 方法:

  1. /**
  2. * 编辑员工信息
  3. *
  4. * @param employeeDTO
  5. * @return
  6. */
  7. @PutMapping
  8. @ApiOperation("编辑员工信息")
  9. public Result update(@RequestBody EmployeeDTO employeeDTO) {
  10. log.info("编辑员工:{}", employeeDTO);
  11. employeeService.update(employeeDTO);
  12. return Result.success();
  13. }

EmployeeService 接口中声明 update 方法:

  1. /**
  2. * 根据id修改员工信息
  3. *
  4. * @param employeeDTO
  5. */
  6. void update(EmployeeDTO employeeDTO);

EmployeeServiceImpl 中实现 update 方法:

  1. /**
  2. * 根据id修改员工信息
  3. *
  4. * @param employeeDTO
  5. */
  6. @Override
  7. public void update(EmployeeDTO employeeDTO) {
  8. //拷贝对象属性
  9. Employee employee = new Employee();
  10. BeanUtils.copyProperties(employeeDTO, employee);
  11. //设置修改人和修改时间
  12. employee.setUpdateUser(BaseContext.getCurrentId());
  13. employee.setUpdateTime(LocalDateTime.now());
  14. //使用前面开发好的update方法
  15. employeeMapper.update(employee);
  16. }

功能测试

通过 Swagger 接口文档进行测试,通过后再前后端联调测试即可

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

导入分类模块功能代码

需求分析和设计

产品原型:

在这里插入图片描述

业务规则:

  • 分类名称必须是唯一的
  • 分类按照类型可以分为菜品分类和套餐分类
  • 新添加的分类状态默认为 “禁用”

接口设计:

  • 新增分类
  • 分类分页查询
  • 根据 id 删除分类
  • 修改分类
  • 启用禁用分类
  • 根据类型查询分类

数据库设计 (category表):

在这里插入图片描述

代码导入

从技术层面上来看,分类管理模块和员工管理模块其实是非常类似的。除了操作的表不一样,实际上都是对单个表的增删改查,技术难度是差不多的,所以关于分类管理的代码我们就不再细讲,直接导入即可,小伙伴有不懂的地方也可以在评论区提出来。

在第二天的资料中找到分类管理的代码,导入即可

在这里插入图片描述

导入的时候可以按照从后往前的顺序导入,这样代码不会报错

导入完,手动编译一下,导入的类有时候不会自动编译:

在这里插入图片描述

需要稍微注意一下的是删除分类这个功能,就是我们在删除的时候需要考虑到该分类中是否包含菜品或套餐。

在这里插入图片描述

功能测试

直接进行前后端联调测试即可

在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,125人围观)

还没有评论,来说两句吧...

相关阅读

    相关 苍穹——项目搭建

    本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐...