- 视频链接:黑马程序员【Java 项目实战《瑞吉外卖》,轻松掌握 SpringBoot + MybatisPlus 开发核心技术】
- 资料链接:2022 最新版 Java学习 路线图>第 5 阶段一 企业级项目实战>7.黑马程序员 瑞吉外卖平台实战开发(提取码:dor4)
上一篇:学习【瑞吉外卖②】SpringBoot单体项目:https://blog.csdn.net/yanzhaohanwei/article/details/124993760
前面我们已经完成了后台系统的员工管理功能开发。
在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。
这些字段属于公共字段,也就是很多表中都有这些字段。
具体如下:
列名 | 数据类型 |
---|---|
create_time | datetime |
update_time | datetime |
create_user | bigint |
update_user | bigint |
com/itheima/reggie/controller/EmployeeController.java
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
log.info("新增员工,员工信息:{}", employee.toString());
//设置初始密码 123456,需要进行 md5 加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
/* * * * * * * * * * * * * * * * * * * * * * */
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
/* * * * * * * * * * * * * * * * * * * * * * */
//获得当前登录用户的 id
Long empId = (Long) request.getSession().getAttribute("employee");
/* * * * * * * * * * * * * * * * * * * * * */
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
/* * * * * * * * * * * * * * * * * * * * * */
employeeService.save(employee);
return R.success("新增员工成功");
}
那么能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
Mybatis-Plus 提供的 公共字段 自动填充功能 就可以办到这一点。
MyBatis-Plus 公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值。
使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
1.在实体类的属性上加入 @TableField 注解,指定自动填充的策略
com/itheima/reggie/entity/Employee.java
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) //插入时填充字段
private Long updateUser;
2.按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现 MetaObjectHandle 接口
com/itheima/reggie/common/MyMetaObjecthandler.java
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
//插入时自动填充
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}
//更新时自动填充
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
}
同时我们需要注释掉一些代码
com/itheima/reggie/controller/EmployeeController.java
/**
* 新增员工
*
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
... ...
//employee.setCreateTime(LocalDateTime.now());
//employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的 id
//Long empId = (Long) request.getSession().getAttribute("employee");
//employee.setCreateUser(empId);
//employee.setUpdateUser(empId);
... ...
}
前面我们已经完成了公共字段自动填充功能的代码开发。
但是我们在自动填充 createUser 和 updateUser 时设置的用户 id 是固定值,现在我们需要改造成动态获取当前登录用户的 id。
在之前的操作中,用户登录成功后,我们将用户 id 存入了 HttpSession 中。
那么我们可以从 HttpSession 中获取吗?答案是不可以的。
这里我们可以使用 ThreadLocal 来解决此问题,它是 JDK 中提供的一个类。
在学习 ThreadLocal 之前,我们需先确认一事:即客户端发送的每次 http 请求,对应的在服务端都会分配一个新的线程来处理。
在处理过程中涉及到下面类中的方法都属于相同的一个线程
可以在上面三个方法中分别加入下面代码(获取当前线程 id)
long id = Thread.currentThread().getId();
log.info("线程 id 是:{}", id);
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的 id 是相同的。
2022-06-03 13:33:05.948 INFO 19284 --- [nio-8080-exec-6] c.i.raggie.filter.LoginCheckFilter : 拦截到请求:/employee
2022-06-03 13:33:05.948 INFO 19284 --- [nio-8080-exec-6] c.i.raggie.filter.LoginCheckFilter : 用户已登录,用户 id 为:1
2022-06-03 13:33:05.948 INFO 19284 --- [nio-8080-exec-6] c.i.raggie.filter.LoginCheckFilter : 线程 id:35
2022-06-03 13:40:28.951 INFO 19284 --- [nio-8080-exec-6] c.i.r.controller.EmployeeController : 线程 id:35
... ...
2022-06-03 13:33:05.953 INFO 19284 --- [nio-8080-exec-6] c.i.raggie.common.MyMetaObjecthandler : 线程id为:35
ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。
当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
public void set(T value)
设置当前线程局部变量的值public T get()
返回当前线程所对应的线程局部变量的值我们可以在 LoginCheckFilter 的 doFilter 方法中获取当前登录用户 id,并调用 ThreadLocal 的 set 方法来设置当前线程的线程局部变量的值(用户 id),然后在 MyMetaObjectHandler 的 updateFill 方法中调用 ThreadLocal 的 get 方法来获得当前线程所对应的线程局部变量的值(用户 id)。
com/itheima/reggie/common/BaseContext.java
package com.itheima.reggie.common;
/**
* 基于 ThreadLocal 封装工具类,用户保存和获取当前登录用户 id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
*
* @param id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取值
*
* @return
*/
public static Long getCurrentId() {
return threadLocal.get();
}
}
com/itheima/reggie/filter/LoginCheckFilter.java
//4.判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
com/itheima/reggie/common/MyMetaObjecthandler.java
package com.itheima.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
*
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作,自动填充
*
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("线程id为:{}", id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。
当我们在后台系统中添加菜品时需要选择一个菜品分类。
当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
我们也可以再后台系统的分类管理页面分别添加菜品分类和套餐分类。
新增分类,实际上就是将我们新增窗口录入的分类数据插入到 category 表中。
需要注意的地方是:category 表中对 name 字段加入了唯一约束,保证了分类的名称是唯一的。
backend/page/category/list.html
)发送 ajax 请求,将新增分类窗口输入的数据以 json 形式提交到服务端。可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的 json 数据结构相同,所以服务端只需要提供一个方法统一处理即可。
com/itheima/reggie/entity/Category.java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
private Integer sort;
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
com/itheima/reggie/mapper/CategoryMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.raggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryMapper extends BaseMapper<Category> { }
com/itheima/reggie/service/CategoryService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.raggie.entity.Category;
public interface CategoryService extends IService<Category> { }
com/itheima/reggie/service/impl/CategoryServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.mapper.CategoryMapper;
import com.itheima.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService { }
com/itheima/reggie/controller/CategoryController.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
}
com/itheima/reggie/controller/CategoryController.java
/**
* 新增分类
*
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category) {
log.info("category:{}", category);
categoryService.save(category);
return R.success("新增分类成功");
}
当系统中的分类很多时,若在一个页面中全部展示出来,就会显得比较乱,不便于查看。
所以一般的系统中都会以分页的方式来展示列表数据。
在开发代码之前,需要梳理一下整个程序的执行过程
com/itheima/reggie/controller/CategoryController.java
/**
* 分页查询
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize) {
//分页构造器
Page<Category> pageInfo = new Page<>(page, pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
在分类管理列表页面,可以对某个分类进行删除操作。
需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
在开发代码之前,需要梳理一下整个程序的执行过程。
/**
* 根据 id 删除分类
*
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id) {
log.info("删除分类,id为:{}", id);
categoryService.removeById(id);
return R.success("分类信息删除成功");
}
前面我们已经实现了根据 id 删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐。
所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
前面我们在 Contorller 层中使用了由 MyBatisPlus 中提供了 removeById() 来实现删除方法。
但是现在我们需要在进行删除操作前,判断删除的分类是否关联了菜品或者套餐,故要重写该方法。
com/itheima/reggie/entity/Dish.java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 菜品
*/
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//商品码
private String code;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//顺序
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
com/itheima/reggie/entity/Setmeal.java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//分类 id
private Long categoryId;
//套餐名称
private String name;
//套餐价格
private BigDecimal price;
//状态 0:停用 1:启用
private Integer status;
//编码
private String code;
//描述信息
private String description;
//图片
private String image;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
com/itheima/reggie/mapper/DishMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DishMapper extends BaseMapper<Dish> { }
com/itheima/reggie/mapper/SetmealMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> { }
com/itheima/reggie/service/DishService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Dish;
public interface DishService extends IService<Dish> { }
com/itheima/reggie/service/SetmealService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Setmeal;
public interface SetmealService extends IService<Setmeal> { }
com/itheima/reggie/service/impl/DishServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.mapper.DishMapper;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService { }
com/itheima/reggie/service/impl/SetmealServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.SetmealMapper;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService { }
前面我们使用了由 MyBatisPlus 中提供了 removeById() 方法。
但是现在我们需要在进行删除操作前,判断检查删除的分类是否关联了菜品或者套餐,故要重写该方法。
com/itheima/reggie/service/CategoryService.java
public void remove(Long id);
之后我们需要在 service 的实现类中实现 remove() 方法的具体逻辑代码。
该方法是根据 id 删除分类的。
在此编写逻辑代码之前,我们需要先编写异常处理的代码来处理分类关联了菜品(或套餐)时的情况。
com/itheima/reggie/common/CustomException.java
package com.itheima.reggie.common;
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
com/itheima/reggie/common/GlobalExceptionHandler.java
/**
* 异常处理方法
*
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
com/itheima/reggie/service/impl/CategoryServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.common.CustomException;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.CategoryMapper;
import com.itheima.reggie.service.CategoryService;
import com.itheima.reggie.service.DishService;
import com.itheima.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据 id 删除分类,删除之前需要进行判断
*
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类 id 进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if (count1 > 0) {
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类 id 进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0) {
//已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
com/itheima/reggie/controller/CategoryController.java
/**
* 根据 id 删除分类
*
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id) {
log.info("删除分类,id为:{}", id);
//categoryService.removeById(id);
categoryService.remove(id);
return R.success("分类信息删除成功");
}
实际上就是注释掉 之前写的categoryService.removeById(id);
,再调用 categoryService.remove(id);
。
这里补充一下,官方提供的资料是有问题的。
或许就是官方故意的,等着各位发现问题,并解决问题。
resources
目录下的 backend/page/category/list.html
中的 deleteHandle 传递的是 id。
//删除
deleteHandle(id) {
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
'confirmButtonText': '确定',
'cancelButtonText': '取消',
'type': 'warning'
}).then(() => {
deleCategory(id).then(res => {
if (res.code === 1) {
this.$message.success('删除成功!')
this.handleQuery()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
})
},
为保持一致,resources
目录下的 backend/api/category.js
中的 ids 也应该改为 id。
// 删除当前列的接口
const deleCategory = (id) => {
return $axios({
url: '/category',
method: 'delete',
params: { id }
})
}
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
com/itheima/reggie/controller/CategoryController.java
/**
* 根据 id 修改分类信息
*
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category) {
log.info("修改分类信息:{}", category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
下一篇:学习【瑞吉外卖④】SpringBoot单体项目_菜品管理业务开发:https://blog.csdn.net/yanzhaohanwei/article/details/125138771