提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一.新增员工
1.需求分析和设计
新增员工的产品原型设计如下:
账号:每个人的账号必须唯一
员工姓名:没有要求
手机号:合法的11位手机号码
性别:男or女
身份证号:合法的18位身份证号码
密码为默认的值:123456(新增员工后员工可以登录进行修改密码)
2.接口设计:
前端提交数据采用post提交
传递给后端的参数信息由下图得知:
首先肯定传递的是json数据类型,id为非必须(数据库id设置为自增策略)
其他参数由需求分析与设计得知
后端返回给前端的数据:
code必须:返回参数0代表未成功,1代表成功
data:后端的总数据封装的data对象,包含了前端所需的数据,用于展示到页面
msg:异常或其他消息作为字符串返回给前端页面进行展示
项目约定:
客户端发出的请求:统一使用 /admin 作为前缀
用户端发出的请求:统一使用 /user 作为前缀
3.数据库设计(employee表)
一.代码开发
先了解下各种"O"的含义(DO,VO,DTO等等)https://zhuanlan.zhihu.com/p/296492029
简单来说就是:当前端提交的数据VO和实体类entity(不添加额外业务的DO)&DO中对应的属性差别比较大时,建议使用DTO来封装数据
1.在EmployeeController中创建新增员工方法,接收前端提交的参数
@PostMapping
@ApiOperation("新增员工接口")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
2.在EmployeeService接口中声明新增员工方法
/**
* 新增员工
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);
3.在EmployeeServiceImpl中实现新增员工方法
public void save(EmployeeDTO employeeDTO) {
//实现DTO到entity的转换
Employee employee=new Employee();
//属性拷贝(使用了BeanUtils工具类)
BeanUtils.copyProperties(employeeDTO,employee);
//账号状态默认为1,正常状态
employee.setStatus(StatusConstant.ENABLE);
//默认密码为:123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//创建人,创建时间,修改人,修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
employeeMapper.insert(employee);
}
4.在EmployeeMapper中声明insert方法
@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}, #{idNumber}, #{status}, #{createTime},#{updateTime},#{createUser}, #{updateUser})")
void insert(Employee employee);
四段代码总体从逻辑上来看没有什么难的,很容易理解
二.功能测试
功能测试方式:
1.通过接口文档测试
2.通过前后端联调测试
刚开始进行接口文档测试时没有成功,原因是拦截器拦截到前端页面没有携带JWT令牌,故不能进行添加员工的操作,故只需要给请求头添加一个合法的JWT令牌即可:
三.代码完善
总体上来说,到此程序有以下问题没有得到解决:
1.录入的用户名已经存在,抛出异常后没有处理
2.新增员工时,创建人的id和修改人的id设置为了固定值
1.完善代码问题1
可以通过全局异常处理器来处理:
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//用户名相同时会报错:Duplicate entry 'XXX' for key ...
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);
}
}
测试显示:
2.完善代码问题2
前端每次进行操作的时候,后端拦截器就会检测携带的JWT令牌,在项目的代码中,如果已经携带有令牌,就会解析出当前的员工id,但解析出登录员工id后,如何传递给Service的save方法?
ThreadLocal
ThreadLocal并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。也就是说,前端每次进行的操作就是一个新的线程,根据这个线程对象,我们能取到拦截器里面的员工id并储存到线程存储空间中,同线程的service方法就能从线程存储空间拿到这个值。
ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get()返回(取到) 当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量
在初始工程中已经封装了 ThreadLocal 操作的工具类,可以直接使用
//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);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
添加BaseContext.setCurrentId(empId)存储empid
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
添加BaseContext.getCurrentId()取出empid
最后,别忘了将代码推送给git仓库
二.员工分页查询
需求分析和设计
业务规则:
1.根据页码展示员工信息
2.每页展示10条数据
3.分页查询时可以根据需要,输入员工姓名进行查询
接口设计&返回数据:
一.代码开发
根据分页查询接口设计对应的DTO:
对应的类为:EmployeePageQueryDTO
后面所有的分页查询,统一都封装成PageResult对象:
员工信息分页查询后端返回的对象类型为:Result
根据接口定义创建分页查询方法:
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}",employeePageQueryDTO);
PageResult pageResult=employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
在EmployeeService接口中声明pageQuery方法:
/**
* 分页查询
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在 EmployeeServiceImpl 中实现 pageQuery 方法:
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page=employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> result = page.getResult();
return new PageResult(total,result);
}
注意: 此处使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。底层基于 mybatis 的拦截器实现。
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
在 EmployeeMapper 中声明 pageQuery 方法:
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在 EmployeeMapper.xml 中编写SQL:
<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>
总结: 在前面学习mvc的时候,其实就已经编写过很多次的查询,对于这个项目来说,没有什么特别的技术难点,但比前面系统学习过的分页查询来说,多了以下几个方面:
1.采用二次封装(封装了一次PageResult,后又封装成了Result)
2.采用了PageHelper类来快速计算出应该展示的数据,为开发节省时间
二.功能测试&代码完善
可以通过接口文档进行测试,也可以进行前后端联调测试,发现操作时间字段展示有问题,如下:
所以需要将日期格式化,使其显示清晰明了
解决方法有两种:
这里推荐方式二的原因是方式一只能在某个属性上进行日期格式化,当后面还需要格式化某个属性就还需要加注解,显得代码冗余和繁琐
方式二实现代码:
/**
* 拓展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);
}
再次进行前后端联调测试:
日期格式化成功!
最后,将员工分页查询代码提交并推送到git仓库
三.启用禁用员工账号
需求分析和设计
业务规则:
1.可以对状态为“启用” 的员工账号进行“禁用”操作
2.可以对状态为“禁用”的员工账号进行“启用”操作
3.状态为“禁用”的员工账号不能登录系统(EmployeeServiceImpl已经实现)
补充:
路径参数和地址栏传参是两种不同的方式,用于将参数传递给URL。它们的主要区别在于参数的传递方式和用途:
1.路径参数(Path Parameters):
传递方式:路径参数是通过将参数编码到URL的路径中而传递的,通常出现在URL的一部分,被包含在花括号 {} 中。例如,/example/{paramValue} 中的 {paramValue} 是一个路径参数。
用途:路径参数通常用于创建 RESTful 风格的URL,其中参数通常表示资源标识符或资源的属性。它们通常用于HTTP的GET请求,以请求特定资源或资源属性。
例如,一个使用路径参数的URL可以是:http://example.com/api/resource/123,其中 123 就是路径参数,用于请求标识为123的资源。
2.地址栏传参(Query Parameters):
传递方式:地址栏传参是通过将参数编码到URL的查询部分(通常以问号 ? 开头)而传递的。参数通常以键值对的形式出现,如 ?param1=value1¶m2=value2。
用途:地址栏传参通常用于向服务器提交请求参数,这些参数可以用于过滤、排序、分页或任何其他与请求相关的参数。它们通常用于HTTP的GET请求,但也可用于POST请求。
例如,一个使用地址栏传参的URL可以是:http://example.com/api/resource?param1=value1¶m2=value2,其中 param1 和 param2 是地址栏参数,用于请求服务器过滤或排序资源。
总之,路径参数主要用于标识资源或资源属性,而地址栏传参用于向服务器传递请求参数,用于过滤或控制请求的行为。选择使用哪种方式取决于您的应用程序需求和RESTful设计的原则。
一.代码开发&功能测试
1.根据接口设计中的请求参数形式对应的在 EmployeeController 中创建启用禁用员工账号的方法:
/**
* 启用禁用员工账号
* @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();
}
2.在 EmployeeService 接口中声明启用禁用员工账号的业务方法:
/**
* 启用禁用员工账号
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
3.在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:
/**
* 启用禁用员工账号
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Employee employee=Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
4.在 EmployeeMapper 接口中声明 update 方法:
/**
* 根据主键动态修改属性
*/
void update(Employee employee);
5.在 EmployeeMapper.xml 中编写SQL:
<update id="update" parameterType="employee">
update employee
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</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>
功能测试:可以通过接口文档进行测试,最后完成前后端联调测试即可
总结:代码没有什么难度,最后在Mapper.xml中编写的是任意参数的更新语句
四.编辑员工
一.需求分析和设计
返回数据即回显数据(根据id查询员工信息)
返回数据即更新操作返回的信息
1.回显数据
需求分析和设计在 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);
}
在 EmployeeService 接口中声明 getById 方法:
/**
* 根据id查询员工
* @param id
* @return
*/
Employee getById(Long id);
在 EmployeeServiceImpl 中实现 getById 方法:
/**
* 根据id查询员工信息
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee=employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
在 EmployeeMapper 接口中声明 getById 方法:
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
2.修改员工数据
在 EmployeeController 中创建 update 方法:
/**
* 编辑员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息:{}",employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}
在 EmployeeService 接口中声明 update 方法:
/**
* 编辑员工信息
* @param employeeDTO
*/
void update(EmployeeDTO employeeDTO);
在 EmployeeServiceImpl 中实现 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);
}
二.功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可
回忆:
1.resultType 属性用于指定一个 SQL 查询语句的执行结果应该映射为的 Java 对象类型。
2.parameterType 是用于指定 SQL 语句(例如insert, update, delete, select) 中的参数类型的属性。它定义了传递给 SQL 语句的参数类型,以便 MyBatis 在执行 SQL 语句时正确映射参数。
(1).指定参数类型:它指定了传递给 SQL 语句的参数的 Java 类型。这可以是一个完全限定的 Java 类型,例如 com.example.model.User,也可以是一个简单的 Java 类型,例如 int、String 等。
(2).传递参数:在 SQL 语句中,你可以使用 #{parameterName} 的语法引用参数,其中 parameterName 是你在 parameterType 中指定的参数类型中的属性。例如,如果 parameterType 是 com.example.model.User,你可以在 SQL 语句中使用 #{id}、#{name} 等引用 User 类的属性。
3.配置 type-aliases-package: com.sky.entity 表示想要使用 MyBatis 的自动扫描功能,将指定包中的所有类作为类型别名注册,这样在映射文件中就可以使用简化的别名引用这些类。
如果 com.sky.entity 包下包含了所有数据库对应的实体类,这个配置将会非常方便,因为你不需要为每个实体类手动定义别名。MyBatis 将自动为这个包下的每个类创建一个别名,通常是类名的首字母小写形式。
例如,如果有一个名为 com.sky.entity.User 的实体类,MyBatis 会自动将其别名设置为 user,这意味着可以在映射文件中使用 resultType=“user” 来引用 User 类,而不必使用完整的类名。
这种配置方式可以大大简化 MyBatis 映射文件的编写,减少了需要手动定义别名的工作,提高了可维护性。
五.导入分类模块功能代码
一.需求分析和设计
业务规则:
1.分类名称必须是唯一的
2.分类按照类型可以分为菜品分类和套餐分类
3.新添加的分类状态默认为“禁用”
二.代码导入
导入代码即可
三.功能测试
直接进行前后端联调测试即可
四.Q&A:
Q1:分类管理的mapper有三个:CatgoryMapper,DishMapper,SetmealMapper,为什么要引入后面两个mapper?
A:为了解答这个问题,可以用下面的关系图来解释:
在这张图里面,我们可以看到,其实分类管理关联了两张表,一张是菜品表,还有一张是套餐表,也就是说在进行分类管理删除某个分类,例如图上的酒水饮料,后端代码必须检查是否有关联了菜品(王老吉,北冰洋,雪花啤酒),如果关联了,那么就不能进行删除这项分类,其一是因为在后续的添加菜品的功能时,要选择加入的分类,其二是如果删除了分类,那么菜品无法进行管理(特别是像数据库里面的数据变得十分庞大的时候,误删除了一个分类后续操作想要菜品添加回分类后显得十分困难),所以说必须关联两张表判断分类是否为空,为空则可以进行删除.
Q2:我并没有在CategoryController看到EmployeeController类下面类似的按照id查询的方法,那进行修改分类的时候,数据是怎么样回显到修改页面上的呢?
A:虽然没有的回显操作,但回显通常在前端完成,而不是在后端控制器中进行。后端主要负责接收请求,处理请求,然后返回响应数据。回显通常涉及前端代码,例如前端页面的JavaScript代码或前端框架(如React、Angular、Vue.js)来显示已保存的数据。