苍穹外卖:菜品分页查询与删除功能(保姆级详解)

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介: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)两个核心信息,前端拿到后就能知道总共有多少条数据、当前页显示哪些数据。

java 复制代码
    @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)

它是连接数据库原始数据和前端展示界面的桥梁,是分页查询中最核心的数据载体。

java 复制代码
    /**
     * 菜品分页查询
     * @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?

  1. AOP思想:PageHelper通过拦截器实现关注点分离,业务代码不需要关心分页细节

  2. 自动计算:自动计算偏移量(offset)并拼接LIMIT子句

  3. 自动COUNT:自动执行COUNT查询获取总记录数

  4. 自动包装:自动将List包装成Page对象,提供分页信息

  5. 统一处理:所有分页查询使用相同的方式,代码更简洁

PageHelper的魔法本质

  • startPage():将分页参数存入ThreadLocal

  • 拦截器:拦截SQL执行,从ThreadLocal读取参数

  • SQL改造:自动添加COUNT和LIMIT

  • 结果包装:返回包含分页信息的Page对象

java 复制代码
<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的底层执行:

java 复制代码
// 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:

java 复制代码
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

java 复制代码
package com.sky.service;

public interface DishService {
    
    /**
     * 批量删除菜品
     * @param ids 菜品ID列表
     */
    void deleteBatch(List<Long> ids);
}

/**
 * 解析:
 * 1. 定义业务接口,明确功能
 * 2. 批量删除方法,接收ID列表
 */

Service层实现 - 核心业务逻辑

java 复制代码
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层 - 数据访问

java 复制代码
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}
业务含义 当前表的主键 当前表的主键集合 菜品表的外键
典型场景 删除单个菜品 批量删除菜品 删除菜品的口味

结语:如果对你有帮助,请,**点赞,关注,收藏,**你的支持就是我最大的动力!!

相关推荐
EverestVIP1 小时前
c++前置声明的方式与说明
开发语言·c++
亚马逊云开发者1 小时前
MCP 到底是什么?一篇讲透:协议原理 + Bedrock 实战 + 安全防护
java
Navicat中国1 小时前
利用 PostgreSQL 的强大力量:Supabase 简介
数据库·postgresql·navicat·supabase
yqzyy2 小时前
Redis 设置密码无效问题解决
数据库·redis·缓存
0和1的舞者2 小时前
高并发论坛系统:单元测试 + 接口自动化 + 性能测试 + CI/CD 全链路测试报告
java·测试开发·测试工具·jmeter·pytest·测试·测试报告
程序员鱼皮2 小时前
OpenClaw接入飞书保姆级教程,几分钟搞定手机养龙虾!
前端·人工智能·后端
huangliang07032 小时前
oracle使用模版创建分区表
数据库·oracle
Victor3562 小时前
MongoDB(36)如何使用聚合进行分组?
后端
江不清丶2 小时前
Kafka消息积压排查与治理:从应急处理到长期优化
数据库·kafka·linq