- 本文章发布在CSDN上,一是方便博主自己线上阅览,二是巩固自己所学知识。
- 博客内容主要参考上述视频和资料,视频中出现的 PPT 内容大体也为本文所录。
- 参考博客: 【瑞吉外卖项目剩余功能补充】
- 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
文章目录
上一篇:学习【瑞吉外卖④】SpringBoot单体项目:https://blog.csdn.net/yanzhaohanwei/article/details/125138771
0.总目录
- 学习【瑞吉外卖①】SpringBoot单体项目
- 软件开发流程、瑞吉外卖项目介绍、环境搭建、后台登录功能、后台退出功能
- 学习【瑞吉外卖②】SpringBoot单体项目(后台)
- 完善登录功能、 新增员工功能、员工信息分页查询功能、启用 / 禁用员工账号功能、编辑员工信息功能
- 学习【瑞吉外卖③】SpringBoot单体项目(后台)
- 公共字段自动填充功能、新增分类功能、分类信息分页查询功能、删除分类功能、修改分类功能
- 学习【瑞吉外卖④】SpringBoot单体项目(后台)
- 文件上传下载功能、新增菜品功能、菜品信息分页查询功能、修改菜品功能
- 其他功能:删除菜品(单个 / 批量)功能、停售 / 启售菜品(单个 / 批量)功能。
- 学习【瑞吉外卖⑤】SpringBoot单体项目(后台)
- 新增套餐功能、套餐分页查询功能、删除套餐功能(单个 / 批量)
- 其他功能:停售 / 启售(批量 / 单个)套餐功能、修改套餐功能
- 学习【瑞吉外卖⑥】SpringBoot单体项目(移动端)
- 手机验证码登录功能(短信发送、手机验证码登录)
- 学习【瑞吉外卖⑦】SpringBoot单体项目(移动端)
- 用户地址簿功能:增删改查功能,设置、查看默认地址功能。
- 菜品展示功能(套餐展示功能也包含在其中)
- 购物车功能:购物车中增加/减少 套餐/菜品的功能,菜品/套餐 在购物车中的展示功能,购物车中 菜品/套餐 清空的功能
- 订单功能:用户下单功能、用户查看订单功能、用户再来一单功能。
- 用户登出功能
- 学习【瑞吉外卖⑧】SpringBoot单体项目(后台)
- 订单展示功能、订单状态修改功能
1.新增套餐功能
1.1.需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐。
在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片。
在移动端会按照套餐分类来展示对应的套餐。
1.2.数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到 setmeal 表,还需要向 setmeal_dish 表插入套餐和菜品关联数据。
所以在新增套餐时,涉及到两个表:setmeal(套餐表)、setmeal_dish(套餐菜品关系表)
- setmeal(套餐表)
- setmeal_dish(套餐菜品关系表)
1.3.代码开发
1.3.1.准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好。
- 实体类 SetmealDish
- DTO 类 SetmealDto
- Mapper 接口 SetmealDishMapper
- 业务层接口 SetmealDishService
- 业务层实现类 SetmealDishServicelmpl
- 控制层 SetmealController
此外,Setmeal 相关的实体类、Mapper、Service 在【瑞吉外卖③】分类业务管理开发中的 4.3.2 里已经创建过了,这里不再赘述。
com/itheima/reggie/entity/SetmealDish.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 SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐 id
private Long setmealId;
//菜品 id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
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;
}
com/itheima/reggie/dto/SetmealDto.java
package com.itheima.reggie.dto;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
com/itheima/reggie/mapper/SetmealMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {}
com/itheima/reggie/service/SetmealDishService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.SetmealDish;
public interface SetmealDishService extends IService<SetmealDish> {}
业务层接口实现类 SetmealDishServicelmpl
com/itheima/reggie/service/impl/SetmealDishServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.SetmealDish;
import com.itheima.reggie.mapper.SetmealDishMapper;
import com.itheima.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {}
com/itheima/reggie/service/SetmealDishService.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.CategoryService;
import com.itheima.reggie.service.SetmealDishService;
import com.itheima.reggie.service.SetmealService;
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("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private CategoryService categoryService;
}
1.3.2.梳理交互过程
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程。
- 页面(
backend/page/combo/add.html
)发送 ajax 请求,请求服务端获取套餐分类数据并展示到下拉框中。 - 页面发送 ajax 请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中。
- 页面发送 ajax 请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中。
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器。
- 页面发送请求进行图片下载,将上传的图片进行回显。
- 点击保存按钮,发送 ajax 请求,将套餐相关数据以 json 形式提交到服务端。
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
其中 1、2、4、5 的交互我们在前面已经实现了。
1.3.3.根据菜品分类查询数据
1.3.3.1.前端分析
1.3.3.2.代码编写
com/itheima/reggie/controller/DishController.java
/**
* 根据条件查询对应的菜品数据
*
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<Dish>();
//添加查询条件
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//调用 service 方法拿到 list 集合
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
这里展示一下 Idea 控制台打印的 SQL 语句
SELECT id,name,category_id,price,code,image,description,status,sort,create_time,update_time,create_user,update_user \
FROM dish \
WHERE (category_id = ?) ORDER BY sort ASC,update_time DESC;
点击“添加菜品”按钮就会弹出“添加菜品”界面,此时界面中出现了具体菜品信息。
说明根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中的功能已经实现。
实体类 Dish(com/itheima/reggie/entity/Dish.java
) 中存在 private Integer status; // 0 停售;1 起售
这样的字段。
所以我们需要加一些查询的条件,只查状态为 1 的菜品。
com/itheima/reggie/controller/DishController.java
queryWrapper.eq(Dish::getStatus, 1);
此时 idea 控制台上的输出语句
SELECT id,name,category_id,price,code,image,description,status,sort,create_time,update_time,create_user,update_user \
FROM dish \
WHERE (category_id = ? AND status = ?) ORDER BY sort ASC,update_time DESC;
1.3.4.新增套餐功能
1.3.4.1.前端分析
1.3.4.2.代码编写
com/itheima/reggie/service/SetmealService.java
public interface SetmealService extends IService<Setmeal> {
//新增套餐,同时需要保存套餐和菜品的关联关系
public void saveWithDish(SetmealDto setmealDto);
}
com/itheima/reggie/service/impl/SetmealServiceImpl.java
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
*
* @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);
}
}
com/itheima/reggie/controller/SetmealController.java
/**
* 新增套餐
*
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto) {
log.info("套餐信息:{}", setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
idea 控制台上输出的 SQL 语句。
INSERT INTO setmeal \
( id, category_id, name, price, status, code, description, image, create_time, update_time, create_user, update_user ) \
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? );
INSERT INTO setmeal_dish \
( id, setmeal_id, dish_id, name, price, copies, create_time, update_time, create_user, update_user ) \
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? );
2.套餐分页查询功能
2.1.需求分析
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看。
一般的系统中都会以分页的方式来展示列表数据。
2.2.梳理交互过程
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程。
-
页面(
backend/page/combo/list.html
)发送 ajax 请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据。 -
页面发送请求,请求服务端进行图片下载,用于页面图片展示。
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这 2 次请求即可。
2.3.前端分析
- 页面(
backend/page/combo/list.html
)发送 ajax 请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据。
2.4.代码编写
com/itheima/reggie/controller/SetmealController.java
/**
* 套餐分页查询
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<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::getUpdateTime);
setmealService.page(pageInfo, queryWrapper);
//对象拷贝
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 R.success(dtoPage);
}
3.删除套餐功能
3.1.需求分析
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。
也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意:对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
3.2.梳理交互过程
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程
- 删除单个套餐时,页面发送 ajax 请求,根据套餐 id 删除对应套餐
- 删除多个套餐时,页面发送 ajax 请求,根据提交的多个套餐 id 删除对应套餐
开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这 2 次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的。
不同的是传递的 id 个数,所以在服务端可以提供一个方法来统一处理。
3.3.代码编写
com/itheima/reggie/service/SetmealService.java
//删除套餐,同时需要删除套餐和菜品的关联数据
public void removeWithDish(List<Long> ids);
com/itheima/reggie/service/impl/SetmealServiceImpl.java
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
*
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {
//查询套餐状态,确定是否可用删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<Setmeal>();
//sql 语句:select count(*) from setmeal where id in (1,2,3) and status=1;
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);
//sql 语句:delete from setmeal_dish where setemal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<SetmealDish>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);
//删除关系表中的数据
setmealDishService.remove(lambdaQueryWrapper);
}
com/itheima/reggie/controller/SetmealController.java
/**
* 删除套餐功能
*
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
log.info("ids:{}", ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
当删除套餐成功时,IDEA 工具的控制台的输出的部分 SQL 语句如下所示。
SELECT COUNT( * ) FROM setmeal WHERE (id IN (?) AND status = ?)
DELETE FROM setmeal WHERE id IN ( ? )
DELETE FROM setmeal_dish WHERE (setmeal_id IN (?))
因为是status=0
(停售状态)时,才可以删除成功。
但是我们目前又没有做停售功能,为了方便功能测试的进行,我们可以先在数据库中改动信息。
4.其他功能
至此,套餐管理业务开发中,尚未完成的功能有:停售 / 启售(批量 / 单个)套餐功能、修改套餐功能。
官方给出的视频里并没有对这些功能的讲解,意在各位自学者自行开发这些功能。
接下来的代码编写则是参考了《瑞吉外卖项目剩余功能补充》这篇博客。
4.1.套餐停售/启售功能
4.1.1.分析
需求分析
- 在套餐管理列表页面点击停售/启售按钮,可以停售/启售对应的套餐信息。
- 也可以通过复选框选择多个套餐,点击批量停售/批量启售按钮一次停售/启售多个套餐。
梳理交互过程
在开发代码之前,需要梳理一下停售/启售时前端页面和服务端的交互过程
- 停售/启售单个套餐时,页面发送 ajax 请求,根据套餐 id 来停售/启售对应套餐
- 停售/启售多个套餐时,页面发送 ajax 请求,根据提交的多个套餐 id 来停售/启售对应套餐
开发停售/启售套餐功能,其实就是在服务端编写代码去处理前端页面发送的这 2 次请求即可。
观察停售/启售单个套餐和批量停售/启售套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的。
不同的是传递的 id 个数,所以在服务端可以提供一个方法来统一处理。
4.1.2.代码开发
com/itheima/reggie/service/SetmealService.java
//停售/启售套餐(单个/批量)
public void status(Integer status, List<Long> ids);
com/itheima/reggie/service/impl/SetmealServiceImpl.java
/**
* 停售/启售套餐(单个/批量)
*
* @param status
* @param ids
*/
@Transactional
public void status(Integer status, List<Long> ids) {
//条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<Setmeal>();
//添加条件
queryWrapper.in(ids != null, Setmeal::getId, ids);
List<Setmeal> list = this.list(queryWrapper);
for (Setmeal setmeal : list) {
if (setmeal != null) {
setmeal.setStatus(status);
this.updateById(setmeal);
}
}
}
com/itheima/reggie/controller/SetmealController.java
/**
* 停售/启售套餐(单个/批量)
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
setmealService.status(status, ids);
return R.success("售卖状态修改成功");
}
以下是IDEA控制台的部分输出语句。
SELECT id,category_id,name,price,status,code,description,image,create_time,update_time,create_user,update_user \
FROM setmeal WHERE (id IN (?));
UPDATE setmeal SET
category_id=?, name=?, price=?, status=?, code=?, description=?, image=?, \
create_time=?, update_time=?, create_user=?, update_user=? WHERE id=?
4.2.套餐修改功能
4.2.1.分析整理
需求分析
在套餐管理列表页面点击修改按钮,跳转到修改套餐页面,在修改页面回显套餐相关信息并进行修改,最后点击确定按钮完成修改操作。
梳理交互过程
在开发代码之前,需要梳理一下修改套餐时前端页面(add.html)和服务端的交互过程。
- 页面发送 ajax 请求,请求服务端获取分类数据,用于套餐分类下拉框中数据展示
- 页面发送 ajax 请求,请求服务端,根据 id 查询当前套餐信息,用于套餐信息回显
- 页面发送请求,请求服务端进行图片下载,用于页面回显。
- 点击保存按钮,页面发送 ajax 请求,将修改后的菜品相关数据以 json 形式提交到服务端
开发修改套餐功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
其中第1步、第3步的操作前面已经完成了。(新增、修改界面本质上用的是同一个界面)
4.2.2.套餐信息回显
com/itheima/reggie/service/SetmealService.java
//根据 id 查询套餐信息
public SetmealDto getById(Long id);
com/itheima/reggie/service/impl/SetmealServiceImpl.java
/**
* 根据 id 查询套餐信息
*
* @param id
* @return
*/
@Override
public SetmealDto getById(Long id) {
//查询套餐的基本信息,从 setmeal 表查询
Setmeal setmeal = super.getById(id);
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
//查询当前套餐对应的菜品。从关联表(setmeal_dish)中查询,
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<SetmealDish>();
queryWrapper.eq(SetmealDish::getSetmealId, setmeal.getId());
List<SetmealDish> list = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(list);
return setmealDto;
}
com/itheima/reggie/controller/SetmealController.java
/**
* 根据 id 查询套餐信息(包括套餐信息对应的菜品信息)
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> get(@PathVariable Long id) {
SetmealDto setmealDto = setmealService.getById(id);
return R.success(setmealDto);
}
4.2.3.修改套餐信息
com/itheima/reggie/service/SetmealService.java
//更新套餐信息,同时更新对应的菜品信息
public void updateWithDish(SetmealDto setmealDto);
com/itheima/reggie/service/impl/SetmealServiceImpl.java
/**
* 更新套餐信息,同时更新对应的菜品信息
*
* @param setmealDto
*/
@Override
@Transactional
public void updateWithDish(SetmealDto setmealDto) {
//更新 setmeal 表的基本信息
this.updateById(setmealDto);
//清理当前套餐对应的菜品数据,即 setmeal_dish 表的 delete 操作
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<SetmealDish>();
queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
setmealDishService.remove(queryWrapper);
//添加当前提交的菜品数据,即 setmeal_dish 表的 insert 操作
List<SetmealDish> mealDishes = setmealDto.getSetmealDishes();
mealDishes = mealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(mealDishes);
}
com/itheima/reggie/controller/SetmealController.java
/**
* 修改套餐信息,同时更新对应的菜品信息
*
* @param setmealDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody SetmealDto setmealDto) {
log.info(setmealDto.toString());
if (setmealDto == null) {
return R.error("请求异常");
}
if (setmealDto.getSetmealDishes() == null) {
return R.error("套餐没有菜品,请添加套餐");
}
setmealService.updateWithDish(setmealDto);
return R.success("修改菜品成功");
}
下一篇:学习【瑞吉外卖⑥】SpringBoot单体项目 https://blog.csdn.net/yanzhaohanwei/article/details/125228024