• 一个SpringBoot单体项目--》瑞吉外卖项目之后台管理端基础功能开发


    外卖平台

    一 、项目基础实现

    1.1、搭建项目之项目引入静态资源

    关于SpringBoot一体化开发,可以将静态资源(html/css/js等)放在resource文件下,然后编写一个SpringMvc的WebMvcConfig(需要继承一个WebMvcConfigurationSupport,重写一个addResoucehandlers(增加来源处理器)方法)进行响应配置代码的编写;
    demo:
    
    • 1
    • 2
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
    
    @Slf4j
    @Configuration
    public class WebMvcConfig extends WebMvcConfigurationSupport {
        /**
         * 设置静态资源映射
         * @param registry
         */
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            log.info("开始进行静态资源映射...");  	
                  			  registry.addResourceHandler("/backend/**(访问谁)").addResourceLocations("classpath:/backend/(映射到哪里去)");
            registry.addResourceHandler("/front/** 访问谁)").addResourceLocations("classpath:/front/"(映射到哪里去));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.2、项目功能实现 --> 后台用户

    1.2.1 -->后台用户登录逻辑的实现

    1.2.1.1 前后端交互流程

    请添加图片描述

    1.2.1.2 流程说明

    前端发送一个post请求,请求体中携带本次请求应该带有的参数,在后台,经过javaweb提供的过滤器(使用它,需要在启动类中加入@ServletConponentScan),进行访问路径的过滤,然后放行到员工的cotroller层,在cotroller进行账号是否存在,密码是否正确,以及该账号的状态是否已被禁用,然后同一个返回一个结果给前端控制器,前端控制器将数据给前端页面;
    注意:在登录成功后,要记录该登录用户到session域对象中,以备之后对用户信息的回显,判断用户登录状态,登出用户的操作;
    
    • 1
    • 2

    1.2.2–>后台用户登出逻辑的实现

    1.2.2.1 请求流程分析

    前端发一个post请求到后端,后端清理session中保存的用户id,返回结果
    
    • 1

    1.2.3–>后台新增员工逻辑的实现

    1.2.3.1 程序执行流程

    A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
    B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
    C. Service调用Mapper操作数据库,保存数据
    
    • 1
    • 2
    • 3

    1.2.3.2 代码实现流程

    A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
    B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。
    
    • 1
    • 2

    1.2.3.2 该功能实现注意事项

    因为在数据库中对用户名进行了唯一的约束,假设,前台提交的增加用户的用户名,在数据库中已经存在,便会抛出一个Duplicate entry异常;显然,在spring中如果让它抛出了,就不得劲了,此时我们便使用Spring提供的全局异常处理机制,也就是说,所有的异常处理都交给spring,spring对异常进行处理;
    
    • 1
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    import java.sql.SQLIntegrityConstraintViolationException;
    
    /**
     * 全局异常处理
     */
    @ControllerAdvice(annotations = {RestController.class, Controller.class})
    @ResponseBody
    @Slf4j
    public class  GlobalExceptionHandler {
    
        /**
         * 异常处理方法
         * @return
         */
        @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
        public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
            log.error(ex.getMessage());
            if(ex.getMessage().contains("Duplicate entry")){
                String[] split = ex.getMessage().split(" ");
                String msg = split[2] + "已存在";
                return R.error(msg);
            }
            return R.error("未知错误");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    注解说明:
    @ControllerAdvice : 指定拦截那些类型的控制器;
    ​ @ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;

    1.2.4–>后台员工信息的分页查询逻辑的实现

    1.2.4.1 程序执行流程

    A. 点击菜单,打开员工管理页面时,执行查询:
    1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
    2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
    3). Service调用Mapper操作数据库,查询分页数据
    4). Controller将查询到的分页数据, 响应给前端页面
    5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
    B. 搜索栏输入员工姓名,回车,执行查询:
    1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
    2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
    3). Service调用Mapper操作数据库,查询分页数据
    4). Controller将查询到的分页数据, 响应给前端页面
    5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
    最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx

    1.2.4.2 代码实现

    1. 分页查询==》本项目dao层框架使用的是mybatis+mybatispuls==〉在dao层框架中提供了分页插件,如果要使用,只需要进行相应配置与导入即可;
    2. 配置分页插件步骤:创建一个mybatisPlus的配置文件,要加上@Configration注解,然后编写一个返回返回值为mybatisPlusInterceptor(mybatisplus的拦截器)的方法;在方法中new一个mybatisPlusInterceptor,然后调用该对象中addInnerInterceptor(增加内部拦截器方法),该方法的概述为分页内部拦截器:
    demo:
    
    • 1
    • 2
    • 3
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 配置MP的分页插件
     */
    @Configuration
    public class MybatisPlusConfig {
    
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor(){
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return mybatisPlusInterceptor;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    3. cotroller中代码实现逻辑
    	1. 构造分页构造器;new 一个page<>对象
    	2. 构造条件构造器
    	3. 添加过滤条件
    	4. 添加排序条件
    	5. 执行查询
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.2.5–>后台启用/禁用员工逻辑的实现

    1.2.5.1 展示效果

    员工的启用\禁用操作中,在前台两者的操作只能是管理员进行,也就是说只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示,这是在前台代码中对登录的用户进行了一个判断;
    
    • 1

    1.2.5.2 程序执行流程

    1). 页面发送ajax请求,将参数(id、status)提交到服务端
    2). 服务端Controller接收页面提交的数据并调用Service更新数据	
    3). Service调用Mapper操作数据库
    注意:启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
    
    • 1
    • 2
    • 3
    • 4

    1.2.5.3 代码实现后出现的问题

    关于后台查询出来的员工对象的id值为一个19位的Long类型,但是前端的Long类型只能处理16位,此时需要我们将后端发给前台的long类型的数据,提前转为String类型,而当前台提交过来也会将String再转为Long
    具体实现:
    该功能是由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 解决方法是对这个消息转换器进行扩展
    实现步骤:
    1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
    
    • 1
    • 2
    • 3
    • 4
    • 5
    package com.itheima.reggie.common;
    
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
    import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
    import java.math.BigInteger;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    import java.time.format.DateTimeFormatter;
    import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
    
    /**
     * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
     * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
     * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
     */
    public class JacksonObjectMapper extends ObjectMapper {
    
        public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
        public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
        public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    
        public JacksonObjectMapper() {
            super();
            //收到未知属性时不报异常
            this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
    
            //反序列化时,属性不存在的兼容处理
            this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    
    
            SimpleModule simpleModule = new SimpleModule()
                    .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                    .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                    .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
    
                    .addSerializer(BigInteger.class, ToStringSerializer.instance)
                    .addSerializer(Long.class, ToStringSerializer.instance)
                    //long ==》String
                    .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                    .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                    .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
    
            //注册功能模块 例如,可以添加自定义序列化器和反序列化器
            this.registerModule(simpleModule);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
    
    • 1
    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.2.6–>后台编辑员工逻辑的实现

    1.2.6.1 程序执行流程

    1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
    2). 在add.html页面获取url中的参数[员工id]
    3). 发送ajax请求,请求服务端,同时提交员工id参数
    4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面	
    5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
    6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
    7). 服务端接收员工信息,并进行处理,完成后给页面响应
    8). 页面接收到服务端响应信息后进行相应处理
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1.2.6.2 根据ID查询员工

    代码实现:

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
        log.info("根据id查询员工信息...");
        Employee employee = employeeService.getById(id);
        if(employee != null){
            return R.success(employee);
        }
        return R.error("没有查询到对应员工信息");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1.2.6.3 修改员工

    代码实现:

    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());
    
        Long empId = (Long)request.getSession().getAttribute("employee");
    
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(empId);
        employeeService.updateById(employee);
    
        return R.success("员工信息修改成功");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1.3 关于实现MP提供的公共字段填充

    分析:我们可以从数据库中各个表中观察得知,每一张数据表都含有createUser、createTi me、updateUser、updateTime这些公共字段,而在本项目的技术选型中,选用mybatis以及它的最好搭档mybatisplus进行dao层的框架的搭建,而在mybatisplus中提供了公共字段自动填充的功能,通过在实体类中需要填充的字段加上@TableField注解以及编写一个自定义元数据类去实现一个MetaObjectHandler接口,实现其中的方法,该类需要交给spring容器进行管理;在通过元数据对象.setValue去设置公共字段填充内容;
    关于自动填充创建/更新用户ID,子啊自定义元数据类中在通过session与获取userID的自动填充,显然是不现实的,接下来想另外一个方法去进行登录用户的信息的保存与提取—》通过实现得知,当一个请求来到后端—〉首先经过了过滤器,然后经过cotroller ,然后经过元数据处理类,最后在去service,dao层,而这一条数据处理链,都是由一个线程来处理的,也就是说,客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:所以我们可以用这个线程先保存信息,通过thread中threadlocal局部表量,去设置/获取参数;
    ThreadLocal常用方法:
    A. public void set(T value) : 设置当前线程的线程局部变量的值
    B. public T get() : 返回当前线程所对应的线程局部变量的值
    C. public void remove() : 删除当前线程所对应的线程局部变量的值

    1.3.1 基于ThreadLocal封装的工具类

    作用:设置线程中数据与获取线程中数据
    实现:

    package cn.zkwf.takeout.common;
    
    import cn.zkwf.takeout.entity.Employee;
    
    /**
     * @description:线程数据共享工具类
     * @author: Sw_Ljb
     * @PACKAGE_NAME:cn.zkwf.takeout.common
     * @time: 2022/7/21 下午12:53
     * @version: 1.0
     */
    
    public class ThreadContext {
    
        //1、直接new出来这个对象 范性T是参数类型
        private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
    
        /**
         * 设置参数值
         * @param empId
         */
        public static void setParameter(Long empId ){
            threadLocal.set(empId);
        }
    
        /**
         * 得到参数
         * @return
         */
        public static Long getParameter(){
            return threadLocal.get();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    1.3.2 自动填充类

    package cn.zkwf.takeout.common;
    
    import cn.zkwf.takeout.entity.Employee;
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    
    /**
     * @description: 元数据处理类
     * @author: Sw_Ljb
     * @PACKAGE_NAME:cn.zkwf.takeout.common
     * @time: 2022/7/21 下午12:46
     * @version: 1.0
     */
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
    
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("MP进入插入时自动注入元素信息");
            //1、设置自动注入时间
            metaObject.setValue("createTime", LocalDateTime.now());
            metaObject.setValue("updateTime",LocalDateTime.now());
            //2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享
            //2.1 得到线程中的值
            Long empID = ThreadContext.getParameter();
            metaObject.setValue("createUser",empID);
            metaObject.setValue("updateUser",empID);
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("MP进入更新数据时自动注入元素信息");
            //1、设置更新自动注入时间
            metaObject.setValue("updateTime",LocalDateTime.now());
            //2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享
            //2.1 得到线程中的值
            Long empID = ThreadContext.getParameter();
            metaObject.setValue("updateUser",empID);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    1.4 项目后台功能实现 --> 分类管理

    1.4.1 新增分类

    1.4.1.1 程序执行过程

    1). 在页面(backend/page/category/list.html)的新增分类表单中填写数据,点击 “确定” 发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
    2). 服务端Controller接收页面提交的数据并调用Service将数据进行保存
    3). Service调用Mapper操作数据库,保存数据

    1.4.1.1 代码实现

    1. 基础环境的搭建(实体类+mapper(继承basemapper<实体类>)+service(interface IService<实体类>)(serviceimpl(extends ServiceImpl<实体类mapper,实体类> interface service))+controller);
    2. 代码实现流程:前端将数据转为json传递给后端,后端在controller中使用@RequestBody注解 使用实体类去接受,然后直接调用service直接操作;
    
    • 1
    • 2

    1.4.2 分类信息的分页查询

    1.4.2.1 程序执行过程

    1). 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端

    2). 服务端Controller接收页面提交的数据并调用Service查询数据

    3). Service调用Mapper操作数据库,查询分页数据

    4). Controller将查询到的分页数据响应给页面

    5). 页面接收到分页数据并通过ElementUI的Table组件展示到页面上

    1.4.2.2 代码实现

    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize){
        //分页构造器
        Page<Category> pageInfo = new Page<>(page,pageSize);
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);
    
        //分页查询
        categoryService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.4.3 删除分类

    1.4.3.1 执行流程

    1). 点击删除,页面发送ajax请求,将参数(id)提交到服务端

    2). 服务端Controller接收页面提交的数据并调用Service删除数据

    3). Service调用Mapper操作数据库

    1.4.3.2 代码实现

    分析:在执行删除分类操作时,删除逻辑不能时直接将分类,而是应该先检查当前分类下是否关联了菜品或者套餐,然后将这个分类的id拿着,去菜品或者套餐中查询是否含有该分类的数据,如果有抛一个自定义异常,如果没有,则可以删除该分类;
    实现步骤:

    1. 创建自定义异常
    package cn.zkwf.takeout.exception;
    
    /**
     * @description:
     * @author: Sw_Ljb
     * @PACKAGE_NAME:cn.zkwf.takeout.exception
     * @time: 2022/7/21 下午2:05
     * @version: 1.0
     */
    
    public class CostomExcetion extends RuntimeException{
    
        public CostomExcetion(String message) {
            super(message);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 在全局异常处理器中将自定义异常加入
      全局异常处理器:
    package cn.zkwf.takeout.common;
    
    import cn.zkwf.takeout.exception.CostomExcetion;
    import cn.zkwf.takeout.resulttype.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.sql.SQLIntegrityConstraintViolationException;
    
    /**
     * @description:
     * @author: Sw_Ljb
     * @PACKAGE_NAME:cn.zkwf.takeout.common
     * @time: 2022/7/20 下午1:53
     * @version: 1.0
     */
    @Slf4j
    @ControllerAdvice(annotations = {RestController.class, Controller.class})
    @ResponseBody
    public class GlobalExceptionHandler {
    
    
        @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
        public R<String>  exceptionHandler(SQLIntegrityConstraintViolationException ex){
            //1、打印日志
            log.info("自定义全局异常处理正在运行!!");
            //Duplicate entry 'zhangsan' for key 'employee.idx_username'
            if (ex.getMessage().contains("Duplicate entry ")){
                //2、判断当前抛出的异常是否为Duplicate entry
                String[] strArray = ex.getMessage().split(" ");
                //3、截取异常信息中重点错误信息
                String msg= strArray[2] +"已经存在了!!";
                //4、设置返回信息
                return R.error(msg);
    
            }
            //4、设置返回信息
            return R.error("未知错误!");
        }
    
    
        @ExceptionHandler(CostomExcetion.class)
        public R<String>  costomerExceptionHandler(CostomExcetion ex){
            //1、打印日志
            log.info("自定义全局异常处理正在运行!!==>自定义异常");
            return R.error(ex.getMessage());
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    1. 扩展分类service,实现该方法
      CategoryServiceImpl.remove():
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;
    
    /**
     * 根据id删除分类,删除之前需要进行判断
     * @param id
     */
    @Override
    public void remove(Long id) {
        //添加查询条件,根据分类id进行查询菜品数据
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        //如果已经关联,抛出一个业务异常
        if(count1 > 0){
            throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
        }
    
        //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if(count2 > 0){
            throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
        }
    
        //正常删除分类
        super.removeById(id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    1.4.4 修改分类

    1.4.4.1 分析

    当前端点"修改"按钮的时候,当信息被修改后,点击保存按钮,页面在发送一个请求,去修改分类信息
    
    • 1

    1.4.4.1 实现

    回显:回显这一步的操作前端已经实现
    
    • 1

    请添加图片描述
    修改:

    /**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Category category){
        log.info("修改分类信息:{}",category);
        categoryService.updateById(category);
        return R.success("修改分类信息成功");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1.5 项目后台功能实现 --> 菜品管理

    1.5.1 关于菜品管理 需要上传菜品图片 ==》涉及文件上传与下载

    文件上传三要素:

    表单属性取值说明
    methodpost必须选择post方式提交
    enctypemultipart/form-data采用multipart格式上传文件
    typefile使用input的file控件上传

    文件下载两种表现形式

    1. 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
    2. 直接在浏览器中打开,通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

    上传逻辑:

    1). 获取文件的原始文件名, 通过原始文件名获取文件后缀

    2). 通过UUID重新声明文件名, 文件名称重复造成文件覆盖

    3). 创建文件存放目录

    4). 将上传的临时文件转存到指定位置

    下载逻辑:

    1). 定义输入流,通过输入流读取文件内容

    2). 通过response对象,获取到输出流

    3). 通过response对象设置响应数据格式(image/jpeg)

    4). 通过输入流读取文件数据,然后通过上述的输出流写回浏览器

    5). 关闭资源

    具体实现:

    package cn.zkwf.takeout.controller;
    
    import cn.zkwf.takeout.resulttype.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletResponse;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.util.UUID;
    
    /**
     * @description:
     * http://localhost:7070/common/upload
     * Request Method: POST
     * @author: Sw_Ljb
     * @PACKAGE_NAME:cn.zkwf.takeout.controller
     * @time: 2022/7/22 下午2:54
     * @version: 1.0
     */
    @Slf4j
    @RestController
    @RequestMapping("/common")
    public class FileController {
    
        @Value(value = "${serverpath.serverImpPath}")
        private String serverImpPath;
    
        //Content-Disposition: form-data; name="file"; filename="0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"
        @RequestMapping("/upload")
        public R<String> upload(MultipartFile file){
            log.info("上传路径{}:",serverImpPath);
    
            //更保险的做法 将上传过来的照片再次创建一个uuid
            String suffdex = file.getOriginalFilename().substring(file.getOriginalFilename().indexOf('.'));
            String imgName = UUID.randomUUID().toString()+suffdex;
    
            try {
                //上传路径/Users/mac/Desktop/fliedemo/takeoutServerImg保险期间对目录进行一次判断
                File imgfile = new File(serverImpPath);
                if (!imgfile.exists()){
                    imgfile.mkdirs();
                }
                file.transferTo(new File(serverImpPath+"/"+imgName));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return R.success(imgName);
        }
        /**
         * 下载数据
         * @param response
         * @param name
         */
        @GetMapping("/download")
        public void download(HttpServletResponse response, String name){
            log.info("下载文件{}",name);
            try {
                //输入流
                FileInputStream fis = new FileInputStream(new File(serverImpPath+"/"+name));
                log.info(serverImpPath+"/"+name);
                //输出流
                ServletOutputStream outputStream = response.getOutputStream();
                //先读出来 再响应给前台
    
                //设置响应类型
                response.setContentType("image/jpeg");
    
                //1、定义一个读出来存的字节数组
                byte[] bytes = new byte[1024];
                //2、定一个读多少
                int len= 0;
                while ((len = fis.read(bytes))!=-1){
                    //3、写会前台
                    outputStream.write(bytes,0,len);
                    outputStream.flush();
                }
                //4、记得关流,释放资源
                outputStream.close();
                fis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    1.5.2 菜品新增

    分析:新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:dish/dish_flavor

    1.5.2.1 交互流程

    1). 点击新建菜品按钮, 访问页面(backend/page/food/add.html), 页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
    2). 页面发送请求进行图片上传,请求服务端将图片保存到服务器(上传功能已实现)
    3). 页面发送请求进行图片下载,将上传的图片进行回显(下载功能已实现)
    4). 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
    对于前端传递到服务端的数据分析:
    如果使用菜品类Dish来封装,只能封装菜品的基本属性,flavors属性是无法封装的。这个时候,我们需要自定义一个实体类,然后继承自 Dish,并对Dish的属性进行拓展,增加 flavors 集合属性(内部封装DishFlavor)。

    各种类型的实体模型

    请添加图片描述

    1.5.2.2 代码实现

    页面传递的菜品口味信息,仅仅包含name 和 value属性,缺少一个非常重要的属性dishId, 所以在保存完菜品的基本信息后,我们需要获取到菜品ID,然后为菜品口味对象属性dishId赋值。
    具体逻辑如下:
    ①. 保存菜品基本信息 ;
    ②. 获取保存的菜品ID ;
    ③. 获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值;
    ④. 批量保存菜品口味列表;
    注意:由于需要进行了两次数据库的保存操作,操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解 @Transactional来控制事务。Service层方法上加的注解@Transactional要想生效,需要在引导类上加上注解 @EnableTransactionManagement, 开启对事务的支持。
    代码实现:

    @Autowired
    private DishFlavorService dishFlavorService;
    /**
    * 新增菜品,同时保存对应的口味数据
    * @param dishDto
    */
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);
    	
        Long dishId = dishDto.getId();//菜品id
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
    
        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.5.3 菜品分页查询

    1.5.3.1 需求分析

    系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
    请添加图片描述
    在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、更新时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

    1.5.3.2 前端和服务端交互过程

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

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

    1.5.3.3 代码实现

    分析:
    实体类 Dish 中,仅仅包含 categoryId, 不包含 categoryName,这里我们可以返回DishDto对象,在该对象中我们可以拓展一个属性 categoryName,来封装菜品分类名称。
    具体逻辑为:
    1). 构造分页条件对象
    2). 构建查询及排序条件
    3). 执行分页条件查询
    4). 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称
    5). 封装数据并返回
    实现:

    /**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        //构造分页构造器对象
        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::getUpdateTime);
    
        //执行分页查询
        dishService.page(pageInfo,queryWrapper);
    
        //对象拷贝
        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
        List<Dish> records = pageInfo.getRecords();
        List<DishDto> list = records.stream().map((item) -> {
            
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);
            Long categoryId = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);
            
            if(category != null){
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());
        dishDtoPage.setRecords(list);
        
        return R.success(dishDtoPage);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

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

    1.5.3 菜品修改

    1.5.3.1 需求分析

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

    1.5.3.2 交互流程

    1). 点击菜品列表的中的修改按钮,携带菜品id跳转至add.html
    2). 进入add.html,页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
    3). add.html获取id, 发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
    4). 页面发送请求,请求服务端进行图片下载,用于页图片回显
    5). 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
    我们只需要在这里实现两个功能即可:
    1). 根据ID查询菜品及菜品口味信息
    2). 修改菜品及菜品口味信息

    1.5.3.3 功能实现

    根据ID查询菜品信息
    具体逻辑为: 
    A. 根据ID查询菜品的基本信息 
    B. 根据菜品的ID查询菜品口味列表数据
    C. 组装数据并返回
    
    • 1
    • 2
    • 3
    • 4
    /**
    * 根据id查询菜品信息和对应的口味信息
    * @param id
    * @return
    */
    public DishDto getByIdWithFlavor(Long id) {
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);
    	
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);
    	
        //查询当前菜品对应的口味信息,从dish_flavor表查询
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
        dishDto.setFlavors(flavors);
    	
        return dishDto;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    修改菜品信息
    实现步骤:
    点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端。在修改菜品信息时需要注意,除了要更新dish菜品表,还需要更新dish_flavor菜品口味表;在该方法中,我们既需要更新dish菜品基本信息表,还需要更新dish_flavor菜品口味表。而页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢,其实,无论菜品口味信息如何变化,我们只需要保持一个原则: 先删除,后添加。
    
    • 1
    • 2
    @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.6 项目后台功能实现 --> 套餐管理

    1.6.1 --> 新增套餐

    1.6.1.1 需求分析

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

    1.6.1.2 数据模型

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

    1.6.1.3 前端页面与服务端的交互过程

    1). 点击新建套餐按钮,访问页面(backend/page/combo/add.html),页面加载发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
    2). 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
    3). 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
    4). 页面发送请求进行图片上传,请求服务端将图片保存到服务器
    5). 页面发送请求进行图片下载,将上传的图片进行回显
    6). 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
    分析发送的请求有5个,分别是:
    A. 根据传递的参数,查询套餐分类列表
    B. 根据传递的参数,查询菜品分类列表
    C. 图片上传
    D. 图片下载展示
    E. 根据菜品分类ID,查询菜品列表
    F. 保存套餐信息

    1.6.1.4 代码开发

    根据分类查询菜品
    /**
    * 根据条件查询对应的菜品数据
    * @param dish
    * @return
    */
    @GetMapping("/list")
    public R<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 R.success(list);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    保存套餐

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.6.2 --> 套餐的分页查询

    1.6.2.1 前端页面和服务端的交互过程

    1). 访问页面(backend/page/combo/list.html),页面加载时,会自动发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
    2). 在列表渲染展示时,页面发送请求,请求服务端进行图片下载,用于页面图片展示

    1.6.2.2 代码开发

    基本信息查询

    基本逻辑:
    1). 构建分页条件对象
    2). 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序
    3). 执行分页查询
    4). 由于查询套餐信息时, 只包含套餐的基本信息, 并不包含套餐的分类名称, 所以在这里查询到套餐的基本信息后, 还需要根据分类ID(categoryId), 查询套餐分类名称(categoryName),并最终将套餐的基本信息及分类名称信息封装到SetmealDto。组装数据并返回
    代码实现:

    /**
    * 套餐分页查询
    * @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    1.6.3 --> 删除套餐

    1.6.3.1 --> 需求分析

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

    1.6.3.2 --> 交互过程

    1). 点击删除, 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
    2). 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
    分析:开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,一次请求为根据ID删除,一次请求为根据ID批量删除。观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

    1.6.3.3 --> 代码实现

    分析:删除套餐时, 我们不仅要删除套餐, 还要删除套餐与菜品的关联关系。
    实现: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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
  • 相关阅读:
    【Golang】gin框架入门
    【javaEE】多线程初阶(Part8线程池!!!)【重要】
    springboot中实现生成验证码和登录校验功能
    接口自动化测试框架搭建【附教程加源码】
    KT148A语音芯片下载过程的问题集锦 包含下载不了批量生产的说明
    Python 虚拟环境管理工具(详细)
    区块链(12):java区块链项目之集群部署
    笔耕不辍 -- 继续前行
    Java重写与重载
    论文字体,Word字体大小对照换算表(字号、磅、英寸、像素)
  • 原文地址:https://blog.csdn.net/ljbnb/article/details/126008758