本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集
1. 分类管理业务开发
1.1 公共字段自动填充
1.1.1 问题分析
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:


能不能对于这些公共字段在某个地方统一处理,来简化开发呢?答案就是使用Mybatis Plus提供的公共字段自动填充功能。
1.1.2 代码实现
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候,为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
具体实现:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略
1 2 3 4 5 6 7 8 9 10 11
| @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、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值。
在common包下创建MyMetaObjectHandler类,并实现MetaObjectHandler接口。如下:
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
|
@Slf4j @Component 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)); } }
|
注意:当前我们设置createUser和updateUser为固定值,后面我们需要进行改造,改为动态获得当前登录用户的id。
1.1.3 功能完善
前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时,设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。
有的同学可能想到,用户登录成功后,我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以不能从session中获取当前登录用户的id(”employee”)。所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理。在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码(获取当前线程id):
1 2
| long id = Thread.currentThread().getId() ; log.info("线程id:{}" ,id);
|
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:

什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
- public void set(T value) 设置当前线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
实现步骤:
1、编写BaseContext工具类,基于ThreadLocal封装的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){ threadLocal.set(id); }
public static Long getCurrentId(){ return threadLocal.get(); } }
|
2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
1 2 3 4 5 6 7 8 9 10 11
| HttpSession session = request.getSession(); if(session.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; }
|
3、在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
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
|
@Slf4j @Component 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", BaseContext.getCurrentId()); metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充【update】..."); log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("updateUser", BaseContext.getCurrentId()); } }
|
说明:
每次请求时,会到LoginCheckFilter检查是否放行,其中要检查是否登录。若登录了,则通过调用BaseContext.get(id)
,拿到用户id,放进threadLocal(在同一线程这一作用域内有效)。
接下来到元数据对象处理器MyMetaObjectHandler中,统一为公共字段赋值——它实现了MetaObjectHandler,当sql执行insert或update时,会执行MetaObjectHandler的insertFill()和updateFill()方法。
1.2 新增分类
1.2.1 需求分析
后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。
当我们在后台系统中添加菜品时,需要选择一个菜品分类;当我们在后台系统中添加一个套餐时,需要选择一个套餐分类。
在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

1.2.2 数据模型
新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:

1.2.3 代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好。
实体类Category(直接从课程资料中导入)
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
| @Data public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
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;
private Integer isDeleted;
}
|
Mapper接口CategoryMapper
1 2 3
| @Mapper public interface CategoryMapper extends BaseMapper<Category> { }
|
Service层接口CategoryService
1 2
| public interface CategoryService extends IService<Category> { }
|
Service层实现类CategoryServiceImpl
1 2 3
| @Service public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService { }
|
控制层CategoryController
1 2 3 4 5 6 7
| @Slf4j @RestController @RequestMapping("/category") public class CategoryController { @Autowired private CategoryService categoryService; }
|
执行过程
在开发代码之前,需要梳理一下新增分类功能的执行过程:
1、在”分类管理”页面(backend/page/category/list.html)点击”新增xx分类”,填写名称、排序,点”确定”后,在浏览器中可以看到页面发送了ajax请求,将新增分类窗口输入的信息以json形式提交到服务端。

其中data是json形式,例如新增套餐分类时type为2,data为{“name”:”精品套餐”,”type”:”2”,”sort”:”12”}。url为http://localhost:8080/category ,method为POST。
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
可以看到,新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可。

程序开发
在CategoryController中,写一个方法处理该POST请求:
1 2 3 4 5 6 7
| @PostMapping public R<String> save(@RequestBody Category category){ log.info("category:{}",category); categoryService.save(category); return R.success("新增分类成功"); }
|
说明:从浏览器调试工具中可以看到,页面发送ajax请求,携带数据(name, sort, type),所以此处用@RequestBody获取参数。
1.3 分类信息分页查询
1.3.1 需求分析
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
1.3.2 代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
这一步的具体过程:
请求/category/list.html页面时,会自动执行VUE的钩子函数created()
,并执行init()
方法。其中执行getCategoryPage()
方法,传入参数page和pageSize。该方法会发送ajax请求到/category/page(method=get),将params参数(page和pageSize)以“?xxx=xxx&xxx=xxx”的形式,拼接到请求地址url后,提交到服务端。

2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上

1 2 3 4 5 6 7 8 9 10 11 12 13
| @GetMapping("/page") public R<Page> page(int page, int pageSize) { Page<Category> pageInfo = new Page<>(page,pageSize); LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByAsc(Category::getSort); categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo); }
|
注意:
1、这里要把实体类Category中的private Integer isDeleted;
一句注释掉,才能查询到数据。
2、请求中带着参数(/category/page?page=1&pageSize=10)发到服务端,controller方法声明和请求参数同名的形参,就可以接收请求参数。
1.4 删除分类
1.4.1 需求分析
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是,当分类关联了菜品或者套餐时,此分类不允许被删除。
1.4.2 代码开发

执行过程:
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端

这一步的具体过程:
在list.html页面点”删除”,执行deleteHandle(id)。其中给出”确认删除”的提示后,如果点击”确定”,就执行deleCategory(id),传入id,发送ajax请求到/category(delete方式),params以name=value&name=value的方式发送请求参数,请求参数都会被拼接到请求地址url后。请求为/category?ids=1627842099162492930,method=DELETE。
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
程序开发:
1 2 3 4 5 6 7 8
| @DeleteMapping public R<String> delete(Long ids){ log.info("删除分类,id为{}",ids); categoryService.removeById(ids); return R.success("分类信息删除成功"); }
|
1.4.3 代码完善
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal (从课程资料中复制)
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
| @Data public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Long categoryId;
private BigDecimal price;
private String code;
private String image;
private String description;
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;
private Integer isDeleted; }
|
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
| @Data public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id
private Long categoryId;
private String name;
private BigDecimal price;
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;
private Integer isDeleted; }
|
2、Mapper接口DishMapper和SetmealMapper
1 2 3
| @Mapper public interface DishMapper extends BaseMapper<Dish> { }
|
1 2 3
| @Mapper public interface SetmealMapper extends BaseMapper<Setmeal> { }
|
3、Service接口DishService和SetmealService
1 2
| public interface DishService extends IService<Dish> { }
|
1 2
| public interface SetmealService extends IService<Setmeal> { }
|
4、Service实现类DishServicelmpl和SetmealServicelmpl
1 2 3
| @Service public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService { }
|
1 2 3
| @Service public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService{ }
|
1.4.4 关键代码
1、在CategoryService中添加remove方法
1 2 3 4 5 6 7
|
public void remove(Long id);
|
2、在CategoryServicelmpl中实现remove方法
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
| @Service public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired private DishService dishService;
@Autowired private SetmealService setmealService;
@Override public void remove(Long id) { LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>(); dishLambdaQueryWrapper.eq(Dish::getCategoryId, id); int count1 = dishService.count(dishLambdaQueryWrapper);
if (count1 > 0){ throw new CustomException("当前分类已关联菜品,不能删除"); }
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>(); setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id); int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0){ throw new CustomException("当前分类已关联套餐,不能删除"); }
super.removeById(id); } }
|
3、定义异常类CustomException
在common包新建CustomException类:
1 2 3 4 5 6 7 8
|
public class CustomException extends RuntimeException{ public CustomException(String message) { super(message); } }
|
4、在全局异常处理器GlobalExceptionHandler添加异常处理方法,捕获自定义的CustomException异常
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@ExceptionHandler(CustomException.class) public R<String> exceptionHandler(CustomException ex){ log.error(ex.getMessage()); return R.error(ex.getMessage()); }
|
1.5 修改分类
需求分析
在分类管理列表页面点击“修改”按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击“确定”按钮完成修改操作。
代码实现
1 2 3 4 5 6 7 8
|
@PutMapping public R<String> update(@RequestBody Category category){ categoryService.updateById(category); return R.success("分类信息修改成功"); }
|
说明:
形参:前端发来json形式的请求参数({“id”:”1397844263642378242”,”name”:”湘菜2”,”sort”:1}),可以用@RequestBody注解,接收请求体中的参数。