苍穹外卖:菜品分页查询与删除功能(保姆级详解)
🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:苍穹外卖日记,SSM框架深入,JavaWeb
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:前面我们完成了新增菜品的业务功能,这里我们将继续完成项目,实现菜品的分页查询功能,和删除菜品的业务功能,提供详细的代码讲解和逻辑解释。
分页查询:
分析产品原型:
┌─────────────────────────────────────────────────────────────┐ │ 菜品管理 > 菜品列表 [+新增菜品] │ ├─────────────────────────────────────────────────────────────┤ │ [菜品名称] [分类选择▼] [状态▼] [查询] [重置] │ ├─────────────────────────────────────────────────────────────┤ │ □ 菜品图片 菜品名称 分类 价格 状态 最后更新时间 操作 │ ├─────────────────────────────────────────────────────────────┤ │ □ [图片] 宫保鸡丁 川菜 ¥38 ● 起售 2024-01-15 [修改] [停售] │ │ □ [图片] 麻婆豆腐 川菜 ¥28 ● 起售 2024-01-15 [修改] [停售] │ │ □ [图片] 北京烤鸭 主打菜 ¥88 ○ 停售 2024-01-14 [修改] [起售] │ │ □ [图片] 西湖醋鱼 浙菜 ¥68 ● 起售 2024-01-13 [修改] [停售] │ ├─────────────────────────────────────────────────────────────┤ │ < 1 2 3 4 5 ... 10 > │ │ 每页10条 共43条记录 │ └─────────────────────────────────────────────────────────────┘
功能区详解
A. 顶部操作区
标题导航:菜品管理 > 菜品列表(面包屑导航)
新增按钮:右上角醒目位置,绿色主按钮
B. 查询条件区
| 组件 | 类型 | 作用 |
|---|---|---|
| 菜品名称 | 输入框 | 模糊搜索 |
| 分类选择 | 下拉框 | 按菜品分类筛选 |
| 状态 | 下拉框 | 起售/停售筛选 |
| 查询按钮 | 按钮 | 触发查询 |
| 重置按钮 | 按钮 | 清空条件 |
C. 列表展示区
表头设计:
□ 复选框(批量操作预留)
菜品图片(缩略图)
菜品名称
分类(所属分类)
价格(带¥符号)
状态(带颜色标识)
最后更新时间
操作(链接按钮)
状态标识:
● 绿色圆点:起售
○ 灰色圆点:停售
操作按钮:
修改:文字链接
停售/起售:状态切换按钮
D分页区
页码导航
每页显示条数(默认10条)
总记录数统计
业务规则:
根据页码展示菜品信息,每页展示十条菜品数据,分页查询时可以根据需要根据菜品名称,菜品分类,菜品状态进行查询。
逻辑图示:
┌─────────┐ ┌─────────┐ ┌─────────┐ │ 前端 │ │ 后端 │ │ 数据库 │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ 1. 用户输入查询条件 │ │ │────────────────────>│ │ │ │ │ │ 2. 点击查询按钮 │ │ │────────────────────>│ │ │ │ │ │ 3. 封装请求参数 │ │ │ GET /admin/dish/page│ │ │ ?page=1&pageSize=10 │ │ │ &name=宫保&status=1 │ │ │────────────────────>│ │ │ │ │ │ │ 4. 接收参数 │ │ │ DishPageQueryDTO │ │ │────────────────────>│ │ │ │ │ │ 5. 调用Service │ │ │ pageQuery() │ │ │────────────────────>│ │ │ │ │ │ │ 6. 执行SQL │ │ │ SELECT... │ │ │<───┘ │ │ 7. 返回PageResult │ │ │ (total+records) │ │ │<────────────────────│ │ │ │ │ 8. 返回JSON数据 │ │ │ <────────────────────│ │ │ │ │ │ 9. 渲染表格和分页 │ │ │────────────────────>│ │
代码实现:
Controller:
pageResult是后端封装分页查询结果的统一对象,它包含了总记录数(total)和当前页数据列表(records)两个核心信息,前端拿到后就能知道总共有多少条数据、当前页显示哪些数据。
@GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page (DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询,#{}",dishPageQueryDTO); PageResult pageResult= dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult);Service:
PageHelper.startPage
作用:像一个"开关",开启分页功能
原理:将分页参数存入ThreadLocal(当前线程的私有存储空间)
为什么放在这里:必须在查询语句之前执行,才能拦截即将执行的SQL
dishMapper.pageQuery()—— 执行查询
作用:看似普通查询,实则被PageHelper增强
原理:PageHelper拦截器会自动修改SQL,添加LIMIT子句
为什么返回Page:PageHelper自动将List包装成Page对象,包含分页信息
new PageResult()—— 结果封装
作用:从Page对象中提取关键信息,封装成统一格式
为什么不用Page对象直接返回:Page对象包含太多内部信息,PageResult是精简版
getTotal():总记录数(用于前端分页组件)
getResult():当前页数据列表(用于前端表格渲染)
page.getResult() 返回的就是:
物理上:Page对象内部存储数据列表的那个ArrayList
逻辑上:当前页需要展示的N条业务数据
用途上:提供给前端渲染表格的数据源
数量上:等于pageSize(最后一页可能少于pageSize)
它是连接数据库原始数据和前端展示界面的桥梁,是分页查询中最核心的数据载体。
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize()); Page<DishVO> page=dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(),page.getResult());Mapper:
为什么不需要手动拼接page和pageSize?
AOP思想:PageHelper通过拦截器实现关注点分离,业务代码不需要关心分页细节
自动计算:自动计算偏移量(offset)并拼接LIMIT子句
自动COUNT:自动执行COUNT查询获取总记录数
自动包装:自动将List包装成Page对象,提供分页信息
统一处理:所有分页查询使用相同的方式,代码更简洁
PageHelper的魔法本质:
startPage():将分页参数存入ThreadLocal
拦截器:拦截SQL执行,从ThreadLocal读取参数
SQL改造:自动添加COUNT和LIMIT
结果包装:返回包含分页信息的Page对象
<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.*,c.name as categoryName from sky_take_out.dish d left outer join sky_take_out.category c on d.category_id = c.id <where> <if test="name!=null"> and d.name like concat('%',#{name},'%') </if>> <if test="categoryId!=null"> and d.category_id=#{categoryId} </if>> <if test="status!=null"> and d.status=#{status} </if>> </where>> </select>因为我们要查询的是菜品分类的名字而不是ID,所以要查询两张表,
关于PageHelper的底层执行:
// 2.1 你写的SQL(原始) SELECT d.*, c.name as categoryName FROM dish d LEFT JOIN category c ON d.category_id = c.id WHERE d.name LIKE '%鸡%' // 2.2 PageHelper拦截后,实际执行了2条SQL // 第1条SQL:自动执行的COUNT查询 SELECT COUNT(*) FROM ( SELECT d.*, c.name as categoryName FROM dish d LEFT JOIN category c ON d.category_id = c.id WHERE d.name LIKE '%鸡%' ) tmp_count; // 第2条SQL:自动添加LIMIT的分页查询 SELECT d.*, c.name as categoryName FROM dish d LEFT JOIN category c ON d.category_id = c.id WHERE d.name LIKE '%鸡%' LIMIT 0, 10; -- PageHelper自动添加的! // 第3步:PageHelper把两条结果包装成Page对象返回 // page对象包含了total(43)和list(10条数据)删除菜品:
需求分析和设计:
可以一次删除一个菜品,也可以一次删除多个菜品
起售的菜品不能删除
被套餐关联的菜品不能删除
删除菜品后,关联的菜品口味数据也要删除
数据库设计:
dish表,dish-flavor表,setmeal-dish表
实现流程:
Controller:
package com.sky.controller.admin; /** * 菜品管理Controller * 接收前端删除菜品的请求 */ @RestController @RequestMapping("/admin/dish") @Api(tags = "菜品管理") @Slf4j public class DishController { @Autowired private DishService dishService; /** * 删除菜品(支持批量) * @param ids 菜品ID列表,例如:ids=1,2,3 * @return */ @DeleteMapping @ApiOperation("删除菜品") public Result delete(@RequestParam List<Long> ids) { log.info("删除菜品,ids:{}", ids); // 调用Service层执行业务 dishService.deleteBatch(ids); return Result.success(); } }/** * 解析: * 1. @DeleteMapping:处理HTTP DELETE请求 * 2. @RequestParam List<Long> ids:接收URL参数,如 /admin/dish?ids=1,2,3 * 3. 直接调用Service,Controller不处理业务逻辑 */Service
package com.sky.service; public interface DishService { /** * 批量删除菜品 * @param ids 菜品ID列表 */ void deleteBatch(List<Long> ids); } /** * 解析: * 1. 定义业务接口,明确功能 * 2. 批量删除方法,接收ID列表 */Service层实现 - 核心业务逻辑
package com.sky.service.impl; /** * 菜品管理Service实现类 * 核心业务逻辑都在这里 */ @Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; @Autowired private SetmealDishMapper setmealDishMapper; /** * 批量删除菜品 * @param ids 菜品ID列表 */ @Override @Transactional // 事务注解,保证数据一致性 public void deleteBatch(List<Long> ids) { log.info("批量删除菜品,ids:{}", ids); // ========== 第1步:参数校验 ========== if (ids == null || ids.isEmpty()) { throw new BusinessException("请选择要删除的菜品"); } // ========== 第2步:检查菜品状态(是否起售) ========== // 根据ID列表查询所有菜品信息 List<Dish> dishList = dishMapper.selectByIds(ids); for (Dish dish : dishList) { if (dish.getStatus() == 1) { // 1表示起售 throw new BusinessException("菜品【" + dish.getName() + "】正在起售中,不能删除"); } } // ========== 第3步:检查是否被套餐关联 ========== // 根据菜品ID查询关联的套餐ID List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if (setmealIds != null && setmealIds.size() > 0) { // 查询套餐名称用于提示 List<Setmeal> setmeals = setmealMapper.selectByIds(setmealIds); String setmealNames = setmeals.stream() .map(Setmeal::getName) .collect(Collectors.joining("、")); throw new BusinessException("菜品已被套餐【" + setmealNames + "】关联,不能删除"); } // ========== 第4步:删除关联的口味数据 ========== dishFlavorMapper.deleteByDishIds(ids); log.info("删除菜品关联的口味数据,dishIds:{}", ids); // ========== 第5步:删除菜品本身 ========== dishMapper.deleteByIds(ids); log.info("删除菜品成功,ids:{}", ids); } }Mapper层 - 数据访问
package com.sky.mapper; @Mapper public interface DishMapper { /** * 根据ID列表批量查询菜品 * @param ids 菜品ID列表 * @return 菜品列表 */ List<Dish> selectByIds(@Param("ids") List<Long> ids); /** * 根据ID列表批量删除菜品 * @param ids 菜品ID列表 */ void deleteByIds(@Param("ids") List<Long> ids); }注意事项:
根据主键 id查询菜品的起售状态,主键id返回所有的字段,所以可以调用查询字段的方法
id:单个菜品的唯一标识(如:
id = 1)ids:多个菜品ID的集合(如:
ids = [1,2,3])dishId:强调是菜品的ID,通常用于关联查询(如口味表、套餐表中的外键)
| 场景 | 命名 | 类型 | 示例 | 说明 |
|---|---|---|---|---|
| 主键ID | id | Long | dish.getId() | 实体类的主键字段 |
| 外键ID | xxxId | Long | flavor.setDishId() | 指向其他表的外键 |
| 批量主键 | ids | List<Long> | deleteByIds(ids) | 多个主键的集合 |
| 批量外键 | xxxIds | List<Long> | deleteByDishIds(dishIds) | 多个外键的集合 |
| 遍历元素 | item名 | 类型 | foreach中的item | 取决于业务含义 |
| 对比维度 | id | ids | dishId |
|---|---|---|---|
| 数据类型 | Long(单个数值) | List<Long>(集合) | Long(单个数值) |
| 英文含义 | 单数:一个ID | 复数:多个ID | 单数:菜品的ID |
| 数量 | 1个 | N个(≥1) | 1个 |
| 所属表 | 当前操作的表 | 当前操作的表 | 通常是外键,指向dish表 |
| 在SQL中的位置 | WHERE id = #{id} | WHERE id IN (1,2,3) | WHERE dish_id = #{dishId} |
| 业务含义 | 当前表的主键 | 当前表的主键集合 | 菜品表的外键 |
| 典型场景 | 删除单个菜品 | 批量删除菜品 | 删除菜品的口味 |
结语:如果对你有帮助,请,点赞,关注,收藏,你的支持就是我最大的动力!!
