苍穹外卖-day02:员工管理模块(统一前缀,必须非必须、实体类接收缺点、log占位符、属性拷贝,TODO、常全局参数、本地线程、分页插件、日期格式化、启用禁用 动态更新)分类管理模块(分类删除前提)
苍穹外卖-day02课程内容新增员工员工分页查询启用禁用员工账号编辑员工导入分类模块功能代码功能实现:员工管理、菜品分类管理。单表员工管理效果:菜品分类管理效果:1. 新增员工(员工管理)1.1 需求分析和设计1.1.1 产品原型一般在做需求分析时,往往都是对照着产品原型进行分析,因为产品原型比较直观,便于我们理解业务。后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。新增员工原型:当填
苍穹外卖-day02
课程内容
- 新增员工
- 员工分页查询
- 启用禁用员工账号
- 编辑员工
- 导入分类模块功能代码
功能实现:员工管理、菜品分类管理。
单表
员工管理效果:
菜品分类管理效果:
1. 新增员工(员工管理)
1.1 需求分析和设计
1.1.1 产品原型
一般在做需求分析时,往往都是对照着产品原型进行分析,因为产品原型比较直观,便于我们理解业务。
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。
新增员工原型:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
注意事项:
- 账号必须是唯一的
- 手机号为合法的11位手机号码
- 身份证号为合法的18位身份证号码
- 密码默认为123456
- 录入完员工信息后,这个员工就可以拿到这个账号来登录我们的系统了,登录就需要有登录的密码而我们这个表单里好像并没有密码,这是为什么呢???
- 这个地方约定的是新增出来的员工,它的密码都是默认的密码123456,当然它登录进来之后可以去修改密码,因为这个系统里面也有修改密码的功能。所以说在新增的时候这个密码我们都给它设置为默认的密码123456。
1.1.2 接口设计(统一前缀,必须非必须)
找到资料–>项目接口文档–>苍穹外卖-管理端接口.html
明确新增员工接口的请求路径、请求方式、请求参数、返回数据。
- 请求路径:为什么不直接使用/employee,而是在前面还要加上这个前缀admin呢??
- 此项目分为了2个端(管理端和用户端),管理端提供给商家使用,用户端主要是给用户点餐使用的,这2个端都会请求到我们的后端,这个时候想要区分一下具体这个请求是那个端发过来的,约定
管理端
发出的请求,统一使用/admin
作为前缀,约定用户端
发出的请求,统一使用/user
作为前缀. - 好处:通过前缀来区分不同的端方便我们做一些处理的工作,比如统一拦截用户端发过来的请求来看一下用户的状态,这样的话很容易就拦截到。如果没有这样的约定,那么这个请求发送过来的也没有什么规范,此时我们后端就很难区分这个请求到底是哪个端来发送的。
- 所以后期我们所有的接口都需要遵守这个约定,只要是管理端都以…前缀为开头,只要是用户端都以…前缀为开头。
- 此项目分为了2个端(管理端和用户端),管理端提供给商家使用,用户端主要是给用户点餐使用的,这2个端都会请求到我们的后端,这个时候想要区分一下具体这个请求是那个端发过来的,约定
- 请求方式:新增员工,所以post请求比较合适
- 请求参数:通过json格式来提交页面上看到的表单项,其中id是非必须的,也就是说前端给我们提交或者是不提交都可以
- 返回数据:返回的也是json格式并且会封装成一个result统一响应结果给前端页面
- code:状态码是必须的,约定返回1表示业务操作成功,返回0代表业务操作失败。
- data:返回的数据非必须,一般查询返回数据,这个地方是新增所以不需要返回
- message:提示信息非必须,一般成功不设置,失败设置一个提示信息
本项目约定:
- 管理端发出的请求,统一使用
/admin
作为前缀。 - 用户端发出的请求,统一使用
/user
作为前缀。
1.1.3 表设计
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
employee表结构:
- id:设置了主键自增所以不用我们去维护它的值,而是由数据库维护
- username:用户名其实就是上面产品原型上的账号,它是登录的依据所以必须是唯一的
- status:账号的状态,1代表账号正常允许登录, 0锁定代表账号异常不允许登录
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 姓名 | |
username | varchar(32) | 用户名 | 唯一 |
password | varchar(64) | 密码 | |
phone | varchar(11) | 手机号 | |
sex | varchar(2) | 性别 | |
id_number | varchar(18) | 身份证号 | |
status | Int | 账号状态 | 1正常 0锁定 |
create_time | Datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
其中,employee表中的status字段已经设置了默认值1,表示状态正常。
1.2 代码开发
1.2.1 设计DTO类(直接使用实体类接收请求参数的缺点)
根据新增员工接口设计对应的DTO
前端传递参数列表:
思考:是否可以使用对应的实体类来接收呢?
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据
即:实体类当中包含很多不需要提交的属性时,此时直接使用实体类来接收请求参数,可读性差、浪费性能。如果使用DTO精确封装,可读性好并且不会造成性能浪费。
由于上述传入参数和实体类有较大差别,所以自定义DTO类。
进入sky-pojo模块,在com.sky.dto包下,已定义EmployeeDTO
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeeDTO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}
1.2.2 Controller层(log占位符)
EmployeeController中创建新增员工方法
进入到sky-server模块中,在com.sky.controller.admin包下,在EmployeeController中创建新增员工方法,接收前端提交的参数。
/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);//{}占位符:表示当程序运行的时候,它就会把后面的这个参数动态的拼接到占位符的位置
employeeService.save(employeeDTO);//该方法后续步骤会定义
return Result.success();
}
注:Result类定义了后端统一返回结果格式。
进入sky-common模块,在com.sky.result包下定义了Result.java
package com.sky.result;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
1.2.3 Service层接口
在EmployeeService接口中声明新增员工方法
进入到sky-server模块中,com.sky.server.EmployeeService
/**
* 新增员工
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);
1.2.4 Service层实现类(属性拷贝,TODO,常量类)
在EmployeeServiceImpl中实现新增员工方法
com.sky.server.impl.EmployeeServiceImpl中创建方法
/**
* 新增员工
* @param employeeDTO
* 传递过来的是DTO类,这是为了方便封装前端传递过来的数据,但是最终
* 传递到持久层建议还是使用我们的实体类。所以这个地方需要做一个类型转换
* 将DTO转化为我们的实体Employee。
*
*/
@Override //此注解不是必须的可以去掉
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//1.直接使用set,get一个个的转化太麻烦,可以使用对象属性拷贝,
//一次性的将这些属性给它拷过来,前提是2个类之间的属性要一一对应(属性名一致)
//把employeeDTO的属性拷贝给employee中对应的属性上
BeanUtils.copyProperties(employeeDTO, employee); //Spring提供的工具类
//2.实体类employee中的属性更多一些,DTO只是把前面几个属性拷贝过来,所以employee剩余的属性需要自己去设置
//设置账号的状态,默认正常状态 1表示正常 0表示锁定
employee.setStatus(StatusConstant.ENABLE); //常量类的方式,比硬编码更灵活
//设置密码,默认密码123456 md5加密,并且是常量类的方式调用123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//TODO 设置当前记录创建人id和修改人id
// 其实指的就是我们当前这个登录用户的id,通过这些参数获取不到,所以暂时固定写死一个值,
// 后面会学习一个新的技术来解决这个问题。
employee.setCreateUser(10L);//目前写个假数据,后期修改
employee.setUpdateUser(10L);
employeeMapper.insert(employee);//后续步骤定义
}
在sky-common模块com.sky.constants包下已定义StatusConstant.java
package com.sky.constant;
/**
* 状态常量,启用或者禁用
*/
public class StatusConstant {
//启用
public static final Integer ENABLE = 1;
//禁用
public static final Integer DISABLE = 0;
}
1.2.5 Mapper层
在EmployeeMapper中声明insert方法
com.sky.EmployeeMapper中添加方法
/**
* 插入员工数据
* @param 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);
在application.yml中已开启驼峰命名,故id_number和idNumber可对应。
mybatis:
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
1.3 功能测试
代码已经发开发完毕,对新增员工功能进行测试。
功能测试实现方式:
- 通过接口文档测试
- 通前后端联调测试
接下来我们使用上述两种方式分别测试。
1.3.1 接口文档测试(常用,全局参数设置)
启动服务:访问http://localhost:8080/doc.html,进入新增员工接口
json数据:
{
"id": 0,
"idNumber": "111222333444555666",
"name": "xiaozhi",
"phone": "13812344321",
"sex": "1",
"username": "小智"
}
添加断点,debug运行
响应码:401 报错,发现请求并没有停留在断点位置
原因:项目初始代码有一个拦截器JwtTokenAdminInterceptor,它是jwt令牌校验的拦截器,从请求头里面获取出令牌,之后对令牌进行一个校验。显然这一次请求没校验通过,因为刚才执行这个接口的时候 并没有把这个令牌给它提交过来
通过断点调试:进入到JwtTokenAdminInterceptor拦截器
添加断点此时token为null,所以它在校验的时候会抛出异常 ,被下面catch捕获设置状态码为401,之后返回
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌 jwtProperties.getAdminTokenName()获取为token
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
报错原因:由于JWT令牌校验失败,导致EmployeeController的save方法没有被调用
解决方法:调用员工登录接口获得一个合法的JWT令牌
使用admin用户登录获取令牌
添加令牌:
将合法的JWT令牌添加到全局参数中
文档管理–>全局参数设置–>添加参数
接口测试:
其中,请求头部含有JWT令牌
查看employee表:
测试成功。
1.3.2 前后端联调测试
启动nginx,访问 http://localhost
登录–>员工管理–>添加员工
保存后,查看employee表
测试成功。
注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
1.4 代码完善(ThreadLocal)
目前,程序存在的问题主要有两个:
- 录入的用户名已存,抛出的异常后没有处理
- 用户名在数据库存储时设置了唯一约束
- 新增员工时,创建人id和修改人id设置为固定值
接下来,我们对上述两个问题依次进行分析和解决。
1.4.1 问题一:用户名重复,需要处理异常
描述:录入的用户名已存,抛出的异常后没有处理
分析:
新增username=zhangsan的用户,若employee表中之前已存在。
后台报错信息:
查看employee表结构:
发现,username已经添加了唯一约束,不能重复。
解决:
通过全局异常处理器来处理。
进入到sky-server模块,com.sky.hander包下,GlobalExceptionHandler.java添加方法
/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username' 出现异常后控制台输出的异常信息
//捕获到异常后具体如何处理呢???
//思路:先获取异常信息,之后判断异常信息是否包含Duplicate entry(重复的键值对)这个关键字,
// 如果包含就可以确认输出的日志确实是上面报错输出的异常信息,接下来希望给前端返回一个
// zhangsan已存在,所以需要把zhangsan这个字符串动态的提取出来,之后在拼接常量类中设置的属性 String ALREADY_EXISTS = "已存在";
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");//通过空格进行分割,得到一个数组对象
String username = split[2]; //zhangsan 在第3个位子,也就是数组下标为2的位置
String msg = username + MessageConstant.ALREADY_EXISTS; //之后进行拼接
return Result.error(msg); //返回统一响应结果 xxx已存在
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR); //未知错误
}
}
进入到sky-common模块,在MessageConstant.java添加
public static final String ALREADY_EXISTS = "已存在";
再次,接口测试:
1.4.2 问题二:新增员工,创建人id修改为动态获取
描述:新增员工时,创建人id和修改人id设置为固定值
分析:
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//................
//当前设置的id为固定值10//
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
//
//.................................
employeeMapper.insert(employee);//后续步骤定义
}
解决:
需要先看一下基于jwt的这种认证的方式,只有把这个流程搞明白之后才能够想办法来获取当前登录用户的id。
通过某种方式动态获取当前登录员工的id。
在拦截器这个位置可以把请求拦截下来,然后把用户携带过来的token进行一个校验,如果校验通过了说明登录成功,此时token里面就存在登录用户的id,因为我们登录成功之后在生成token的时候已经把这个id给它放进去了,所以反向解析也可以吧这个id解析出来。
员工登录成功后会生成JWT令牌,存入用户id,之后响应给前端:
在sky-server模块
package com.sky.controller.admin;
/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
//.........
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
//............
return Result.success(employeeLoginVO);
}
}
后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
拦截器把这个token拦截下来之后,如果校验通过就可以把这个empid给它取出来,因为当时生成的时候就放进去了,所以现在肯定可以解析出来,所以我们可以在拦截器这个位置把id取到。
JwtTokenAdminInterceptor.java
package com.sky.interceptor;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//..............
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
//解析后的结果是一个Map集合
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
思考:在拦截器位置解析出登录员工id后,如何传递给Service的save方法?
通过ThreadLocal进行传递。
1.4.3 ThreadLocal
介绍:
ThreadLocal 并不是一个Thread线程,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。只要在这个线程整个生命周期之内,就可以共享这一份存储空间。
客户端每次发起的请求,tomact服务器都会给我们分配一个单独的线程,然后在这个线程上可能要执行不同的代码,比如拦截器的代码、controller层的代码、service层的代码,它们都属于同一个线程 此时就可以使用ThreadLocal,将对应的数据存进去 然后在对应的地方给它取出来
常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
对ThreadLocal有了一定认识后,接下来继续解决问题二
初始工程中已经封装了 ThreadLocal 操作的工具类:
在sky-common模块
package com.sky.context;
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
在拦截器中解析出当前登录员工id,并放入线程局部变量中:
在sky-server模块中,拦截器:
package com.sky.interceptor;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//.............................
//2、校验令牌
try {
//.................
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
/将用户id存储到ThreadLocal
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//......................
}
}
}
在Service中获取线程局部变量中的值:
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
//.............................
//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
测试:使用admin(id=1)用户登录后添加一条记录
查看employee表记录
1.5 代码提交
点击提交:
提交过程中,出现提示:
继续push:
推送成功:
2. 员工分页查询(员工管理)
2.1 需求分析和设计
2.1.1 产品原型
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 “员工姓名”。
查询员工原型:
业务规则:
- 根据页码展示员工信息
- 每页展示10条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
2.1.2 接口设计
找到资料–>项目接口文档–>苍穹外卖-管理端接口.html
- 请求方式:查询所以为get请求
- 提交的参数:页码,每页的记录数,员工的姓名
- 响应的数据:总记录数total,当前页展示的数据集合records
注意事项:
- 请求参数类型为Query,不是json格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan
- 返回数据中records数组中使用Employee实体类对属性进行封装。
2.2 代码开发
2.2.1 设计DTO类
根据请求参数进行封装,在sky-pojo模块中
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeePageQueryDTO implements Serializable {
//员工姓名
private String name;
//页码
private int page;
//每页显示记录数
private int pageSize;
}
2.2.2 封装PageResult
后面所有的分页查询,统一都封装为PageResult对象。
在sky-common模块
package com.sky.result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
员工信息分页查询后端返回的对象类型为: Result
package com.sky.result;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
2.2.3 Controller层
在sky-server模块中,com.sky.controller.admin.EmployeeController中添加分页查询方法。
/**
* 员工分页查询
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//后续定义
return Result.success(pageResult);
}
2.2.4 Service层接口
在EmployeeService接口中声明pageQuery方法:
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
2.2.5 Service层实现类(mybatis 分页插件)
在EmployeeServiceImpl中实现pageQuery方法:
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//需要在查询功能之前开启分页功能:当前页的页码 每页显示的条数
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
//这个方法有返回值为Page对象,里面保存的是分页之后的相关数据
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义
long total = page.getTotal(); //获取总记录数
List<Employee> records = page.getResult(); //当前页数据集合
return new PageResult(total, records); //封装到PageResult中
}
注意:此处使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。底层基于 mybatis 的拦截器实现
原理:它会把后面的一条sql进行一个动态的拼接,类似于mybatis的动态sql,他会动态的把这个limit关键字给我们拼进去,并且把这2个参数动态的去计算,因为我们已经把页码和每页的记录数传给它了,动态的去算之后拼接到sql语句中,最终实现了分页查询。
好处:这个字符串拼接不需要自己去做了,而是让这个插件去做。
故在pom.xml文中添加依赖(初始工程已添加)
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper}</version>
</dependency>
2.2.6 Mapper层
在 EmployeeMapper 中声明 pageQuery 方法:
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在 src/main/resources/mapper/EmployeeMapper.xml 中编写SQL:
- name是员工的姓名,姓名作为条件我们用等值查询好像并不合适,应该使用模糊查询更合理一些,使用concat字符串函数动态拼接为字符串。
- 不需要自己手动写Limit关键字了,因为使用PageHelper 这个插件可以动态的给我们去追加Limit之后的内容。
<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>
2.3 功能测试
可以通过接口文档进行测试,也可以进行前后端联调测试。
接下来使用两种方式分别测试:
2.3.1 接口文档测试(token失效,重新设置全局参数)
重启服务:访问http://localhost:8080/doc.html,进入员工分页查询
响应结果:返回401报错,JWT令牌在校验时出现了问题
明明之前已经配置过全局参数了为什么还是没有校验通过呢???
原因:在生成jwt令牌时设置了token的有效期为2个小时
解决:失效之后重新进行登录,生成一个新的令牌,之后把这个新的令牌复制到全局参数里面。
把原先的导航栏叉掉,点击一个新的员工分页查询窗口发送请求,可以看到此时正确响应结果
2.3.2 前后端联调测试
点击员工管理
输入员工姓名为zhangsan
不难发现,最后操作时间格式不清晰,在代码完善中解决。
2.4 代码完善(日期时间格式化返回)
问题描述:操作时间字段显示有问题。
解决方式:
1). 方式一
在属性上加上注解,对日期进行格式化
但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。
测试:
- 实体类上一个加一个不加
- 重启服务,查看页面效果,添加注解的是格式化显示时间
2). 方式二(推荐 )
在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
/**
* 扩展Spring MVC框架的消息转化器
*
* 程序启动时会自动加载这个方法
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//创建出来的消息转换器并没有交给框架,框架也不会使用消息转换器,所以需要
// 将自己的消息转化器converter加入容器converters中,converters存放的是整个
// SpringMVC框架所使用到的消息转换器,是一个集合。
//converters.add(converter):容器中自带的有一些消息转换器,这样写相当于自己的消息转换器排在最后面,默认使用不到
//converters.add(0,converter):优先使用自己的消息转换器,0表示索引,自己的消息转换器放在第一位 优先使用
converters.add(0,converter);
}
对象转换器类:JacksonObjectMapper,这个类不需要自己编写,只需要知道这个类起一个大概的作用即可。
package com.sky.json;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
添加后,再次测试
时间格式定义,sky-common模块中
package com.sky.json;
public class JacksonObjectMapper extends ObjectMapper {
//.......
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
//.......
}
}
2.5 代码提交
后续步骤和新增员工代码提交一致,不再赘述。
3. 启用禁用员工账号(员工管理)
3.1 需求分析与设计
3.1.1 产品原型
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。
启禁用员工原型:
业务规则:
- 可以对状态为“启用” 的员工账号进行“禁用”操作
- 可以对状态为“禁用”的员工账号进行“启用”操作
- 状态为“禁用”的员工账号不能登录系统
3.1.2 接口设计
1). 路径参数携带状态值。
2). 同时,把id传递过去,明确对哪个用户进行操作。
只传一个路径参数不够,那你要对哪个账号进行状态的修改呢???所以需要把这个员工的id给提交过去。
3). 返回数据code状态是必须,其它是非必须。
3.2 代码开发
3.2.1 Controller层
在sky-server模块中,根据接口设计中的请求参数形式对应的在 EmployeeController 中创建启用禁用员工账号的方法:
/**
* 启用禁用员工账号
* 返回值类型Result的泛型可加可不加,查询操作需要返回data数据 建议加上泛型
* 对于非查询类的泛型就不需要了,因为最终只要给它返回一个code即可,data往往是空的
* @param status 路径参数,RESTful风格接收
* @param id 普通参数,普通方式接收
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用员工账号:{},{}",status,id);
employeeService.startOrStop(status,id);//后绪步骤定义 (状态,员工的id)
return Result.success();
}
3.2.2 Service层接口
在 EmployeeService 接口中声明启用禁用员工账号的业务方法:
/**
* 启用禁用员工账号
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
3.2.3 Service层实现类(更新写为动态sql,根据传递的参数更新多个字段)
在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:
/**
* 启用禁用员工账号:本质上是根据员工的id修改员工表中的status状态字段
*
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
//update employee set status = ? where id = ?
//为了修改的通用性,建议把这个update的语句写成是一个动态的,也就是说不单单是
// 修改这个status,根据你传递过来的参数的不同可以修改多个字段,这样更新语句的sql通用性更好。
//动态更新直接传递这2个参数不合适,传递的应该是一个实体类,通过builder方式构建(前面讲过)
// 创建一个实体类对象,并且设置这2个属性值
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
3.2.4 Mapper层
在 EmployeeMapper 接口中声明 update 方法:
/**
* 根据主键动态修改属性
* @param employee
*/
void update(Employee employee);
在 EmployeeMapper.xml 中编写SQL:
把可能修改的字段都列到这了,这样就编写了一个动态的sql,所以后期只要是修改类的操作 都可以调用这条sql。
<update id="update" parameterType="Employee">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</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>
3.3 功能测试
3.3.1 接口文档测试
测试前,查询employee表中员工账号状态
开始测试
测试完毕后,再次查询员工账号状态
3.3.2 前后端联调测试
测试前:
点击启用:
3.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。
4. 编辑员工(员工管理)
4.1 需求分析与设计
4.1.1 产品原型
在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。
员工列表原型:
修改页面原型:
注:点击修改时,数据应该正常回显到修改页面。
4.1.2 接口设计
根据上述原型图分析,编辑员工功能涉及到两个接口:
- 根据id查询员工信息
- 编辑员工信息
1). 根据id查询员工信息
2). 编辑员工信息
注:因为是修改功能,请求方式可设置为PUT。
4.2 代码开发
4.2.1 回显员工信息功能
1). Controller层
在 EmployeeController 中创建 getById 方法:
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
2). Service层接口
在 EmployeeService 接口中声明 getById 方法:
/**
* 根据id查询员工
* @param id
* @return
*/
Employee getById(Long id);
3). Service层实现类
在 EmployeeServiceImpl 中实现 getById 方法:
/**
* 根据id查询员工
*
* @param id
* @return
*/
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
//查询出来的信息包含密码,这个密码也要传给前端,虽然这个后端的密码已经加密了,
// 但是现在连这个加密的密码都不想让你看,查出来密码后给它修改为****穿给前端,
// 进一步加强这个安全性。
employee.setPassword("****");
return employee;
}
4). Mapper层
在 EmployeeMapper 接口中声明 getById 方法:
/**
* 根据id查询员工信息
* @param id
* @return
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
4.2.2 修改员工信息功能
1). Controller层
在 EmployeeController 中创建 update 方法:
/**
* 编辑员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
2). Service层接口
在 EmployeeService 接口中声明 update 方法:
/**
* 编辑员工信息
* @param employeeDTO
*/
void update(EmployeeDTO employeeDTO);
3). Service层实现类
在 EmployeeServiceImpl 中实现 update 方法:
/**
* 编辑员工信息
*
* @param employeeDTO
*/
@Override
public void update(EmployeeDTO employeeDTO) {
//在上面写启用禁用功能时,已经写了一个通用的update方法,这里直接使用
// 这个通用的update方法即可,这个方法需要的是Employee类型而这里
// 传递的是EmployeeDTO类型,所以需要进行一个数据转换。
Employee employee = new Employee();
//employeeDTO的属性拷贝到employee对应的属性上
BeanUtils.copyProperties(employeeDTO, employee); //对象属性拷贝 (新增员工时讲过)
//设置employee剩余的属性
employee.setUpdateTime(LocalDateTime.now());//更新时间
employee.setUpdateUser(BaseContext.getCurrentId());//修改人的id(新增员工时讲过 通过ThreadLocal获得)
employeeMapper.update(employee);
}
在实现启用禁用员工账号功能时,已实现employeeMapper.update(employee),在此不需写Mapper层代码。
4.3 功能测试
4.3.1 接口文档测试
分别测试根据id查询员工信息和编辑员工信息两个接口
1). 根据id查询员工信息
查询employee表中的数据,以id=4的记录为例
开始测试
获取到了id=4的相关员工信息
2). 编辑员工信息
修改id=4的员工信息,name由zhangsan改为张三丰,username由张三改为zhangsanfeng。
查看employee表数据
4.3.2 前后端联调测试
进入到员工列表查询
对员工姓名为杰克的员工数据修改,点击修改,数据已回显
修改后,点击保存
4.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。
5. 导入分类模块功能代码(分类管理)
说明:从技术层面来说,这个分类管理模块跟这个员工管理模块是非常类似的,无非是操作的表不一样,实际上都属于单表的增删改查,技术难度也是非常类似的,所以最后这个分类管理直接提供现成的代码,那它导入到我们的项目中直接使用就行了。
5.1 需求分析与设计
5.1.1 产品原型
后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。
先来分析菜品分类相关功能。
新增菜品分类:当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。
菜品分类分页查询:系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
根据id删除菜品分类:在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
修改菜品分类:在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
启用禁用菜品分类:在分类管理列表页面,可以对某个分类进行启用或者禁用操作。
分类类型查询:当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。
分类管理原型:
业务规则:
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为“禁用”
- 为什么是禁用而不是启用呢???
- eg:新添加的一个分类,如果这个分类默认是启用状态 ,它就会展示在我们的移动端,因为当前是一个新的分类 这个分类下面肯定是没有菜品的,所以此时只是在移动端展示一个分类的话是没有任何意义的。所以设置新添加的分类默认是禁用状态,禁用状态其实是不会展示在我们这个移动端,防止了这种情况的出现。
5.1.2 接口设计
根据上述原型图分析,菜品分类模块共涉及6个接口。
- 新增分类
- 分类分页查询
- 根据id删除分类
- 修改分类
- 启用禁用分类
- 根据类型查询分类
接下来,详细地分析每个接口。
找到资料–>项目接口文档–>苍穹外卖-管理端接口.html
1). 新增分类
2). 分类分页查询
3). 根据id删除分类
4). 修改分类
5). 启用禁用分类
6). 根据类型查询分类
5.1.3 表设计
category表结构:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 分类名称 | 唯一 |
type | int | 分类类型 | 1菜品分类 2套餐分类 |
sort | int | 排序字段 | 用于分类数据的排序 |
status | int | 状态 | 1启用 0禁用 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
5.2 代码导入(删除分类前提,分类下面没有数据)
导入资料中的分类管理模块功能代码即可
可按照mapper–>service–>controller依次导入,这样代码不会显示相应的报错。
进入到sky-server模块中
5.2.1 Mapper层
-
问题
:当前这个模块不是我们的分类模块吗,除了分类的Mapper为什么还有菜品的菜品的Mapper、套餐的Mapper呢??? -
因为:这个地方有一个业务的限定,要删除分类的时候并不是直接删除,而是需要判断一下这个分类下面有没有挂菜品、套餐,也就是说我们需要去查询菜单表、套餐表,看看这个菜品还有套餐是不是有属于当前这个分类的,所以这个地方会使用到菜品和套餐的mapper。
DishMapper.java:菜品的Mapper
package com.sky.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
}
SetmealMapper.java:套餐的Mapper
package com.sky.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface SetmealMapper {
/**
* 根据分类id查询套餐的数量
* @param id
* @return
*/
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);
}
CategoryMapper.java:分类的Mapper
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CategoryMapper {
/**
* 插入数据
* @param category
*/
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Category category);
/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);
/**
* 根据id删除分类
* @param id
*/
@Delete("delete from category where id = #{id}")
void deleteById(Long id);
/**
* 根据id修改分类
* @param category
*/
void update(Category category);
/**
* 根据类型查询分类
* @param type
* @return
*/
List<Category> list(Integer type);
}
CategoryMapper.xml,进入到resources/mapper目录下
<?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.CategoryMapper">
<select id="pageQuery" resultType="com.sky.entity.Category">
select * from category
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
<if test="type != null">
and type = #{type}
</if>
</where>
order by sort asc , create_time desc
</select>
<update id="update" parameterType="Category">
update category
<set>
<if test="type != null">
type = #{type},
</if>
<if test="name != null">
name = #{name},
</if>
<if test="sort != null">
sort = #{sort},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
<select id="list" resultType="Category">
select * from category
where status = 1
<if test="type != null">
and type = #{type}
</if>
order by sort asc,create_time desc
</select>
</mapper>
5.2.2 Service层
CategoryService.java
package com.sky.service;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;
public interface CategoryService {
/**
* 新增分类
* @param categoryDTO
*/
void save(CategoryDTO categoryDTO);
/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);
/**
* 根据id删除分类
* @param id
*/
void deleteById(Long id);
/**
* 修改分类
* @param categoryDTO
*/
void update(CategoryDTO categoryDTO);
/**
* 启用、禁用分类
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
/**
* 根据类型查询分类
* @param type
* @return
*/
List<Category> list(Integer type);
}
CategoryServiceImpl.java
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 分类业务层
*/
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
/**
* 新增分类
* @param categoryDTO
*/
@Override
public void save(CategoryDTO categoryDTO) {
Category category = new Category();
//属性拷贝
BeanUtils.copyProperties(categoryDTO, category);
//分类状态默认为禁用状态0
category.setStatus(StatusConstant.DISABLE);
//设置创建时间、修改时间、创建人、修改人
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.insert(category);
}
/**
* 分页查询
* @param categoryPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
//下一条sql进行分页,自动加入limit关键字分页
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
/**
* 根据id删除分类
*
* 思路:
* 分类的删除是根据传入的参数分类id进行删的
* 首先根据这个分类id查询菜品表,如果查询的结果有记录数,说明当前分类下面有菜品,所以不能直接删除
* 其次根据这个分类id查询套餐表,如果查询的结果有记录数,说明当前分类下面有套餐,所以不能直接删除
* 最终分类下面没有数据了,此时就可以删除分类了。
* @param id
*/
@Override
public void deleteById(Long id) {
//查询当前分类是否关联了菜品,如果关联了就抛出业务异常
Integer count = dishMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
}
//查询当前分类是否关联了套餐,如果关联了就抛出业务异常
count = setmealMapper.countByCategoryId(id);
if(count > 0){
//当前分类下有菜品,不能删除
throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
}
//删除分类数据
categoryMapper.deleteById(id);
}
/**
* 修改分类
* @param categoryDTO
*/
@Override
public void update(CategoryDTO categoryDTO) {
Category category = new Category();
BeanUtils.copyProperties(categoryDTO,category);
//设置修改时间、修改人
category.setUpdateTime(LocalDateTime.now());
category.setUpdateUser(BaseContext.getCurrentId());
categoryMapper.update(category);
}
/**
* 启用、禁用分类
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
Category category = Category.builder()
.id(id)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
categoryMapper.update(category);
}
/**
* 根据类型查询分类
* @param type
* @return
*/
@Override
public List<Category> list(Integer type) {
return categoryMapper.list(type);
}
}
5.2.3 Controller层
CategoryController.java
package com.sky.controller.admin;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 分类管理
*/
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 新增分类
* @param categoryDTO
* @return
*/
@PostMapping
@ApiOperation("新增分类")
public Result<String> save(@RequestBody CategoryDTO categoryDTO){
log.info("新增分类:{}", categoryDTO);
categoryService.save(categoryDTO);
return Result.success();
}
/**
* 分类分页查询
* @param categoryPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分类分页查询")
public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
log.info("分页查询:{}", categoryPageQueryDTO);
PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
return Result.success(pageResult);
}
/**
* 删除分类
* @param id
* @return
*/
@DeleteMapping
@ApiOperation("删除分类")
public Result<String> deleteById(Long id){
log.info("删除分类:{}", id);
categoryService.deleteById(id);
return Result.success();
}
/**
* 修改分类
* @param categoryDTO
* @return
*/
@PutMapping
@ApiOperation("修改分类")
public Result<String> update(@RequestBody CategoryDTO categoryDTO){
categoryService.update(categoryDTO);
return Result.success();
}
/**
* 启用、禁用分类
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用分类")
public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
categoryService.startOrStop(status,id);
return Result.success();
}
/**
* 根据类型查询分类
* @param type
* @return
*/
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
List<Category> list = categoryService.list(type);
return Result.success(list);
}
}
全部导入完毕后,进行编译
5.3 功能测试
重启服务,访问http://localhost:80,进入分类管理
分页查询:
分类类型:
启用禁用:
点击禁用
修改:
回显
修改后
新增:
点击确定,查询列表
删除:
删除后,查询分类列表
删除成功
5.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)