基于SpringBoot+MybatisPlus开发的外卖管理项目

reggie(瑞吉外卖)项目过程总结

记,第一次跟黑马视频学做项目的过程笔记。

仅记录,不做任何责任声明。

我坚信进步的过程就是一次一次试错的结果。

随时会修改,因为我们都随时在进步。

说不定以后你就是那个大神!说的不是我,是你!
同时也希望有前辈能给些意见~

一、项目创建

1、创建SpringBoot项目,(maven)

  • SprinBoot版本选择3.0以下的,3.0以上需要JDK17

  • JDK版本统一1.8

  • 勾选组件,web、SQL驱动、lombok

  • Pom.xml中导入自定义坐标:

    • <!--MybatisPlus-->
      <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus-boot-starter</artifactId>
                  <version>3.5.3.1</version>
              </dependency>
      <!--JSON格式转换工具-->
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>fastjson</artifactId>
                  <version>2.0.21</version>
              </dependency>
      <!--跟java.lang这个包的作用类似,Commons Lang这一组API也是提供一些基础的、通用的操作和处理-->
              <dependency>
                  <groupId>commons-lang</groupId>
                  <artifactId>commons-lang</artifactId>
                  <version>2.6</version>
              </dependency>
      <!--druid连接池-->
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid-spring-boot-starter</artifactId>
                  <version>1.1.23</version>
              </dependency>
      
  • 修改application.properties文件的格式为yml,并添加相关配置

    • server:
        port: 8080
      spring:
        application:
          name: reggie_take_out
        datasource:
          druid:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
            username: root
            password: jasonzhang1511
      mybatis-plus:
        configuration:
          #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
          map-underscore-to-camel-case: true
          log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        global-config:
          db-config:
            id-type: ASSIGN_ID  #定义生成ID的类型 雪花算法
            
      
  • 创建要用的的包在com.jason

    • 实体类:domain或entity
    • 数据层:dao或mapper
    • 服务层:service及其实现类层impl
    • 控制层:controller
    • 后续可能会出现的包:通用层:common/异常处理/结果封装过滤层:filter/过滤器配置层:config/静态资源映射等...

二、登陆功能-前后端协调-细节处理

是否需要静态资源映射?

spring扫描静态资源,默认是在resources/statics,

如果开发时用的是别的静态资源包,则需要进行配置

新建一个config文件夹,

创建WebMvcConfig类,

继承WebMvcConfigurationSupport

重写addResourceHandlers(ResourceHandlerRegistry registry)方法

protected void addResourceHandlers (ResourceHandlerRegistry registry) {
//访问/backend包下的任何资源时,重定向到/backend去,可以重设静态资源包 		
  registry.addResourceHandler("/backend/**")
  .addResourceLocations("classpath:/backend/");
    }

1、编写domain、dao、service

  1. domain/entity: 使用@Data注解,让lombok自动生成一些列方法。实体类实现Serializable接口id为Long型,因为MP生成的id是雪花算法生成,若要跟随数据库自增,可以设置id的属性为AUTO

  2. dao/mapper: 接口要继承MP的BaseMapper<实体类>,并且添加类注解@Mapper

  3. service&impl:

    • service: 是一个接口,继承自MP提供的IService<实体类>

    • ServiceImpl: 继承自MP提供的ServiceImp<实体类Mapper,实体类>,同时也要实现Service接口加上类注解@Service

      public interface EmployeeService extends IService<Employee> {}
      
      @Service
      public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService {}
      

2、编写Controller

  1. 根据前端页面反馈或规定,确定编写controller的信息:

    • 请求的路径
    • 请求的形式:Get/Post/Put/Delete…
    • 返回形式以及参数:一般返回的都是前后端协调后的协议,common 包下的R【R类中规定了后端处理完成之后需要返回给前端的信息( 属性: code,msg,data,map; 方法: success,error,add),前端接到返回的R,再按照格式去取其中需要的信息即可】
  2. Controller中注意的事项

    1. @RestController使用Rest风格。

    2. @RequestMapping("/employee")针对请求路径进行映射

    3. 内部调用@AutoWired ...employeeService 进行方法调用。

    4. 接收到/login请求,携带来JSON数据,用@RequstBody Employee employee来接收,同时也需要一个请求参数HttpServletRequest request用来存储session

      • 对密码进行MD5加密处理

        //1 对密码进行MD5加密处理
                String password = employee.getPassword();
                password = DigestUtils.md5DigestAsHex(password.getBytes());
        
      • 根据请求对象的username进行查询数据库,并返回一个对象

      • 判断入返回的对象为空,则用户不存在,return R.error

      • 对象不为空,判断对象的密码与输入的密码是否相同,若不同,return R.error

      • 用户名和密码都匹配成功需要判断用户的状态是否为1,即启用。否则 return R.error

      • 经过上述三级判断,用户登录成功,将用户的唯一标识id存储为到session中,

      • return R.success(查询出的对象);如下

      @PostMapping("/login")
      public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
      				//1 对密码进行MD5加密处理
              String password = employee.getPassword();
              password = DigestUtils.md5DigestAsHex(password.getBytes());
      
              //2 根据提交的用户名进行查询
              LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(Employee::getUsername,employee.getUsername());
              Employee one = employeeService.getOne(queryWrapper);
              //3 若没有查询到 则return失败的结果
              if (one == null){
                  return R.error("登陆失败,用户不存在。");
              }
              //4 密码比对,如果不一致则返回登陆失败的结果
              if (!one.getPassword().equals(password)){
                  return R.error("登录失败,密码错误。");
              }
              //5 查看员工状态status是否是禁用
              if (one.getStatus()!=1){
                 return R.error("登录失败,用户已被禁用。");
              }
              //6 登陆成功,将员工id存入session并返回登陆成功结果
              request.getSession().setAttribute("employee",one.getId());
              //返回成功信息 放入查出来的对象
              return R.success(one);
      }
      

3、Servlet过滤器Filter实现登陆拦截

  1. 在包filter下创建拦截器类LoginFilter,实现Filter接口,重写doFilter()方法,内有三个参数:

    • ServletRequest servletRequest:一般使用HttpServletRequest,进行强转再使用。
    • ServletResponse servletResponse:一般使用HttpServletResponse,进行强转再使用。
    • FilterChain filterChain:控制是否放行。
  2. 类上注解@webFilter(filterName = "loginCheckFilter" ,urlPatterns = "/*"),指定拦截的路径为所有。同时在SpringBoot启动类上也要注解@ServletComponentScan进行扫描有@WebFilter注解的类

  3. 具体拦截策略:

    1. 通过 request.getRequestURI() 获取请求的URI;

    2. 定义一个String[] 数组用来存放静态资源路径和登陆注册相关的请求路径;

    3. 通过AntPathMatcherPATH_MATCHER属性的方法match来匹配请求的URI是否是需要放行的路径。

      public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
      //路径匹配封装为一个方法check
      public boolean check(String[] urls, String requestURI){
              for (String url : urls) {
                  boolean match = PATH_MATCHER.match(url, requestURI);
                  if (match){
                      return true;
                  }
              }
              return false;
          }
      
    4. 若匹配到的是放行的资源,进行放行:

      //  若匹配到的是放行的资源 放行
      if (check){
          filterChain.doFilter(request,response);
          return; //若放行 后面代码无需执行 直接return
      }
      
    5. 若时已登陆的状态,放行

      //  若是已登陆的状态 放行
      if (request.getSession().getAttribute("employee")!=null){
          filterChain.doFilter(request,response);
          return; //若放行 后面代码无需执行 直接return
      }
      
    6. 经上方判定,不是放行的资源,且为未登陆,返回数据:

      //  返回未登陆的结果 通过输出流的方式 向客户端页面响应JSON数据,客户端通过ajax处理返回的数据,进行重定向。
          response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
      

4、退出

清除session即可。

@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
    request.getSession().removeAttribute("employee");
    return R.success("退出成功");
}

三、员工管理业务开发

1、分页查询

  1. 客户端发送的请求时分页查询/employee/page,并携带了两个参数,page和pageSize

  2. 查阅前端页面可以发现要求返回的数据是 this.data.records 和 total 这些参数都是MP的Page<T>类下的属性 直接使用即可。

  3. 当然使用分页查询,我们需要配置MP的分页插件,添加分页内部拦截器,并交给spring进行管理。

    /**
     *配置MP的分页插件
     */
    @Configuration //配置类统一注解
    public class MybatisPlusConfig {
        @Bean //交给spring管理
        public MybatisPlusInterceptor mybatisPlusInterceptor(){
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
            //添加分页内部拦截器
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return mybatisPlusInterceptor;
        }
    }
    
  4. 上面配置好之后,就可以直接使用MP提供的page方法。

@GetMapping("/page") //参数直接携带过来,不是JSON,不用注解。但是参数和变量名一定要写一样。
public R<Page> page(int page,int pageSize,String name){
    //构造分页构造器,两个参数为前端页面传过来的当前页和每页展示条数
    Page<Employee> pageInfo = new Page(page,pageSize);
    //构造条件构造器,通过条件构造器,可以为sql语句添加很多条件
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件 name 若name为空则无需执行该语句
    queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
    //添加排序条件 通过创建时间排序
    queryWrapper.orderByDesc(Employee::getCreateTime);
    //执行查询,并封装给pageInfo对象,所以直接返回经过操作的 pageInfo对象即可。
    employeeService.page(pageInfo, queryWrapper);
    return R.success(pageInfo);
}

2、启用禁用

该功能只有管理员才可以编辑,其他用户登录后不会显示此操作按钮

前端代码已经实现该功能,我们只需要实现后端相关的操作即可

  • 确定前端请求的路径/employee;以及请求方式Put
  • 确定前端请求携带的参数 Employee
  • 确定需要返回的类型R.success("修改成功")

–> 在EmployeeController中编写update方法,还额外需要一个HttpServletRequest request 参数,用来获取操作者的信息,并存储操作记录(在Employee中有更新操作用户和更新时间需要每次都设定)

@PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee ){
        Long employee1 = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(employee1);//设置操作修改的人
        employee.setUpdateTime(LocalDateTime.now());//设置修改信息的时间
        employeeService.updateById(employee);
        return R.success("修改成功。");
    }
  • 但是在经过测试后,提示修改成功,但是数据库中的操作对象的status属性并没有改变

    发现前端发送的id丢失了精度

    (Long型19位,js中只保证16位,导致后三位精度丢失

  • 需要在common包添加一个对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象

  • 即将Long型的ID序列化为JSON形式,在传输数据时两端进行序列化与反序列化。

  • 同时也可以对日期和时间类型的属性进行统一格式处理。

    /**
     * 对象映射器:基于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型转为字符串,方式数据精度丢失。
                    .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);
        }
    }
    
  • 在配置包config下的WebMvcConfig配置类中扩展mvc框架的消息转换器,重写方法extendMessageConverters,将上述编写的随想映射器加载进去排在第一位即可。

    /**
         * 扩展mvc框架的消息转换器
         * @param converters
         */
        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            log.info("启动消息转换器...");
            //创建消息转换器对象
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            //设置对象转换器,底层使用jackson将java转为json
            messageConverter.setObjectMapper(new JacksonObjectMapper());
            //将上面的创建的消息转换器追加到mvc框架的转换器集合中,指定追加的未知位第一位,index=0
            converters.add(0,messageConverter);
        }
    

3、编辑信息

  • 点击编辑按钮时,需要对员工信息进行回显,调用根据id查询数据的方法。

  • 分析请求路径/employee/{id};请求方式Get

  • 在EmployeeController中编写getById()的方法。

    /**
         * 根据id查询员工信息 进行回显
         * @param id
         * @return
         */
        @GetMapping("/{id}")//占位,与参数id进行映射
        public R<Employee> getById(@PathVariable Long id){
          //参数从路径传来,注意注解@PathVariable 
            Employee emp = employeeService.getById(id);
            if (emp == null){
                return R.error("操作失败,未查询到...");
            }
            return R.success(emp);
        }
    
  • 测试通过,另外编写信息后,点击确定时的方法不用再写了,上方update已经完成了。

针对公共字段的代码优化

1、公共字段自动填充

在项目中我们发现,几乎每一个数据表中都有相同的字段:

创建时间,创建人,修改时间,修改人

他们被称为公共字段,MybaitsPlus提供了对公共字段自动填充的处理,分为以下两步:

  1. 在实体类的属性上添加@TableField注解,指定自动填充策略

    		//创建时间
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
        //更新时间
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private LocalDateTime updateTime;
    
  2. 根据MP框架的要求,编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口,用来管理自动填充公共字段的类。并注解`@Component,交给spring去管理。

  3. 重写两个方法:插入:insertFill()更新:updateFill(),使用参数metaObjectsetValue方法,第一个参数为“属性名”第二个参数为要设置的值,如下:

    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
        /**
         * 执行插入/新增时 自动填充
         * 调用工具类 获取在过滤器中判定为登陆的用户的id
         * @param metaObject
         */
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("插入时执行的自动填充...{}",metaObject);
            metaObject.setValue("createTime", LocalDateTime.now());
            metaObject.setValue("updateTime", LocalDateTime.now());
            metaObject.setValue("createUser", BaseContext.getCurrentId());
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
        /**
         * 执行更新/修改时 自动填充
         * 调用工具类 获取在过滤器中判定为登陆的用户的id
         * @param metaObject
         */
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("更新时执行的自动填充...{}",metaObject);
            metaObject.setValue("updateTime", LocalDateTime.now());
            metaObject.setValue("updateUser", BaseContext.getCurrentId());
        }
    }
    
  4. 关于第3步中获取操作用户id的问题详解:

    1. 在公共字段设置操作人和创建人时,我们需要动态获取当前登陆用户的 id。但是我们写的类和继承的类中并没有HttpSession可以调取,这时候需要用到ThreadLocal类,它是JDK中提供的一个类。

      在客户端每次发送http请求时,对应的在服务端都会重新创建一个线程来处理。

      在一个线程的处理过程中,涉及到的下面的方法都属于一个线程中:

      LoginCheckFilter中的doFilter方法

      EmployeeController中的update方法

      MyMetaObjectHandler中的updateFill方法

      在三个方法中都打印Thread.currentThread().getId(),会发现是一样的。

      这样就证明了在一次请求中的线程id是相同的。

      所以:我们可以在登陆请求时就将操作人的id存入Thread中。

      这样在线程中随时可以获得操作人的id。

    2. 我们将id存储到Thread中的操作,放在LoginCheckFilter中的doFilter方法中判断登陆成功时执行,注意存储的id是Long型的。

       // 4 若是已登陆的状态 放行
              if (request.getSession().getAttribute("employee")!=null){
                  log.info("监测到已登陆,id为:{}",request.getSession().getAttribute("employee"));
      
                  //调用我们基于ThreadLocal的工具类,将用户id存储到线程中,方便在自动填充时取出用户id
                  Long employeeId = (Long) request.getSession().getAttribute("employee");
                  BaseContext.setCurrentId(employeeId);
      
                  filterChain.doFilter(request,response); //放行
                  return; //若放行 后面代码无需执行 直接return
              }
      
    3. 在上一步之前,我们要创建一个基于ThreadLocal封装的工具类BaseContext,一次请求会有一个Thread,不会混淆。在其中创建get和set方法来保存和去除操作者的id:(在不同的地方调用id,可以直接通过此工具类来调用,无需再重新创建ThreadLocal)

      /**
       * 基于ThreadLocal封装的工具类,用户保存和获取当前用户的id
       * 一次请求一个thread 不会混淆
       */
      public class BaseContext {
          public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
          /**
           * 设置id值,用于在登陆成功后保存操作者的id
           * @param id
           */
          public static void setCurrentId(Long id){
              threadLocal.set(id);
          }
          /**
           * 获取id,用于在元数据对象拦截填充时,获取操作者的id
           * @return
           */
          public static Long getCurrentId(){
              return threadLocal.get();
          }
      }
      

四、分类管理业务开发

1、编写分类Category相关的domain、dao、service、serviceImpl包和类。

其中注意domain中的公共字段要设置@TableFiled(fill= FiledFil.INSERT)

dao中要加类注解@Mapper;serviceImpl中类注解@Service

2、编写controller及方法:新增(菜品分类、套餐分类)、分页、删除、编辑等操作。

过于简单,省略过程。

需要注意的是:若客户端发送请求的参数,不需要转换,则方法不需要加@RequestBody

在分页请求中客户端携带的参数是page:1 pageSize:10,删除功能中参数id=112311213...

若参数是JSON格式,则要加注解,将传来的参数转换为对应的对象即可。

//客户端传来的参数就是id,直接接收即可,分页查询中也是不需要加参数注解,请求携带的参数就是
@DeleteMapping
public R<String> deleteById(Long id){
    categoryService.remove(id);
    return R.success("删除成功");
}

编辑中的回显功能,在前端html中已经添加了,我们只需要编写一个修改的方法即可。

3、删除操作需要注意的相关问题:

因为本页面是分类管理,每个分类下面会关联很多菜品或者套餐,删除时要判断是否关联?不删除,提示失败 ; 删除,提示成功

为此,我们需要在service中编写remove方法,虽然MP中也有,但是并不满足我们的要求,我们需要在之前加入判断。

在新写的remove方法中,先进行条件判断,最后再调用MP的removeById方法。

要进行判断,我们就需要创建判断的两个类,菜品Dish和套餐Setmeal与数据库对应;

并编写对应的mapper(dao)、service、serviceImpl,

之后在分类的服务层CategoryService和CategoryServiceImpl中注入上面的两个类,

编写remove方法,最后调用MP的removeById即可。

4、CategoryServiceImpl中的方法编写

@Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    public void remove(Long id){
        LambdaQueryWrapper<Dish> dishWrapper = new LambdaQueryWrapper<>();
        dishWrapper.eq(Dish::getCategoryId,id);//用id与Dish中的categoryId进行匹配
        long count1 = dishService.count(dishWrapper);
        if (count1>0){
            //删除的菜品分类或套餐分类中关联了菜品,删除失败,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,删除失败!");
        }
        LambdaQueryWrapper<Setmeal> setmealWrapper = new LambdaQueryWrapper<>();
        setmealWrapper.eq(Setmeal::getCategoryId,id);
        long count2 = setmealService.count(setmealWrapper);
        if (count2>0){
            //删除的菜品分类或套餐分类中关联了套餐,删除失败,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,删除失败!");
        }
        //上面两个条件都不成立时 就执行MP提供的ServiceImpl的removeById方法,执行删除。
        super.removeById(id);
    }

五、菜品管理业务开发

0、上传和下载

经查看前端代码得知,上传和下载的请求路径为/commom下的/update/download

**注意:**参数的传递,上传的是文件,使用的参数只能用MultipartFile file,这里的file名字必须与页面传的参数名字一致。

  • 参数接收到的file是一个临时文件,会随着程序的接收而自动删除。

  • 这里需要将file转存到自己的文件夹下。等到浏览器下载进行回显的时候,从同样的文件夹去下载。

  • 转存时需要给文件重新设置一个文件名,但是文件类型后缀不能更改。防止以后上传的数据多的时候,存在重名的情况。

  • 增强代码灵活性,我们使用动态命名的方式。在yml配置文件中,配置一个路径:reggie: path:/Volumes/酷炫的u盘/学习相关/上传图片转存位置 ,在类中我们可以自动装配在一个属性pathName,${reggie.path}

    		//pathName在yml配置文件中进行配置 用@Value取值即可。
        @Value("${reggie.path}")
        private String pathName;
    
    		@PostMapping("/upload")
        public R<String> upload(MultipartFile file) {
            log.info("上传的文件:" + file);
            //注意:此时的file是一个临时文件,随着程序的结束而自动删除。
            // 所以我们需要将其存储在一个文件夹下,方便后续下载到浏览器进行展示。
    
            //获取原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀.jpg(含'.')
            String substring = originalFilename.substring(originalFilename.lastIndexOf("."));
            //使用uuid随机生成新的文件名,防止以后上传文件出现重复名字。
            String filename = UUID.randomUUID().toString() + substring;
            //防止目标目录不存在,可以添加一次判断
    //        File file1 = new File("/Volumes/酷炫的u盘/学习相关/上传图片转存位置");
    //        if (!file1.exists()) {
    //            file1.mkdirs();
    //        }
            try {
                //将临时文件转存到指定位置 使用动态命名。
                file.transferTo(new File(pathName + filename));
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            //返回的是存储的文件名!返回给表单之后,保存时会将文件名保存在数据库。
            return R.success(filename);
        }
    

下载时,页面传来的是文件名,我们可以根据pathName+文件名的形式,通过输入流,读取到文件,再通过response的输出流将文件返回给页面。

		@GetMapping("/download")
    public void download(String name, HttpServletResponse response){

        try {
            //输入流,通过输入流来读取文件内容
            FileInputStream inputStream = new FileInputStream(pathName + name);

            //输出流,通过输出流将文件写回到浏览器,展示图片
            //注意,这里的输出流是要向浏览器写回数据,所以要通过response来获取
            ServletOutputStream outputStream = response.getOutputStream();

            //因为返回的是图片,所以要在写回之前设置格式,
            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = inputStream.read(bytes))!= -1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            //关闭并释放资源
            outputStream.close();
            inputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

1、新增菜品及DTO开发

DTO,data transform object,数据传输对象。

在新增菜品中,还会有分类以及口味信息,所以,一项功能中,将会操作多张表。

但是单单Dish的实体类对象的属性无法满足数据的接收。

这时再创建一个DishDto,包含所有的属性,继承于Dish,再进行操作。

  • 请求路径/dish
  • 请求携带的参数JSON格式的除了dish的属性,还有category和flavor属性,所以这里要使用dto接收。
  • dishService和impl中重写一个saveWithFlavor方法。如下:
@Transactional //涉及多张表的操作,需要开启事务
    public void saveWithFlavor(DishDto dishDto) {
        //先存储dish的基本信息到菜品表dish,因为dishDto继承自dish,所以可以直接添加。
        this.save(dishDto);
        //获得菜品id
        Long id = dishDto.getId();
        //处理id信息,保存到集合中的每个dishFlavor的dishId属性
        //使用流的lambda表达式,对flavors集合中的每个元素进行操作,操作完后又赋给它自己。
        //也可以使用forEach方法。
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
                    item.setDishId(id);
                    return item;
                }).collect(Collectors.toList());
        //保存菜品口味数据到dish_flavor表
        dishFlavorService.saveBatch(flavors);
    }
  • 注意:涉及多张表的时候,记得加上事务@Transactional,在springboot启动类上添加事务管理的支持@EnableTransactionManagement,最后在DishController中进行调用即可。

2、分页查询

  • MP相关的分页插件,在做员工管理时已经做完了,整体方法于员工管理和分类管理差不多,但是,要注意这里要操作的表多,需要使用Dto对象。
  • 创建两个Page对象:Page<Dish> dishPagePage<DishDto> dishDtoPage
  • 进行条件查询后,将dishPage对象copy给dishDtoPage中,但是不用copyrecords属性,因为records属性中还缺少一个字段categoryName,我们需要操作之后再加上。

注意:前端携带的参数要使用Dto接收,否则获得的数据不全,页面的分类名称无法获得

		@GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name) {

        Page<Dish> pageInfo = new Page<>(page, pageSize);
        //前端需要的信息中还有一个分类名称 dish中没有 dishDto中刚好有
        Page<DishDto> dishDtoPage = new Page<>();
        //添加过滤条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件
        queryWrapper.orderByDesc(Dish::getUpdateTime);
        //添加模糊查询条件 并判断
        queryWrapper.like(StringUtils.isNotEmpty(name), Dish::getName, name);
        //查询
        dishService.page(pageInfo, queryWrapper);
        //对象拷贝,添加新的属性categoryName,忽略records属性,需要向其中添加categoryName
        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);//将每一项copy给dishDto
            Long categoryId = item.getCategoryId();//分类id
            Category category = categoryService.getById(categoryId);//查询分类
            if (category != null){//若不加判断,遇到没有分类的菜品,将会报空指针异常
                String categoryName = category.getName();//分类名称
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());//将经过操作的dto对象收集并返回list
        dishDtoPage.setRecords(list); //将最后得到的list设置给分页即可
        return R.success(dishDtoPage);
    }

3、启售、停售状态修改

  • 确定请求路径为/dish/status/status?ids=123123...,1231231...,1231...,

  • 可将请求数据封装为ids数组,这样无论是单个修改还是批量都可以在一个方法中完成。

  • 参数status,是路径参数,且其携带的值即是目标值,在方法中直接使用即可。

     /**
         * 批量修改启售和停售状态。
         *
         * @param ids    需要操作的id数组
         * @param status  要修改的目标状态值
         * @return
         */
        @PostMapping("/status/{status}")
        public R<String> status(Long[] ids, @PathVariable int status) {
            for (Long id : ids) {
                Dish dish = dishService.getById(id);
                dish.setStatus(status);
                dishService.updateById(dish);
            }
            return R.success("修改状态成功");
        }
    

4、删除和批量删除

  • 与上面的思路相同。

    /**
         * 删除和批量删除
         *
         * @param ids
         * @return
         */
        @DeleteMapping
        public R<String> delete(Long[] ids) {
            boolean bool = dishService.removeByIds(Arrays.asList(ids));
            if (bool) { //进行一次判断是否删除成功。
                return R.success("删除成功...");
            }
            return R.error("删除失败...");
        }
    

5、编辑菜品

此操作中分为两步:

第一步:需要根据id查询,将数据返回并回显。

第二步:点击确定修改之后,需要将封装的数据分别存储到不同的表中。

  1. 根据id查询数据并回显。
    • 在后端接收的数据中,我们发现有些属性在Dish实体类中并没有,所有这里我们还是需要使用DishDto对象,它可以满足对这些数据的封装。

    • 请求路径携带来的id为路径参数,要使用注解@PathVariable

    • 返回值的类型为 R<DishDto>,先从dish表中直接查询基本信息,使用条件构造器从flavor表中查询数据取出,最后将两个数据都给dishDto。

      /**
           * 根据id查询
           * 要将dish信息保存到dishDto中 才能满足所需要的信息
           * @param id
           * @return
           */
          @GetMapping("/{id}")
          public R<DishDto> getById(@PathVariable Long id) {
              //查询菜品的基本信息,从dish表查
              Dish dish = dishService.getById(id);
              DishDto dishDto = new DishDto();
              //条件构造器,从dish_flavor表查询口味信息
              LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(DishFlavor::getDishId,id);
              List<DishFlavor> dishFlavor = dishFlavorService.list(queryWrapper);
              //将基本信息copy给dishDto,再将口味信息给它
              BeanUtils.copyProperties(dish,dishDto);
              dishDto.setFlavors(dishFlavor);
              return R.success(dishDto);
          }
      
  2. 修改/更新菜品信息
    • 和上面的操作类似,也是要操作多张表。传回的数据封装为DishDto对象。

    • 先更新dish表的基本信息,再将flover相关的信息更新到flavor表中。

    • 先清除当前菜品对应的口味数据,再添加当前提交过来的口味。

      /**
           * 修改菜品信息
           * @param dishDto
           * @return
           */
          @Transactional
          @PutMapping
          public R<String> update(@RequestBody DishDto dishDto){
              //先更新dish表
              dishService.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);
              return R.success("修改成功...");
          }
      

六、套餐管理业务开发

0、准备工作

编写相关的domian、dao、service及其impl;dao中要注解@MapperServiceImpl中要注解@Service和事务@Transactional

还需要dto->SetmealDto,注意公共字段的注解FIeldFill

编写SetmealController,加上正确路径以及注解。

@RestController
@RequestMapping("/setmeal")
public class SetmealController{...}

1、新增套餐

  • 请求的路径为"/setmeal",请求方式为Post

  • 新增套餐界面,除了套餐的信息以外,还保存了套餐与菜品的关联信息,所以该操作其实是操作了两张表,需要添加事务。

  • 页面带回的参数的数据要比Setmeal实体类多,所以需要SetmealDto类来接收数据,它继承于Setmeal。

  • 我们在SetmealServiceImpl中重写一个方法saveWithDish,并开启事务。

    		@Autowired
        private SetmealDishService setmealDishService;
    		/**
         * 新增套餐,同时需要保存套餐和菜品的关联关系
         * @param setmealDto
         */
        @Override
        public void saveWithDish(SetmealDto setmealDto) {
            //保存套餐的基本信息 操作setmeal表 insert操作
            this.save(setmealDto);
            //将Dto中与套餐关联的菜品集合取出 并重新给它的套餐id赋值
            List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
            setmealDishes = setmealDishes.stream().map(item->{
                item.setSetmealId(setmealDto.getId());
                return item;
            }).collect(Collectors.toList());
            //保存套餐与菜品的关联信息 操作setmeal_dish表 insert操作
            setmealDishService.saveBatch(setmealDishes);
    
        }
    
  • 在Controller中编写save请求方法,调用上面的方法即可:

     		@Autowired
        private SetmealService setmealService;		
    
    		@PostMapping
        public R<String> save(@RequestBody SetmealDto setmealDto){
            //在service中重写的方法。
            setmealService.saveWithDish(setmealDto);
            return R.success("新增套餐成功...");
        }
    

2、分页查询

  • 参考DishController中的分页查询。

  • 注意:!!!

    //将pageInfo的信息copy给dto的分页构造器,但是要排除records,因为其中还差一项categoryName
          BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
    

    在此处,我想过视频上的内容可能会比较麻烦,难道不能全copy过去之后再添加字段吗?

    所以我试着将records中的信息也copy进去,再将records中的每一条数据加上cetegoryName,最后失败了,这种逻辑会报一个错误,具体错误忘记了(好像是ClassCastException什么的)。应该是在一开始records中每一条数据都是Setmeal,强行copy进SetmealDto中,就会发生不兼容的异常。

    总结:copy的时候将records忽略,单独取出,操作的单位缩小到Setmeal和SetmealDto之间,再进行copy因为是父子关系,所以不会报不兼容异常。上面尝试的方法是将封装了他们的records进行copy,就会报异常。如下:

    //取出刚才排除的pageInfo的records
    List<Setmeal> records = pageInfo.getRecords();
    //使用stream流遍历每一个item
    List<SetmealDto> list =records.stream().map(item->{
        //新建Dto
        SetmealDto setmealDto = new SetmealDto();
        //将item所有的信息copy进dto,还剩下categoryName需要设置进去
        BeanUtils.copyProperties(item,setmealDto);
        //从item中获得categoryId,查询得到categoryName
        Long categoryId = item.getCategoryId();
        Category category = categoryService.getById(categoryId);
        //判断一下,避免报空指针异常
        if (category!=null){
            //将得到的categoryName赋给dto
            String categoryName = category.getName();
            setmealDto.setCategoryName(categoryName);
        }
        //直接将dto返回,并收集为集合,用List<SetmealDto>接收
        return setmealDto;
    }).collect(Collectors.toList());
    //将经过处理的Dto集合重新设置给Dto的分页构造器
    setmealDtoPage.setRecords(list);
    
    //返回给页面的是dto的分页构造器,这样菜能有categoryName。
    return R.success(setmealDtoPage);
    

3、更改售卖状态及批量更改

  • 较为简单,直接遍历页面传来的参数,设置status即可。

    请求的路径为:/setmeal/status/0?ids=123123…,234234…;请求方式Post

    经尝试,ids的接收,如果使用Long[] ids 数组进行接收,不需要加注解,保证ids名字相同即可。若是使用List<Long>集合进行封装,需要加上注解@RequestParam

    问号前面的0,即为想要修改的status值,直接进行操作,不用判断。

    		/**
         * 批量更改售卖状态
         * 注意 @RequestParam List<Long> ids 可以更换为 Long[] ids
         * 若要封装为list集合 需要加注解,使用数组接收的话不需要加注解
         * @param status
         * @param ids
         * @return
         */
        @PostMapping("/status/{status}")
        public R<String> status(@PathVariable int status,@RequestParam List<Long> ids){
    
            for (Long id : ids) {
                Setmeal setmeal = setmealService.getById(id);
                setmeal.setStatus(status);
                setmealService.updateById(setmeal);
            }
            return R.success("状态修改成功...");
        }
    

4、删除和批量删除

  • 该操作也是调用多张表的操作,我们移到service去重写方法,并开启事务。
  • **注意:**删除套餐时,还需要删除其关联的菜品信息;
  • 同时,只有停售状态的套餐才可以删除,是比较合理的。
    • 先根据ids查询非停售状态的套餐有几个,若是大于0,则删除失败;
    • 我们之前写了一个自定义的CustomException异常类,抛出一个异常即可。
    • 若是非停售状态的个数为0,再执行删除操作,该操作分两个表:
      • 先删除套餐表中的数据 setmeal表
      • 再移除套餐与菜品之间的关系数据 seatmeal_dish表
  • 再service中封装的方法如下:(事务已经加在了类注解上,不知道会不会有其他影响,以后会继续研究。)
/**
 * 删除套餐,并相应的删除关联的菜品信息
 * @param ids
 */
@Override
public void removeWithDish(List<Long> ids) {
    //注意:删除的套餐的售卖状态要是停售才可以删除
    //具体的sql应该是下面这样,判断删除id的状态是1的是不是0,大于0,则抛出一个自定的也无异常
    //select count(*) from setmeal where id in (ids) and status = 1;
    //查询套餐状态,确定是否可以删除
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(Setmeal::getId,ids);
    queryWrapper.eq(Setmeal::getStatus,1);
    long count = this.count(queryWrapper);
    //若不能删除,抛出一个业务异常
    if (count>0){
        throw new CustomException("该套餐为启售状态,无法删除。");
    }
    //若可以删除,先删除套餐表中的数据
    this.removeBatchByIds(ids);
    //再移除套餐与菜品之间的关系数据
    LambdaQueryWrapper<SetmealDish> queryWrapper1 = new LambdaQueryWrapper<>();
    queryWrapper1.in(SetmealDish::getSetmealId,ids);
    setmealDishService.remove(queryWrapper1);

}
  • 在controller中我们直接调用即可:

    		/**
         * 删除和批量删除 套餐信息
         * 但是注意要操作两张表
         * 所以在service中重新写了一个方法 并要添加事务
         * @param ids
         * @return
         */
        @DeleteMapping
        public R<String> delete(@RequestParam List<Long> ids){
            //调用重写的方法,内部需要移除套餐数据以及关联表中的关联数据
            setmealService.removeWithDish(ids);
            return R.success("删除成功...");
        }
    

**

未完待续… 奋力学习中…

**

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m是只奶牛猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值