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

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

🎬作者简介: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}
业务含义 当前表的主键 当前表的主键集合 菜品表的外键
典型场景 删除单个菜品 批量删除菜品 删除菜品的口味

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

相关推荐
Flittly19 小时前
【SpringAIAlibaba新手村系列】(10)Text to Voice 文本转语音技术
java·spring boot·agent
笑笑先生19 小时前
Proxy 与 Namespace:终结环境与鉴权的噩梦
后端·微服务·架构
hero.fei19 小时前
排查redis出现报错ERR redis temporary failure
数据库·redis·缓存
JoshRen19 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
诸葛大钢铁20 小时前
Java实现Excel文件合并
java·windows·excel
黎明丶之前20 小时前
Spring Cloud Gateway 升级与 Bucket4j 限流实践
java·spring cloud
用户4979323096320 小时前
大模型篇章(1):初识大模型——开启 AI 新时代的钥匙
后端
后海大草鱼20 小时前
PTE考试谁说RS必须全对?Repeat Sentence提分从0到会就看这篇
前端·后端
野犬寒鸦20 小时前
MySQL复习记录Day01
数据库·后端
程序员木圭20 小时前
05-告别逻辑混乱!Java 流程控制让代码学会"判断和循环"
java·后端