Spring项目--外卖平台(四)


菜品分页查询

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、更新时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

前端页面分析

在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

  1. 访问页面(backend/page/food/list.html)时,发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据

  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

image-20210804224842336

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,而前端发起的请求中, 图片文件下载回显的代码,我们已经实现了。所以,我们只需要开发分页查询的功能即可,分页查询具体的请求信息如下:

请求 说明
请求方式 GET
请求路径 /dish/page
请求参数 ?page=1&pageSize=10&name=xxx

代码实现

上述我们已经分析了分页查询的请求信息,那么接下来,我们就需要在 DishController中开发方法,来完成菜品的条件分页查询,在分页查询时还需要给页面返回分类的名称,而分类的名称前端在接收的时候是通过 categoryName 属性获取的,那么对应的服务端也应该封装到 categoryName 属性中。

<el-table-column prop="categoryName" label="菜品分类"></el-table-column>

而在我们的实体类 Dish 中,仅仅包含 categoryId, 不包含 categoryName,那么我们应该如何封装查询的数据呢? 其实,这里我们可以返回DishDto对象,在该对象中我们可以拓展一个属性 categoryName,来封装菜品分类名称。

@Data
public class DishDto extends Dish {
    private List<DishFlavor> flavors = new ArrayList<>();
    private String categoryName; //菜品分类名称
    private Integer copies;
}

具体逻辑为:

1). 构造分页条件对象

2). 构建查询及排序条件

3). 执行分页条件查询

4). 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称

5). 封装数据并返回

/**
 * 菜品信息分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public Res<Page> page(int page,int pageSize,String nam
    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>
    //条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件
    queryWrapper.like(name != null,Dish::getName,name);
    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTim
    //执行分页查询
    dishService.page(pageInfo,queryWrappe
    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records
    List<Dish> records = pageInfo.getRecords
    List<DishDto> list = records.stream().map((item) -> {
        DishDto dishDto = new DishDto
        BeanUtils.copyProperties(item,dishDt
        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryI
        if(category != null){
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }
        return dishDto;
    }).collect(Collectors.toList(
    dishDtoPage.setRecords(lis
    return Res.success(dishDtoPage);
}

数据库查询菜品信息时,获取到的分页查询结果 Page 的泛型为 Dish,而我们最终需要给前端页面返回的类型为 DishDto,所以这个时候就要进行转换,基本属性我们可以直接通过属性拷贝的形式对Page中的属性进行复制,而对于结果列表 records属性,我们是需要进行特殊处理的(需要封装菜品分类名称);

菜品修改

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作。

前端页面分析

在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:

  1. 点击菜品列表的中的修改按钮,携带菜品id跳转至add.html

  2. 进入add.html,页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(已实现)

  3. add.html获取id, 发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显

  4. 页面发送请求,请求服务端进行图片下载,用于页图片回显(已实现)

  5. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

1. 根据ID查询菜品及菜品口味信息

具体请求信息如下:

请求 说明
请求方式 GET
请求路径 /dish/{id}

2. 修改菜品及菜品口味信息

具体请求信息如下:

请求 说明
请求方式 PUT
请求路径 /dish
请求参数 json格式数据

功能实现

页面发送ajax请求,请求服务端,根据id查询当前菜品信息和对应的口味信息,用于修改页面中菜品信息回显。

  1. 在DishService接口中扩展getByIdWithFlavor方法
//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);
  1. 在DishService实现类中实现此方法

具体逻辑为:

A. 根据ID查询菜品的基本信息

B. 根据菜品的ID查询菜品口味列表数据

C. 组装数据并返回

/**
 * 根据id查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
public DishDto getByIdWithFlavor(Long id) {
    //查询菜品基本信息,从dish表查询
    Dish dish = this.getById(i
    DishDto dishDto = new DishDto();
    BeanUtils.copyProperties(dish,dishDt
    //查询当前菜品对应的口味信息,从dish_flavor表查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dish.getId());
    List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
    dishDto.setFlavors(flavor
    return dishDto;
}

在DishController中创建get方法

/**
 * 根据id查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public Res<DishDto> get(@PathVariable Long i
    DishDto dishDto = dishService.getByIdWithFlavor(i
    return Res.success(dishDto);
}

修改菜品信息

点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端。在修改菜品信息时需要注意,除了要更新dish菜品表,还需要更新dish_flavor菜品口味表。

  1. 在DishService接口中扩展方法updateWithFlavor
//更新菜品信息,同时更新对应的口味信息
public void updateWithFlavor(DishDto dishDto);
  1. 在DishServiceImpl中实现方法updateWithFlavor

    在该方法中,我们既需要更新dish菜品基本信息表,还需要更新dish_flavor菜品口味表。而页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢,其实,无论菜品口味信息如何变化,我们只需要保持一个原则: 先删除,后添加。

@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
    //更新dish表基本信息
    this.updateById(dishDto);

    //清理当前菜品对应口味数据---dish_flavor表的delete操作
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

    dishFlavorService.remove(queryWrapper);

    //添加当前提交过来的口味数据---dish_flavor表的insert操作
    List<DishFlavor> flavors = dishDto.getFlavors();

    flavors = flavors.stream().map((item) -> {
        item.setDishId(dishDto.getId());
        return item;
    }).collect(Collectors.toList());

    dishFlavorService.saveBatch(flavors);
}

在DishController中创建update方法

@PutMapping
public Res<String> update(@RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.updateWithFlavor(dishDto);
    return Res.success("修改菜品成功");
}

新增套餐

套餐就是菜品的集合。

后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

数据模型

新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:

说明 备注
setmeal 套餐表 存储套餐的基本信息
setmeal_dish 套餐菜品关系表 存储套餐关联的菜品的信息(一个套餐可以关联多个菜品)

两张表具体的表结构如下:

1. 套餐表setmeal

image-20210805233615067

在该表中,套餐名称name字段是不允许重复的,在建表时,已经创建了唯一索引。

image-20210805234059563

2. 套餐菜品关系表setmeal_dish

image-20210805233807009

在该表中,菜品的名称name,菜品的原价price 实际上都是冗余字段,因为我们在这张表中存储了菜品的ID(dish_id),根据该ID我们就可以查询出name,price的数据信息,而这里我们又存储了name,price,这样的话,我们在后续的查询展示操作中,就不需要再去查询数据库获取菜品名称和原价了,这样可以简化我们的操作。

在开发业务功能前,先将需要用到的类和接口基本结构创建好,在做这一块儿的准备工作时,我们无需准备Setmeal的相关实体类、Mapper接口、Service接口及实现。

这里就省略了。

前端页面分析

服务端的基础准备工作我们准备完毕之后,在进行代码开发之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:

  1. 点击新建套餐按钮,访问页面(backend/page/combo/add.html),页面加载发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(已实现)

获取套餐分类列表的功能我们不用开发,之前已经开发完成了,之前查询时type传递的是1,查询菜品分类; 本次查询时,传递的type为2,查询套餐分类列表。

  1. 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(已实现)

本次查询分类列表,传递的type为1,表示需要查询的是菜品的分类。查询菜品分类的目的,是添加套餐关联的菜品时,我们需要根据菜品分类,来过滤查询菜品信息。

  1. 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中

  1. 页面发送请求进行图片上传,请求服务端将图片保存到服务器(已实现)

  2. 页面发送请求进行图片下载,将上传的图片进行回显(已实现)

  3. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

经过上述的页面解析及流程分析,我们发送这里需要发送的请求有5个,分别是 :

A. 根据传递的参数,查询套餐分类列表

B. 根据传递的参数,查询菜品分类列表

C. 图片上传

D. 图片下载展示

E. 根据菜品分类ID,查询菜品列表

F. 保存套餐信息

而对于以上的前4个功能我们都已经实现, 所以我们接下来需要开发的功能主要是最后两项, 具体的请求信息如下:

1. 根据分类ID查询菜品列表

请求 说明
请求方式 GET
请求路径 /dish/list
请求参数 ?categoryId=1397844263642378242

2. 保存套餐信息

请求 说明
请求方式 POST
请求路径 /setmeal
请求参数 json格式数据

代码开发

在当前的需求中,我们只需要根据页面传递的菜品分类的ID(categoryId)来查询菜品列表即可,我们可以直接定义一个DishController的方法,声明一个Long类型的categoryId,这样做是没问题的。但是考虑到该方法的拓展性,我们在这里定义方法时,通过Dish这个实体来接收参数。

在DishController中定义方法list,接收Dish类型的参数:

在查询时,需要根据菜品分类categoryId进行查询,并且还要限定菜品的状态为起售状态(status为1),然后对查询的结果进行排序。

/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public Res<List<Dish>> list(Dish dish){
    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
    //添加条件,查询状态为1(起售状态)的菜品
    queryWrapper.eq(Dish::getStatus,1);
    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
	
    List<Dish> list = dishService.list(queryWrapper);
	
    return Res.success(list);
}

保存套餐

在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 setmealDishes。所以这个时候我们使用Setmeal就不能完成参数的封装了,我们需要在Setmeal的基本属性的基础上,再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表,而我们在准备工作中,导入进来的SetmealDto能够满足这个需求。

  1. SetmealController中定义方法save,新增套餐

    在该Controller的方法中,我们不仅需要保存套餐的基本信息,还需要保存套餐关联的菜品数据,所以我们需要再该方法中调用业务层方法,完成两块数据的保存。

    页面传递的数据是json格式,需要在方法形参前面加上@RequestBody注解, 完成参数封装。

@PostMapping
public Res<String> save(@RequestBody SetmealDto setmealDto){
    log.info("套餐信息:{}",setmealDto);

    setmealService.saveWithDish(setmealDto);

    return Res.success("新增套餐成功");
}
  1. SetmealService中定义方法saveWithDish
/**
 * 新增套餐,同时需要保存套餐和菜品的关联关系
 * @param setmealDto
 */
public void saveWithDish(SetmealDto setmealDto);
  1. SetmealServiceImpl实现方法saveWithDish

    具体逻辑:

    A. 保存套餐基本信息

    B. 获取套餐关联的菜品集合,并为集合中的每一个元素赋值套餐ID(setmealId)

    C. 批量保存套餐关联的菜品集合

/**
 * 新增套餐,同时需要保存套餐和菜品的关联关系
 * @param setmealDto
 */
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
    //保存套餐的基本信息,操作setmeal,执行insert操作
    this.save(setmealDto);

    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
    setmealDishes.stream().map((item) -> {
        item.setSetmealId(setmealDto.getId());
        return item;
    }).collect(Collectors.toList());

    //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
    setmealDishService.saveBatch(setmealDishes);
}

套餐分页查询

系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。在进行套餐数据的分页查询时,除了传递分页参数以外,还可以传递一个可选的条件(套餐名称)。查询返回的字段中,包含套餐的基本信息之外,还有一个套餐的分类名称,在查询时,需要关联查询这个字段。

前端页面分析

在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:

  1. 访问页面(backend/page/combo/list.html),页面加载时,会自动发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据

  1. 在列表渲染展示时,页面发送请求,请求服务端进行图片下载,用于页面图片展示(已实现)

而对于以上的流程中涉及到2个功能,文件下载功能我们已经实现,本小节我们主要实现列表分页查询功能, 具体的请求信息如下:

请求 说明
请求方式 GET
请求路径 /setmeal/page
请求参数 ?page=1&pageSize=10&name=xxx

代码开发

上述我们已经分析列表分页查询功能的请求信息,接下来我们就在SetmealController中创建套餐分页查询方法。

该方法的逻辑如下:

  1. 构建分页条件对象

  2. 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序

  3. 执行分页查询

  4. 组装数据并返回

  5. 服务端仅返回分类ID(categoryId), 而页面展示需要的是categoryName属性。在查询套餐信息时, 只包含套餐的基本信息, 并不包含套餐的分类名称, 所以在这里查询到套餐的基本信息后, 还需要根据分类ID(categoryId), 查询套餐分类名称(categoryName),并最终将套餐的基本信息及分类名称信息封装到SetmealDto中。

/**
 * 套餐分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public Res<Page> page(int page,int pageSize,String name){
    //分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page,pageSize);
    Page<SetmealDto> dtoPage = new Page<>
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据name进行like模糊查询
    queryWrapper.like(name != null,Setmeal::getName,name);
    //添加排序条件,根据更新时间降序排列
    queryWrapper.orderByDesc(Setmeal::getUpdateTim
    setmealService.page(pageInfo,queryWrappe
    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dtoPage,"records");
    List<Setmeal> records = pageInfo.getRecords
    List<SetmealDto> list = records.stream().map((item) -> {
        SetmealDto setmealDto = new SetmealDto();
        //对象拷贝
        BeanUtils.copyProperties(item,setmealDto);
        //分类id
        Long categoryId = item.getCategoryId();
        //根据分类id查询分类对象
        Category category = categoryService.getById(categoryId);
        if(category != null){
            //分类名称
            String categoryName = category.getName();
            setmealDto.setCategoryName(categoryName);
        }
        return setmealDto;
    }).collect(Collectors.toList(
    dtoPage.setRecords(list);
    return Res.success(dtoPage);
}

删除套餐

在套餐管理列表页面,点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

前端分析

在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程:

1). 点击删除, 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐

2). 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐

开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,一次请求为根据ID删除,一次请求为根据ID批量删除。

观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

具体的请求信息如下:

请求 说明
请求方式 DELETE
请求路径 /setmeal
请求参数 ?ids=1423640210125656065,1423338765002256385

代码开发

在SetmealController中创建delete方法

SetmealService接口定义方法removeWithDish

@DeleteMapping
public Res<String> delete(@RequestParam List<Long> ids){
    log.info("ids:{}",ids);
    setmealService.removeWithDish(ids);
    return Res.success("套餐数据删除成功");
}

SetmealServiceImpl中实现方法removeWithDish

该业务层方法具体的逻辑为:

A. 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除

B. 删除套餐数据

C. 删除套餐关联的菜品数据

/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {
    //select count(*) from setmeal where id in (1,2,3) and status = 1
    //查询套餐状态,确定是否可用删除
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.in(Setmeal::getId,ids);
    queryWrapper.eq(Setmeal::getStatus,1);

    int count = this.count(queryWrapper);
    if(count > 0){
   	 	//如果不能删除,抛出一个业务异常
    	throw new CustomException("套餐正在售卖中,不能删除");
    }

    //如果可以删除,先删除套餐表中的数据---setmeal
    this.removeByIds(ids);

    //delete from setmeal_dish where setmeal_id in (1,2,3)
    LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
    //删除关系表中的数据----setmeal_dish
    setmealDishService.remove(lambdaQueryWrapper);
}

由于当前的业务方法中存在多次数据库操作,为了保证事务的完整性,需要在方法上加注解 @Transactional 来控制事务。


文章作者: 小小星仔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小星仔 !
评论
  目录