需求分析与设计
一:产品原型
一般在做需求分析时,往往都是对照着产品原型进行分析,因为产品原型比较直观,便于我们理解业务。
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。
新增员工原型:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
注意事项:
-
账号必须是唯一的
-
手机号为合法的11位手机号码
-
身份证号为合法的18位身份证号码
-
密码默认为123456
二:接口设计
新增——>post
请求数据格式——>json
返回数据格式——>json(Result)
明确新增员工接口的请求路径、请求方式、请求参数、返回数据。
本项目约定:
-
管理端发出的请求,统一使用/admin作为前缀。
-
用户端发出的请求,统一使用/user作为前缀。
三:表设计
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
employee表结构:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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 |
id是主键且自增,由数据库维护
username用户名(其实是登录时要填的账号)唯一
status账号状态,正常1锁定0,默认1
其中,employee表中的status字段已经设置了默认值1,表示状态正常。
代码开发
一:设计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;
}
细节:
- @Data注释(setter getter tostring等方法)
- 和前端界面要输入的数据保持一致(除了多了主键外)
二:代码实现
Controller层
/**
* 新增员工
* @Param employeeDTO
*/
@ApiOperation(value="新增员工")
@PostMapping
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
细节:
- json格式的输入数据要使用@RequestBody声明
- 请求方式是Post,方法上需要加上@PostMapping
- 因为类上声明了@RequestMapping("/admin/employee"),所以@PostMapping后可以不带路径
Service层
/**
* 新增员工
* @Param employeeDTO
*/
@Override
public void save(EmployeeDTO employeeDTO) {
//Service层需要把DTO对象(Controller)再转换为实体类(Mapper)
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO,employee);
//设置账号的状态,默认正常状态 1表示正常 0表示锁定
employee.setStatus(StatusConstant.ENABLE);
//设置默认密码
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id
//TODO 后续需要把其设置为当前账号
employee.setCreateUser(10L);//目前写个假数据,后期修改
employee.setUpdateUser(10L);
employeeMapper.insert(employee);//后续步骤定义
}
细节:
- Service层需要把DTO对象(Controller)再转换为实体类(Mapper)
- 一个一个赋值比较繁琐,可以使用对象属性拷贝(要求属性名一致)
- 尽量保证不要写死参数
- 设置账号的状态用常量类StatusConstant
- 默认密码用常量类PasswordConstant
- 密码需要经过MD5加密
- 调用DigestUtils.md5DigestAsHex方法
- 记得把密码先转换为Byte类型
- 待后期补全的代码可以在注释中加入“TODO”关键字
Mappper层
/**
* 新增员工
* @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);
细节:
- 插入时不能插入主键(因为其自增,由数据库维护)
功能测试
代码已经发开发完毕,对新增员工功能进行测试。
功能测试实现方式:
-
通过接口文档测试
-
通前后端联调测试
启动服务:访问http://localhost:8080/doc.html,进入新增员工接口
json数据:
{ "id": 0, "idNumber": "111222333444555666", "name": "xiaozhi", "phone": "13812344321", "sex": "1", "username": "小智" }
响应码:401 报错
报错原因:由于JWT令牌校验失败,导致EmployeeController的save方法没有被调用
解决方法:调用员工登录接口获得一个合法的JWT令牌
使用admin用户登录获取令牌
添加令牌:
将合法的JWT令牌添加到全局参数中
文档管理-->全局参数设置-->添加参数
注意参数的名称是 application.yml文件中定义的前端JWT令牌的名称
代码完善
目前,程序存在的问题主要有两个:
-
录入的用户名已存,抛出的异常后没有处理,而且只返回500,前端不知道是什么异常
-
新增员工时,创建人id和修改人id设置为固定值
接下来,我们对上述两个问题依次进行分析和解决。
问题一
描述:录入的用户名已存,抛出的异常后没有处理
分析:
新增username=zhangsan的用户,若employee表中之前已存在。
后台报错信息:Duplicate entry 'zhangsan' for key 'employee.idx_username'
发现,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'
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);
}
}
细节:
- 不写死
- 已经存在的错误提示:MessageConstant.ALREADY_EXISTS
- 未知错误的错误提示:MessageConstant.UNKNOWN_ERROR
问题二
描述:新增员工时,创建人id和修改人id设置为固定值
解决:
通过某种方式动态获取当前登录员工的id。
员工登录成功后会生成JWT令牌并响应给前端,后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id
思考:解析出登录员工id后,如何传递给Service的save方法?
通过ThreadLocal进行传递。
ThreadLocal
介绍:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。 ThreadLocal为每个线程(客户端发起的每一个请求都是一个单独的线程)提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
常用方法:
-
public void set(T value) :设置当前线程的线程局部变量的值,存值
-
public T get() 返回当前线程所对应的线程局部变量的值,取值
-
public void remove() 移除当前线程的线程局部变量,移除值
对ThreadLocal有了一定认识后,接下来继续解决问题二
初始工程中已经封装了 ThreadLocal 操作的工具类(虽然可以直接使用,但一般还是会把其封装成一个工具类):
在sky-common模块Context包的BaseContext
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,并放入线程局部变量中,之后在ServiceImpl方法中取出当前登录员工id即可
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中获取线程局部变量中的值:
//设置当前记录创建人id和修改人id
//从线程共享空间中取出当前登录账号
employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);//后续步骤定义