文章目录
10 公共字段填充功能+ThreadLocal模块改进
1 公共字段自动填充
1.1 问题分析
在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在系统中很多表中都会有这些字段,如下:
而针对于这些字段的赋值方式为:
A. 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
B. 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
目前,在项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下
如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐。
改进:使用Mybatis Plus提供的公共字段自动填充功能来简化开发。
1.2 基本功能实现
1.2.1 思路分析
Mybatis Plus公共字段自动填充,即在插入或者更新的时候为指定字段赋予指定的值,好处统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,有四个公共字段需要在新增/更新中进行赋值操作, 具体情况如下:
字段名 | 赋值时机 | 说明 |
---|---|---|
createTime | 插入(INSERT) | 当前时间 |
updateTime | 插入(INSERT) , 更新(UPDATE) | 当前时间 |
createUser | 插入(INSERT) | 当前登录用户ID |
updateUser | 插入(INSERT) , 更新(UPDATE) |
实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略。
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
1.3 代码实现
1). 实体类的属性上加入@TableField注解,指定自动填充的策略。
在员工Employee实体类的公共字段属性上, 加上注解, 指定填充策略。
package com.fc.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;
/*
*员工实体类 该实体类主要用于和员工表 employee 进行映射。
*/
// 在实体类上添加@Data注解,可以省去代码中大量的 get()、 set()、 toString() 等方法,提高代码的简洁:
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
// map-underscore-to-camel-case: true
// 在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
private String idNumber; //身份证 因为在配置文件中设置驼峰命名,所以与数据库中的不太一样,数据库中为id_number
private Integer status;
@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). 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
package com.fc.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;
/**
* 自定义元数据对象处理器|统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
/**@Description: 插入操作自动填充
* @version v1.0
* @author LiBiGo
* @date 2022/8/15 12:14
*/
log.info("公共字段自动填充insert...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId()); // 使用了Thread 看后续
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
/**@Description: 更新操作自动填充
* @version v1.0
* @author LiBiGo
* @date 2022/8/15 12:14
*/
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());
}
}
2 使用ThreadLocal对公共字段填充功能进行完善
2.1 思路分析
2.1.1 提出设想
前面已经完成公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是在自动填充createUser和updateUser时设置的用户id是固定值,现在需要改造成动态获取当前登录用户的id。
提出设想:用户登录成功后将用户id存入了HttpSession中,现在从HttpSession中获取不就行了?
存在问题:在MyMetaObjectHandler类中是不能直接获得HttpSession对象的,所以需要通过其他方式来获取登录用户id。
2.1.2 分析问题
在修改员工信息时, 业务的执行流程如下图:
客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1). LoginCheckFilter的doFilter方法
2). EmployeeController的update方法
3). MyMetaObjectHandler的updateFill方法
我们可以在上述类的方法中加入如下代码(获取当前线程ID,并输出):
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
经过上述的分析之后,发现可以使用JDK提供的一个ThreadLocal类来解决此问题。
2.2 ThreadLocal
2.2.1 ThreadLocal基本概述
ThreadLocal并不是一个Thread,而是Thread的局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。
2.2.2 ThreadLocal常用方法
A. public void set(T value) : 设置当前线程的线程局部变量的值
B. public T get() : 返回当前线程所对应的线程局部变量的值
C. public void remove() : 删除当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。 如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。
2.3 操作步骤
1). 编写BaseContext工具类,基于ThreadLocal封装的工具类
2). 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3). 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
2.4 代码实现
1). BaseContext工具类
package com.fc.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();
}
}
2).LoginCheckFilter中存放当前登录用户到ThreadLocal
在doFilter方法中, 判定用户是否登录, 如果用户登录, 在放行之前, 获取HttpSession中的登录用户信息, 调用BaseContext的setCurrentId方法将当前登录用户ID存入ThreadLocal。
package com.fc.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.fc.reggie.common.BaseContext;
import com.fc.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*检查用户是否已经完成登陆
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*") //设置拦截器,设置拦截的网页区域
@Slf4j
public class LoginCheckFilter implements Filter {
// 路径匹配器,支持通配符,因为下面的序列使用了通配符
// AntPathMatcher匹配规则 ? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录/字符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
// A. 获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截的请求:{}",requestURI);
// 定义不拦截的序列,只拦截页面数据请求,不拦截页面格式
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// B. 判断本次请求, 是否需要登录, 才可以访问
boolean check = check(urls, requestURI);
// C. 如果不需要,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
// D. 判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee")!=null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee"); // 获取当前用户登陆id
BaseContext.setCurrentId(empId); //设置线程共享
filterChain.doFilter(request,response);
long id = Thread.currentThread().getId();
log.info("检测是否登陆线程ID:{}",id);
return;
}
// E. 如果未登录, 则返回未登录结果
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
public boolean check(String[] urls,String requestURI){
/*
* 路径匹配,检查本次请求是否需要放行
*/
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI); // 使用通配符对象,配对资源
if(match){
return true;
}
}
return false;
}
}
3). MyMetaObjectHandler中从ThreadLocal中获取
将之前在代码中添加动态调用BaseContext中的getCurrentId方法获取当前登录用户ID
3 功能测试
编写完了元数据对象处理器之后,我们就可以将之前在新增和修改方法中手动赋值的代码删除或注释掉。
完善了元数据对象处理器重新启动项目,完成登录操作后,在员工管理模块中,测试增加/更新员工信息功能,直接查询数据库数据变更,看看我们在新增/修改数据时,这些公共字段数据是否能够完成自动填充, 并且看看填充的create_user 及 update_user字段值是不是本地登录用户的ID。
11 菜品新增类别+类别信息分页查询
1 新增分类
1.1 需求分析
后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。当我们在后台系统中添加菜品时需要选择一个菜品分类,在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
在分类管理中,新增分类时可以选择新增菜品分类(川菜、湘菜、粤菜…), 也可以选择新增套餐分类(营养早餐、超值午餐…)。 在添加套餐的时候, 输入的排序字段, 控制的是移动端套餐列表的展示顺序。
1.2 数据模型
新增分类,将新增窗口录入的分类数据,插入到category表,具体表结构如下:
套餐名称是唯一的,不能够重复的,所以在设计表结构时,已经针对于name字段建立了唯一索引,如下:
1.3 前端页面分析
整个程序的执行过程:
1). 在页面(backend/page/category/list.html)的新增分类表单中填写数据,点击 “确定” 发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service将数据进行保存
3). Service调用Mapper操作数据库,保存数据
1.3.1 json数据结构分析
新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可:
具体请求信息整理如下:
请求 | 说明 |
---|---|
请求方式 | POST |
请求路径 | /category |
请求参数 | json格式-{“name”:“川菜”,“type”:“1”,“sort”:2} |
1.4 代码实现
代码实现的具体步骤如下:
- 实体类Category(直接从课程资料中导入即可)
- Mapper接口CategoryMapper
- 业务层接口CategoryService
- 业务层实现类CategoryServiceImpl
- 控制层CategoryController
1). 实体类Category
package com.fc.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
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;
}
2). Mapper接口CategoryMapper
package com.fc.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
3). 业务层接口CategoryService
package com.fc.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fc.reggie.entity.Category;
import com.fc.reggie.mapper.CategoryMapper;
import com.fc.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
/**
* 类别业务层接口
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
4). 业务层实现类CategoryServiceImpl
package com.fc.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fc.reggie.entity.Category;
import com.fc.reggie.mapper.CategoryMapper;
import com.fc.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
5). 控制层CategoryController
package com.fc.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fc.reggie.common.R;
import com.fc.reggie.entity.Category;
import com.fc.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/*
*新增分类
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
/*
* 分页查询
*/
// 分页构造
Page<Category> pageinfo = new Page<>(page,pageSize);
// 构造条件构造器对象
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
// 添加排序条件,根据sore进行排序
queryWrapper.orderByAsc(Category::getSort);
// 进行分页查询
categoryService.page(pageinfo,queryWrapper);
return R.success(pageinfo);
}
}
1.5 功能测试
新启动项目,进入管理系统访问分类管理, 然后进行新增分类测试,需要将所有情况都覆盖全,例如:
1). 输入的分类名称不存在
2). 输入已存在的分类名称
3). 新增菜品分类
4). 新增套餐分类
2 分类信息分页查询
2.1 需求分析
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
2.2 前端页面分析
在开发代码之前,需要梳理一下整个程序的执行过程:
1). 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据响应给页面
5). 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
页面加载时,就会触发Vue声明周期的钩子方法,然后执行分页查询,发送异步请求到服务端,前端代码如下:
页面中使用的是ElementUI提供的分页组件进行分页条的展示:
我们通过浏览器,也可以抓取到分页查询的请求信息, 如下:
具体的请求信息整理如下:
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /category/page |
请求参数 | ?page=1&pageSize=10 |
2.3 代码实现
在CategoryController中增加分页查询的方法,在方法中传递分页条件进行查询,并且需要对查询到的结果,安排设置的套餐顺序字段sort进行排序。
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
/*
*分页查询
*/
// 分页构造
Page<Category> pageinfo = new Page<>(page,pageSize);
// 构造条件构造器对象
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
// 添加排序条件,根据sore进行排序
queryWrapper.orderByAsc(Category::getSort);
// 进行分页查询
categoryService.page(pageinfo,queryWrapper);
return R.success(pageinfo);
}
2.4 功能测试
分页查询的代码编写完毕之后, 我们需要重新启动项目,然后登陆系统后台,点击分类管理,查询分类列表是否可以正常展示。测试过程中可以使用浏览器的监控工具查看页面和服务端的数据交互细节。
12 删除菜品套餐类别+修改套餐类别信息
1 删除分类
1.1 需求分析
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
1.2 前端页面分析
在前端页面中,点击 “删除” 按钮,就会触发定义的方法,然后往服务端发送异步请求,并传递参数id,执行删除分类操作。
删除操作的具体执行流程如下:
1). 点击删除,页面发送ajax请求,将参数(id)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service删除数据
3). Service调用Mapper操作数据库
从上述的分析中,可以得到请求的信息如下:
请求 | 说明 |
---|---|
请求方式 | DELETE |
请求路径 | /category |
请求参数 | ?id=1395291114922618881 |
1.3 改进思路分析
删除分类数据需要检查删除的分类是否关联了菜品或者套餐,所以需要进行功能完善。完善后的逻辑为:
-
根据当前分类的ID,查询该分类下是否存在菜品,如果存在,则提示错误信息
-
根据当前分类的ID,查询该分类下是否存在套餐,如果存在,则提示错误信息
-
执行正常的删除分类操作
那么在这里又涉及到两张表结构 dish(菜品表) 和 setmeal(套餐表)。具体的表结构如下:
1.4 准备工作
1.4.1 Dish菜品实体类
package com.fc.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
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;
}
1.4.2 Setmeal套餐实体类
package com.fc.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
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;
}
1.4.3. Mapper接口DishMapper和SetmealMapper
DishMapper
package com.fc.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fc.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
SetmealMapper
package com.fc.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fc.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}
1.4.3 Service接口DishService和SetmealService
DishService
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fc.reggie.entity.Dish;
public interface DishService extends IService<Dish> {
}
SetmealService
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fc.reggie.entity.Setmeal;
public interface SetmealService extends IService<Setmeal> {
}
1.5 代码实现
1). 创建自定义异常
在业务逻辑操作过程中,如果遇到一些业务参数、操作异常的情况下,直接抛出此异常。
package com.fc.reggie.common;
import javax.xml.crypto.dsig.spec.XSLTTransformParameterSpec;
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
2). 在CategoryService中扩展remove方法
package com.fc.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fc.reggie.entity.Category;
public interface CategoryService extends IService<Category> {
public void remove(Long id);
}
3). 在CategoryServiceImpl中实现remove方法
package com.fc.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fc.reggie.common.CustomException;
import com.fc.reggie.entity.Category;
import com.fc.reggie.entity.Dish;
import com.fc.reggie.mapper.CategoryMapper;
import com.fc.reggie.service.CategoryService;
import com.fc.reggie.service.DishService;
import com.fc.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;
@Override
/*
*根据id删除分类,删除之前需要判定是否关联菜品
*/
public void remove(Long id) {
// A.查询当前分类是否关联菜品,关联则抛出业务异常类
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据id查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
if (count1 > 0){
// 已关联菜品,抛出异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
// B.查询当前分类是否关联套餐,关联则抛出业务异常类
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); // 父类实现了最基本的根据id查询的删除,所以直接调用就行
}
}
那么在上述的业务逻辑中,当分类下关联的有菜品或者套餐时,在业务代码中抛出了自定义异常,会被异常处理器捕获,只需要在异常处理器中捕获这一类的异常,然后给页面返回对应的提示信息即可。
4). 在GlobalExceptionHandler中处理自定义异常
在全局异常处理器中增加方法,用于捕获自定义的异常 CustomException
package com.fc.reggie.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理器
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})// 处理@RestController、@Controller的函数异常 //指定拦截那些类型的控制器;
@ResponseBody //将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
@Slf4j
public class GlobalExceptionHandler {
/*
*异常处理方法
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) // 处理指定异常类
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" "); //提取重复字段,即哪个用户名重复 从0开始第2个即为用户名
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
@ExceptionHandler(CustomException.class) // 处理自定义业务异常类
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
5). 改造CategoryController的delete方法
注释掉原有的代码,在delete方法中直接调用categoryService中自定义的remove方法。
package com.fc.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fc.reggie.common.R;
import com.fc.reggie.entity.Category;
import com.fc.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/*
* 新增分类
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
/*
*分页查询
*/
// 分页构造
Page<Category> pageinfo = new Page<>(page,pageSize);
// 构造条件构造器对象
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
// 添加排序条件,根据sore进行排序
queryWrapper.orderByAsc(Category::getSort);
// 进行分页查询
categoryService.page(pageinfo,queryWrapper);
return R.success(pageinfo);
}
@DeleteMapping
public R<String> delete(Long id){
/*
*根据id删除分类
*/
log.info("删除分类,id为{}",id);
categoryService.remove(id);
return R.success("分类信息删除成功");
}
@PutMapping
public R<String> update(@RequestBody Category category){
/*
*根据id修改分类信息
*/
log.info("根据id修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类成功");
}
}
2 修改分类
2.1 需求分析
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
2.2 前端页面分析
这里面大家会发现,修改功能我们还没有实现,但是当点击 “修改” 按钮的时候,我们并没有开发根据ID查询数据,进行页面回显的功能,但是页面的分类数据确实回显回来了。这是怎么做到的呢,我们来解析一下前端的代码实现(前端代码已经实现):
那么回显这一步的操作前端已经实现,我们就只需要开发一个方法,修改操作的方法即可。我们可以通过浏览器来抓取一下修改操作的请求信息,如图:
具体的请求信息,整理如下:
请求 | 说明 |
---|---|
请求方式 | PUT |
请求路径 | /category |
请求参数 | {id: “1399923597874081794”, name: “超值午餐”, sort: 0} |
2.3 代码实现
d为{}",id);
categoryService.remove(id);
return R.success(“分类信息删除成功”);
}
@PutMapping
public R<String> update(@RequestBody Category category){
/*
*根据id修改分类信息
*/
log.info("根据id修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类成功");
}
}
## 2 修改分类
### 2.1 需求分析
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
[外链图片转存中...(img-Bew10Sr7-1664776995727)]
### 2.2 前端页面分析
这里面大家会发现,修改功能我们还没有实现,但是当点击 "修改" 按钮的时候,我们并没有开发根据ID查询数据,进行页面回显的功能,但是页面的分类数据确实回显回来了。这是怎么做到的呢,我们来解析一下前端的代码实现(前端代码已经实现):
[外链图片转存中...(img-HXGo59mS-1664776995728)]
那么回显这一步的操作前端已经实现,我们就只需要开发一个方法,修改操作的方法即可。我们可以通过浏览器来抓取一下修改操作的请求信息,如图:
[外链图片转存中...(img-VqWCAiLK-1664776995728)]
具体的请求信息,整理如下:
| 请求 | 说明 |
| :------- | :----------------------------------------------------- |
| 请求方式 | PUT |
| 请求路径 | /category |
| 请求参数 | {id: "1399923597874081794", name: "超值午餐", sort: 0} |
### 2.3 代码实现
html页面中相关的代码都已经提供好了,我们已经分析了请求的信息,接下来就可以来创建服务端的CategoryController方法update方法。