瑞吉外卖Day03、04
公共字段自动填充
使用MybatisPlus实现
问题分析
代码实现
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
-
在实体类的属性上加入@TableField注解,指定自动填充的策略
-
按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
对公共的字段加入注解,指定其自动填充的策略
@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;
编写自定义元数据对象处理器
将之前controller中的公共字段手动填充注释掉
功能完善
前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。
有的同学可能想到,用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意,我们在MyMetaObjectHandler类中是不能获得HttpSession.对象的,所以我们需要通过其他方式来获取登录用户id。可以使用ThreadLocals来解决此问题,它是)DK中提供的一个类。
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocali为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
实现步骤:
- 编写BaseContext.工具类,基于ThreadLocal封装的工具类
- 在LoginCheckFilterl的doFilter,方法中调用BaseContext:来设置当前登录用户的id:
- 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户
/**
* 自定义元数据对象处理器
*/
@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",new Long(BaseContext.getCurrentId()));
metaObject.setValue("updateUser",new Long(BaseContext.getCurrentId()));
}
/**
* 更新自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段填充-UPDATE");
// log.info(metaObject.toString());
// log.info("当前线程的id:{}",Thread.currentThread().getId());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
/**
* 基于ThreadLocak封装工具类,用户保存和获取当前session中的id
* 作用范围为一个线程之内,一次请求一个线程
*/
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();
}
}
新增分类功能
数据模型
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
-
实体类Category(直接从课程资料中导入即可)
-
Mapper接口CategoryMapper
-
业务层接口CategoryService
-
业务层实现类CategoryServicelmpl
-
控制层CategoryController
@RestController
@Slf4j
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 新增分类
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("新增菜品为:{}",category.toString());
categoryService.save(category);
return R.success("新增菜品成功");
}
}
分类信息分页查询
代码开发
/**
* 分类分页查询
* @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);
// 调用service进行排序
categoryService.page(pageinfo,queryWrapper);
return R.success(pageinfo);
}
删除分类功能
需求分析
代码开发
功能完善
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 删除分类,删除之前要进行判断
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count = dishService.count(dishLambdaQueryWrapper);
//查询当前的分类是否关联了菜品,如果关联直接抛出异常
if (count > 0){
//抛出异常
throw new CustomException("当前分类下关联了菜品,无法删除");
}
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count1 = setmealService.count(setmealLambdaQueryWrapper);
//查询当前分类是否关联了套餐,如果关联直接抛出异常
if (count1 > 0){
throw new CustomException("当前分类下关联了套餐,无法删除");
}
// 如果判断都没有成功,那么直接删除
super.removeById(id);
}
}
/**
* 根据ID删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类的id:{}",id);
categoryService.remove(id);
return R.success("删除成功");
}
自定义异常
/**
* 自定义业务异常
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
将自定义异常添加到全局异常处理类当中,这样就可以在前端页面提示具体的错误信息
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException exception){
log.info(exception.getMessage());
return R.error(exception.getMessage());//页面上就会显示提示信息
}
修改分类
需求分析
/**
* 修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category.toString());
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件上传下载功能
文件上传介绍
文件下载介绍
文件上传代码实现
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
//file名字要和前端name里面的名字一样
public R<String> upload(MultipartFile file){
// file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件将会删除
log.info("上传文件");
// 新的文件名字
String fileName = UUID.randomUUID().toString()+
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
// 创建一个目录文件
File dir = new File(basePath);
if (!dir.exists()){
dir.mkdirs();
}
try {
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
reggie:
path: D:\reggieimg\
文件下载代码实现
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void dowanload(String name, HttpServletResponse response){
try {
// 输入流来读取文件内容
FileInputStream inputStream = new FileInputStream(new File(basePath+name));
// 输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
// 设置相应文件的格式
response.setContentType("image/jepg");
int len = 0;
// 将读到的数据写入到byte数组中
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) != -1){
// 如果len不等于-1,那就继续读,直到len=-1,表示已经读取完毕
// 将读取到的数据通过输出流写回浏览器
outputStream.write(bytes,0,len);
outputStream.flush();
}
inputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
新增菜品功能
需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
数据模型
代码开发
导入DTO
菜品下拉框的实现
/**
* 菜品下拉框
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
log.info("菜品分类下拉框.");
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件
queryWrapper.orderByAsc(Category::getSort);
queryWrapper.orderByDesc(Category::getUpdateTime);
//通过MybatisPlus使用list方法查询到所有符合条件的菜品,并封装为list传递给前端
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
传输的数据和实体类的属性并不是一一对应,需要引入DTO对象来进行传输
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
在service新增一个方法来将菜品和菜品口味存储到不同的表中
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品和口味数据
* @param dishDto
*/
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
// 保存菜品到菜品表
this.save(dishDto);
// 保存菜品口味数据到口味表
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item)->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
}
涉及到多张表,这里面需要开始事务操作,并且在启动类中加入@EnableTransactionManagement
注解开启事务
@RestController
@Slf4j
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功..");
}
}
菜品信息分页查询
与之前不同的是,需要展示菜品的图片和分类
代码开发
/**
* 分页条件查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//分页构造器
Page<Dish> pageInfo = new Page<Dish>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//增加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo,queryWrapper);
//进行对象拷贝,将上面的page拷贝到下面的page中去
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
//基于stream流的方式,通过lambda表达式,将records中的属性拷贝到Dishdto对象中,并且通过service层查询到categotyId所对应的
//分类名称,设置到DishDto对象当中,返会一个DishDto的list对象到前端页面
List<DishDto> list = records.stream().map((item)->{
DishDto dishDto = new DishDto();
Long categoryId = item.getCategoryId();//分类id
String categoryName = categoryService.getById(categoryId).getName();
dishDto.setCategoryName(categoryName);
BeanUtils.copyProperties(item,dishDto);
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
修改菜品
需求分析
代码开发
/**
* 根据菜品id查询菜品和口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
// 查询菜品基本信息
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
// 查询菜品的口味信息
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
// 将口味的list查询出来
List<DishFlavor> list = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(list);
return dishDto;
}
/**
* 更新菜品和口味信息
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
// 更新菜品表
this.updateById(dishDto);
// 清理当前菜品表的口味数据-delete
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
// 再添加口味味表信息-insert
List<DishFlavor> flavors = dishDto.getFlavors();
flavors.stream().map((item)->{
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
// 请求在url中
/**
* 根据id进行查询菜品信息并回显
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id){
DishDto byIdWithFlavor = dishService.getByIdWithFlavor(id);
return R.success(byIdWithFlavor);
}
/**
* 更新菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}