效果预览
新增员工
1.需求分析
查看产品原型
接口设计
说明
- 管理端发出的请求, 统一使用/admin作为前缀
- 用户端发出的请求, 统一使用/user作为前缀
数据表设计
employee(员工)表
2.代码编写
设计DTO
当前端提交的数据和实体类中对应的属性差别较大时, 一般使用专门的DTO来封装数据
controller
/**
* 新增员工
*
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工: {}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
service
/**
* 保存员工
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
// 对象属性拷贝: 把前端属性拷贝到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());
// 设置创建人和修改人
// TODO: 后期替换真实ID
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
// 插入数据
employeeMapper.insert(employee);
}
mapper
/**
* 插入员工数据
* @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},#{id_number},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
void insert(Employee employee);
说明
数据表的字段名是_分隔, 实体类中的字段名是驼峰命名, 自动映射的前提要开启驼峰命名
3.功能测试
接口测试
全局设置请求头
测试接口
前后端联调
4.代码优化
员工账户重名异常处理
问题复现
修复
在全局异常处理器中处理sql异常
/**
* 处理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);
}
}
记录创建人id和修改人id
问题复现
问题分析
前端请求时, 会携带JWT令牌, 通过解析令牌可以拿到当前登录员工的id, 所以在拦截器中可以拿到当前用户id, 但是我们需要在server中使用这个id
ThreadLocal
ThreadLocal为每个线程提供一份储存空间, 具有线程隔离的效果, 只有在线程内才能获取到对应的值, 线程外不能访问
常用方法
- 设置当前线程的线程局部变量的值: public void set(T value)
- 返回当前线程所对应的线程局部变量的值: public T get()
- 移除当前线程的线程局部变量: public void remove()
- 为了方便使用, 在项目中会把ThreadLocal封装成工具类
tomcat服务器会把每一次的请求响应封装到一份线程中, 所以可以把解析出来的id保存到当前线程中, 然后在server中取值使用
代码修复
在拦截器中储存身份id
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
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);
// 保存当前用户id
BaseContext.setCurrentId(empId);
} catch (Exception ex) {
}
}
}
在service中取用身份id
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
/**
* 保存员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
... ...
// 设置创建人和修改人
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
}
}
员工分页查询
1.需求分析
查看产品原型
分析业务规则
- 根据页码展示员工信息
- 每页展示10条数据
- 分页查询时可以根据需要, 输入员工姓名进行查询
接口设计
2.代码编写
controller
1.设计DTO
根据接口参数设计DTO, 使用设计好的DTO对象封装前端参数
2.PageResult对象
所有的分页查询结果, 统一封装成PageResult对象
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
3.分页查询返回类型
分页查询返回的对象类型为: Result<PageResult>
4.封装接口
/**
* 员工分页查询
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
log.info("员工分页查询,参数为: {}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
service
使用MyBatis提供的 pagehelper 分页插件 快捷完成分页查询
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
/**
* 分页查询员工
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// 使用pagehelper进行分页查询
// startPage(当前页码值, 每页展示条数)
// 1, 设置查询参数
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
// 2, 执行查询语句
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
// 3, 获取查询结果
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total, records);
}
mapper
执行普通的查询语句即可, 插件会自动填充分页查询条件
这里还要手动拼接name查询条件, 使用到动态sql
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name!= ''">
and name like concat('%', #{name},'%')
</if>
</where>
order by create_time desc
</select>
3.测试
接口测试
前后端联调
4.优化
问题
测试出时间格式展示问题, 要进行时间格式处理
解决方式1: 手动指定
在属性上加入注解, 对日期进行格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
解决方式2: 统一处理
在WebMvcConfiguration中扩展Spring MVC的消息转换器, 统一对日期类型进行格式化处理
说明: 项目中已经提供了对象转换器 json/JackonObjectMapper
/**
* 扩展消息转换器
*
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 为消息转换器设置一个对象转换器, 对象转换器可以把java对象序列化为json对象
converter.setObjectMapper(new JacksonObjectMapper());
// 将自己的消息转换器加入到容器中(0代表索引->添加到集合前面优先执行)
converters.add(0, converter);
}
启用禁用员工账号
1.需求分析
查看产品原型
分析业务规则
- 当前"启用"的账号可以设置为"禁用"
- 当前"禁用"的账号可以设置为"启用"
- "禁用"状态的员工账号不能登录系统
接口设计
2.代码实现
controller
/**
* 启用禁用员工账号
* @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
使用构建器创建对象
/**
* 启用禁用员工账号
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
// 使用构建器创建对象, 是一种代码风格
// 作用与 new Employee一直
// 使用前提是该类使用了 @Builder 注解
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
mapper
把所有的修改操作封装到一个动态sql中, 方便代码复用
<update id="update">
update employee
<set>
<if test="name!=null and name!=''">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="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.测试
接口测试
联调测试
编辑员工
1.需求分析
查看原型
- 页面回显: 根据id查询员工信息
- 保存修改: 编辑员工信息
设计接口
1.查询员工
2.编辑员工
2.代码实现
查询接口
controller
/**
* 根据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
把查询到的员工信息的秘密重置一下, 进一步增加安全性
/**
* 根据id查询压员工信息
*
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
mapper
/**
* 根据id查询员工
*
* @param id
* @return
*/
@Select("select * from employee where id =#{id}")
Employee getById(Long id);
编辑接口
controller
/**
* 编辑员工信息
*
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO) {
log.info("编辑员工信息:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
service
/**
* 编辑员工信息
*
* @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);
}
mapper
复用之前的update方法, 完成数据修改
/**
* 编辑员工信息
*
* @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);
}