# day02 项目内容

  • 新增员工
  • 员工分页查询
  • 启用禁用员工账号
  • 编辑员工
  • 导入分类模块功能代码

# 新增员工

# 需求分析和设计

# 产品原型

一般在做需求分析时,往往都是对照着产品原型进行分析,因为产品原型比较直观,便于我们理解业务。

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。

image-20240608001717492

当填写完表单信息,点击 "保存" 按钮后,会提交该表单的数据到服务端,在服务端中需要接受数据,然后将数据保存至数据库中。

注意事项:

  1. 账号必须是唯一的
  2. 手机号为合法的 11 位手机号码
  3. 身份证号为合法的 18 位身份证号码
  4. 密码默认为 123456

# 接口设计

找到资料 --> 项目接口文档 --> 苍穹外卖 - 管理端接口.html

image-20240608002210088

明确新增员工接口的请求路径、请求方式、请求参数、返回数据

本项目约定:

  • 管理端发出的请求,统一使用 /admin 作为前缀。
  • 用户端发出的请求,统一使用 /user 作为前缀。

# 表设计

新增员工,其实就是将我们新增页面录入的员工数据插入到 employee 表。

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)姓名
usernamevarchar(32)用户名唯一
passwordvarchar(64)密码
phonevarchar(11)手机号
sexvarchar(2)性别
id_numbervarchar(18)身份证号
statusInt账号状态1 正常 0 锁定
create_timeDatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人 id
update_userbigint最后修改人 id

其中,employee 表中的 status 字段已经设置了默认值 1,表示状态正常。

# 代码开发

# 设计 DTO 类

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

前端传递参数列表:

image-20240608002702650

思考:

是否可以使用对应的实体类来接收呢?

以下是 sky-pojo 下 entity 中的 employee 实体类,我们可以看出实体类中的属性要比前端传过来的属性多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;

private Integer status;

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;

private Long createUser;

private Long updateUser;

}

[!NOTE]

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

由于上述传入参数和实体类有较大差别,所以自定义 DTO 类。

进入 sky-pojo 模块,在 com.sky.dto 包下,已定义 EmployeeDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

}

# Controller 层

EmployeeController 中创建新增员工方法

进入到 sky-server 模块中,在 com.sky.controller.admin 包下,在 EmployeeController 中创建新增员工方法,接收前端提交的参数。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.save(employeeDTO);//该方法后续步骤会定义
return Result.success();
}

[!CAUTION]

Result 类定义了后端统一返回结果格式。

进入 sky-common 模块,在 com.sky.result 包下定义了 Result.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
}

}

# Service 层接口

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

进入到 sky-server 模块中,com.sky.server.EmployeeService

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

# Service 层实现类

在 EmployeeServiceImpl 中实现新增员工方法

com.sky.server.impl.EmployeeServiceImpl 中创建方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();

//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);

//设置账号的状态,默认正常状态 1表示正常 0表示锁定
employee.setStatus(StatusConstant.ENABLE);

//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

//设置当前记录创建人id和修改人id
employee.setCreateUser(10L);//目前写个假数据,后期修改
employee.setUpdateUser(10L);

employeeMapper.insert(employee);//后续步骤定义
}

在 sky-common 模块 com.sky.constants 包下已定义 StatusConstant.java, 为了解耦,编码更加规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.constant;

/**
* 状态常量,启用或者禁用
*/
public class StatusConstant {

//启用
public static final Integer ENABLE = 1;

//禁用
public static final Integer DISABLE = 0;
}

# Mapper 层

在 EmployeeMapper 中声明 insert 方法

com.sky.EmployeeMapper 中添加方法

1
2
3
4
5
6
7
8
/**
* 插入员工数据
* @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 可对应。

1
2
3
4
mybatis:
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true

# 功能测试

代码已经发开发完毕,对新增员工功能进行测试。

功能测试实现方式:

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

接下来我们使用上述两种方式分别测试。

# 接口文档测试

启动服务:

访问 http://localhost:8080/doc.html,进入新增员工接口

image-20240608142041870

json 数据:

1
2
3
4
5
6
7
8
{
"id": 0,
"idNumber": "452601198307190014",
"name": "大纱布",
"phone": "15223422134",
"sex": "1",
"username": "小智"
}

如果 401 报错,是我们的令牌过期了,这是因为我们在 application.yml 文件里设置了过期时间为两个小时

1
2
# 设置jwt过期时间
admin-ttl: 7200000

通过断点调试:

进入到 JwtTokenAdminInterceptor 拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 校验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 用户登录获取令牌

image-20240608145051110

添加令牌:

将合法的 JWT 令牌添加到全局参数中

文档管理 --> 全局参数设置 --> 添加参数

image-20240608145300467

接口测试:

添加完全局参数后,一定要关闭之前的新增员工调试窗口,重新打开,然后你会发现在请求头部有红色数字 1 的提示,证明 token 已经添加到请求头部

image-20240608150559333

响应码 200,测试成功

image-20240608150710758

查看数据库,成功插入新员工

image-20240608150744926

# 前后端联调测试

启动 nginx, 访问 http://localhost

登录 --> 员工管理 --> 添加员工

image-20240608141128778

保存后,查看 employee 表

image-20240608141235549

测试成功。

[!CAUTION]

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

# 代码完善

目前,程序存在的问题主要有两个:

  • 录入的用户名已存,抛出的异常后没有处理
  • 新增员工时,创建人 id 和修改人 id 设置为固定值

接下来,我们对上述两个问题依次进行分析和解决。

# 问题一

描述:

录入的用户名已存,抛出的异常后没有处理

分析:

新增 username=dashabu 的用户,若 employee 表中之前已存在。

image-20240608151154493

后台报错信息:

image-20240608151222015

查看 employee 表结构后,我们发现 username 为 unique

解决:

通过全局异常处理器来处理。

进入到 sky-server 模块,com.sky.hander 包下,GlobalExceptionHandler.java 添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//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];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}

进入到 sky-common 模块,在 MessageConstant.java 添加

1
public static final String ALREADY_EXISTS = "已存在";

再次,接口测试:

image-20240608152337522

# 问题二

描述:

新增员工时,创建人 id 和修改人 id 设置为固定值

分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//................
//////////当前设置的id为固定值10//////////
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
//////////////////////////////////////
//.................................

employeeMapper.insert(employee);//后续步骤定义
}

解决:

通过某种方式动态获取当前登录员工的 id。

image-20240608152545354

员工登录成功后会生成 JWT 令牌并响应给前端:

在 sky-server 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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:

JwtTokenAdminInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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);
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 进行传递。

# ThreadLocal

介绍:

ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。
ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

常用方法:

  • public void set (T value) 设置当前线程的线程局部变量的值
  • public T get () 返回当前线程所对应的线程局部变量的值
  • public void remove () 移除当前线程的线程局部变量

对 ThreadLocal 有了一定认识后,接下来继续解决问题二

[!CAUTION]

客户端发送的每次请求,后端的 Tomcat 服务器都会分配一个单独的线程来处理请求

image-20240608153205810

初始工程中已经封装了 ThreadLocal 操作的工具类:

在 sky-common 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 模块中,拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 中获取线程局部变量中的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
//.............................

//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.insert(employee);
}

测试:使用 admin (id=1) 用户登录后添加一条记录

image-20240608154111204

查看 employee 表记录

image-20240608154154880

# 员工分页查询

# 需求分析和设计

# 产品原型

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中,除了分页条件以外,还有一个查询条件 "员工姓名"。

image-20240608154332431

业务规则

  • 根据页码展示员工信息
  • 每页展示 10 条数据
  • 分页查询时可以根据需要,输入员工姓名进行查询

# 接口设计

找到资料 --> 项目接口文档 --> 苍穹外卖 - 管理端接口.html

image-20240608154538986

image-20240608154554029

注意事项:

  • 请求参数类型为 Query,不是 json 格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan
  • 返回数据中 records 数组中使用 Employee 实体类对属性进行封装。

# 代码开发

# 设计 DTO 类

根据请求参数进行封装,在 sky-pojo 模块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

}

# 封装 PageResult

后面所有的分页查询,统一都封装为 PageResult 对象。

在 sky-common 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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<PageResult>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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;
}

}

# Controller 层

在 sky-server 模块中,com.sky.controller.admin.EmployeeController 中添加分页查询方法。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 员工分页查询
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//后续定义
return Result.success(pageResult);
}

# Service 层接口

在 EmployeeService 接口中声明 pageQuery 方法:

1
2
3
4
5
6
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

# Service 层实现类

在 EmployeeServiceImpl 中实现 pageQuery 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义

long total = page.getTotal();
List<Employee> records = page.getResult();

return new PageResult(total, records);
}

[!IMPORTANT]

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

故在 pom.xml 文中添加依赖 (初始工程已添加)

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper}</version>
</dependency>

# Mapper 层

在 EmployeeMapper 中声明 pageQuery 方法:

1
2
3
4
5
6
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

在 src/main/resources/mapper/EmployeeMapper.xml 中编写 SQL:

1
2
3
4
5
6
7
8
9
<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>

# 功能测试

测试步骤和之前类似,可以通过接口文档进行测试,也可以进行前后端联调测试。

但是在测试过程中我们发现

image-20240608160914732

image-20240608161020915

时间格式不对

# 代码完善

问题描述:操作时间字段显示有问题。

解决方式:

1). 方式一

在属性上加上注解,对日期进行格式化

image-20221112103501581

但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

2). 方式二(推荐 )

在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式处理

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

添加后,再次测试

image-20240608161606370

时间格式定义,sky-common 模块中,可以作格式修改,只需要注释掉某些格式,选择你想呈现的格式即可

1
2
3
4
5
6
7
8
9
10
package com.sky.json;

public class JacksonObjectMapper extends ObjectMapper {

//.......
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
//.......

}
}

# 启用禁用员工账号

# 需求分析与设计

# 产品原型

image-20240608161741302

业务规则:

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

# 接口设计

image-20240608161837681

1). 路径参数携带状态值。

2). 同时,把 id 传递过去,明确对哪个用户进行操作。

3). 返回数据 code 状态是必须,其它是非必须。

# 代码开发

# Controller 层

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 启用禁用员工账号
* @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();
}

# Service 层接口

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

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

# Service 层实现类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 启用禁用员工账号
*
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();

employeeMapper.update(employee);
}

# Mapper 层

在 EmployeeMapper 接口中声明 update 方法:

1
2
3
4
5
/**
* 根据主键动态修改属性
* @param employee
*/
void update(Employee employee);

在 EmployeeMapper.xml 中编写 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

# 功能测试

image-20240608162502800

# 编辑员工

# 需求分析与设计

# 产品原型

在员工管理列表页面点击 "编辑" 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 "保存" 按钮完成编辑操作。

员工列表原型:

image-20240608162648951

修改页面原型

注:点击修改时,数据应该正常回显到修改页面。

image-20240608162852454

# 接口设计

根据上述原型图分析,编辑员工功能涉及到两个接口:

  • 根据 id 查询员工信息
  • 编辑员工信息

1). 根据 id 查询员工信息

image-20240608173129961

2). 编辑员工信息

image-20240608163148338

注:因为是修改功能,请求方式可设置为 PUT。

# 代码开发

# 回显员工信息功能

# Controller 层

在 EmployeeController 中创建 getById 方法:

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

# Service 层接口

在 EmployeeService 接口中声明 getById 方法:

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

# Service 层实现类

在 EmployeeServiceImpl 中实现 getById 方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据id查询员工
*
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}

# Mapper 层

在 EmployeeMapper 接口中声明 getById 方法:

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

# 修改员工信息功能

# Controller 层

在 EmployeeController 中创建 update 方法:

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

# Service 层接口

在 EmployeeService 接口中声明 update 方法:

1
2
3
4
5
/**
* 编辑员工信息
* @param employeeDTO
*/
void update(EmployeeDTO employeeDTO);

# Service 层实现类

在 EmployeeServiceImpl 中实现 update 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 编辑员工信息
*
* @param employeeDTO
*/
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);

employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.update(employee);
}

在实现启用禁用员工账号功能时,已实现 employeeMapper.update (employee),在此不需写 Mapper 层代码。

# 功能测试

image-20240608163813032

# 导入分类模块功能代码

# 需求分析与设计

# 产品原型

后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类套餐分类

先来分析菜品分类相关功能。

新增菜品分类:

当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。

菜品分类分页查询:

系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

根据 id 删除菜品分类:

在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

修改菜品分类:

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。

启用禁用菜品分类

在分类管理列表页面,可以对某个分类进行启用或者禁用操作。

分类类型查询:

当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。

分类管理原型:

image-20240608164009878

业务规则:

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

# 接口设计

根据上述原型图分析,菜品分类模块共涉及 6 个接口。

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

接下来,详细地分析每个接口。

找到资料 --> 项目接口文档 --> 苍穹外卖 - 管理端接口.html

1). 新增分类

image-20240608164326600

2). 分类分页查询

image-20240608164423610

3). 根据 id 删除分类

image-20240608164445904

4). 修改分类

image-20240608164506691

5). 启用禁用分类

image-20240608164528605

6). 根据类型查询分类

image-20240608164554841

# 表设计

category 表结构:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)分类名称唯一
typeint分类类型1 菜品分类 2 套餐分类
sortint排序字段用于分类数据的排序
statusint状态1 启用 0 禁用
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人 id
update_userbigint最后修改人 id

# 代码导入

导入资料中的分类管理模块功能代码即可

image-20240608164651042

可按照 mapper-->service-->controller 依次导入,这样代码不会显示相应的报错。

进入到 sky-server 模块中

# Mapper 层

DishMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?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>

# Service 层

CategoryService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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);
}

EmployeeServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.PasswordConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.exception.AccountLockedException;
import com.sky.exception.AccountNotFoundException;
import com.sky.exception.PasswordErrorException;
import com.sky.mapper.EmployeeMapper;
import com.sky.result.PageResult;
import com.sky.service.EmployeeService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class EmployeeServiceImpl implements EmployeeService {

@Autowired
private EmployeeMapper employeeMapper;

/**
* 员工登录
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();

//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);

//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}

//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}

//3、返回实体对象
return employee;
}

/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();

//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);

//设置账号的状态,默认正常状态 1表示正常 0表示锁定
employee.setStatus(StatusConstant.ENABLE);

//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.insert(employee);
}

/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

long total = page.getTotal();
List<Employee> records = page.getResult();

return new PageResult(total, records);
}

/**
* 启用禁用员工账号
*
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();

employeeMapper.update(employee);
}

/**
* 根据id查询员工
*
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}

/**
* 编辑员工信息
*
* @param employeeDTO
*/
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);

employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.update(employee);
}

}

# Controller 层

CategoryController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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);
}
}

[!IMPORTANT]

全部导入完毕后,进行编译,在 maven 里一定要进行整个项目的编译测试

image-20240608171747970

# 功能测试

重启服务,访问 http://localhost:80, 进入分类管理

这里测试过,就不展示了