【瑞吉外卖项目复写】基本部分复写笔记

Day1 瑞吉外卖项目概述

mysql的数据源配置

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/regie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root

注意要配置mysql其实是配置druid数据源。

注意url的后缀。

mybatisplus配置

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

①log-impl:在MybatisPlus中,log-impl是用于配置mybatis的日志实现方式的属性。log-impl属性允许您指定mybatis再执行sql语句时使用哪种日志实现。

其中“org.apache.ibatis.logging.stdout.StdOutImpl”是mybatis提供的一种日志实现,它将日志信息输出到标准输出(控制台)。

②assign-id:在mybatisplus中,global-config是全局配置的一部分,用于配置一些全局的属性和策略。在global-config中,db-config是数据库配置的子属性,用于配置数据库相关的一些选项。

具体来说,id-type是db-config的子属性,用于指定主键id的生成策略。

1.auto:自增逐渐,使用与数据库自增长类型的字段(如mysql的auto_increment)

2.input:用户输入主键值,用户手动输入主键的值

3.assign-id:分配id主键,通过代码手动分配主键的值

4.assign-uuid:分配uuid主键,通过代码手动分配uuid类型的主键值

5.none:无主键生成策略,需要手动设置主键的值,不推荐使用

修改静态资源映射路径

如果前端资源不在static或template目录下,则需要修改静态资源映射路径

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

    }
}

第一步:创建config类型的类继承WebMvcConfigurationSupport

第二步:重写addResourceHandlers方法

将前端资源通过addResourceHandler方法、addResourceLocations方法映射到静态资源路径

后台登录功能开发

Java实体类实现序列化

在Java中,实现Serializable接口是为了表明该类的对象可以被序列化。序列化是将对象转换为字节流的过程,以便对象存储在磁盘上或通过网络进行传输。

在实现Serializable接口时,并没有需要实现的抽象方法,它只是一个标记接口(Marker Interface),标志着该类的对象是可以序列化的。

private static final long serialVersionUID=1L:

是在实现Serializable接口的类中顶一个序列化版本号(Serialization Version UID)。这个版本号是为了确保序列化和反序列化过程中的兼容性。

比如对于如下的MyClass类实现了Serializable接口,并显示的设置了serialVersionUID的值为123456789L。这样,当MyClass类发生变化时,版本号将保持一致,从而确保序列化和反序列化的兼容性。

import java.io.Serializable;

public class MyClass implements Serializable{
    private static final long serialVersionUID=123456789L;

    //类的其他成员和方法
    private String name;

    private int age;


}

封装通用响应类

在这个类中,泛型被用作数据的类型参数,允许在运行中指定具体的数据类型。这使得R类在返回数据时可以根据实际需要返回不同类型的数据,而不限于特定类型。

其中map是一个HashMap对象,用于在响应中存储其他键值对的附加信息。

其中add(String key,Object value)实例方法,用于向响应中的map添加附加信息。它接收一个字符串key和一个对象value,将键值对添加到map中,并返回当前R对象本身。这使得可以链式调用该方法来添加多个键值对。

public class R {
    private int code;
    private String errMsg;
    private T data;

    private Map map=new HashMap();

    public static  R success(T object){
        R tr = new R<>();
        tr.data=object;
        tr.code=1;
        return tr;
    }

    public static  R error(String msg){
        R tr = new R<>();
        tr.errMsg=msg;
        tr.code=0;
        return tr;
    }

    public R add(String key,Object value){
        this.map.put(key,value);
        return this;
    }
}

编写Controller报错

【瑞吉外卖项目复写】基本部分复写笔记_第1张图片

居然是因为依赖有问题:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.7.10
         
    
    com.itheima
    reggie_take_out
    1.0-SNAPSHOT

    
        17
        17
    
    

        
            org.springframework.boot
            spring-boot-starter
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            org.springframework.boot
            spring-boot-starter-web
            compile
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        

        
            org.projectlombok
            lombok
            1.18.20
        

        
            com.alibaba
            fastjson
            1.2.76
        

        
            commons-lang
            commons-lang
            2.6
        

        
            mysql
            mysql-connector-java
            8.0.31
            runtime
        

        
            com.alibaba
            druid-spring-boot-starter
            1.1.23
        

        
            cn.hutool
            hutool-all
            5.7.17
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                2.4.5
            
        
    

备注:scope的用法

  1. compile(默认值):这是最常用的scope,表示该依赖在编译、测试、运行和打包时都是可见的。这意味着依赖将被包含在生成的JAR或WAR文件中,并且对所有阶段都是可用的。

  2. provided:该依赖在编译和测试阶段是可见的,但在运行和打包阶段不会包含在生成的JAR或WAR文件中。它假设运行时环境中已经存在该依赖,比如Java EE容器中的一些API,例如Servlet API、JSP API等。

  3. runtime:该依赖在运行和打包阶段是可见的,但在编译和测试阶段不会包含在生成的JAR或WAR文件中。它表示该依赖只在运行时才需要,例如数据库驱动。

  4. test:该依赖只在测试阶段可见,不会包含在生成的JAR或WAR文件中,它用于测试时所需的依赖。

  5. system:类似于provided,但需要明确指定依赖的路径。这样的依赖将不从Maven仓库获取,而是从本地文件系统中的特定路径加载。一般不推荐使用此scope,除非你确实需要。

  6. import:该scope用于定义一个依赖POM的依赖。它表示该依赖将被传递到项目中,并且不会用于构建项目本身。

通过合理使用scope属性,可以帮助优化项目的依赖管理,减少不必要的依赖传递和构建时的冗余。例如,对于只在编译时使用的依赖,可以设置为provided,从而在运行时不包含这些依赖,减小了最终生成的包的大小。

Day2 员工业务管理开发

完善登录功能

现存问题:即使没有登陆也可以直接访问index页面

改进思路:添加Filter

改进步骤:①实现Filter接口

                  ②重写doFilter方法

                  注意:1.匹配路径需要用到路径匹配器AntPatchMatcher。

                                匹配规则:?匹配一个字符

                                                  * 匹配任意字符序列,但不包括路径分隔符

                                                  ** 匹配任意字符序列,包含路径分隔符

                                在使用antPatchMatcher的时候,可以用match()方法进行匹配

                             2.获取请求路径用httpServletRequest.getRequestURI()方法

                             3.如果用户没有登陆,因为doFilter方法的返回值为void,所以应该用response的输出流返回响应数据。

                                       response.getWriter().write(JSON.toJSONString(R.error("NOT LOGIN")));

                        ③完成注解标注

                                i.要在Filter上方标注@WebFilter注解。其中filterName唯一,urlPatterns="/*"代表Filter将过滤所有HTTP请求,即对所有的请求进行拦截和处理。

                                ii.要在启动类上标注@ServletComponentScan,才能扫描到Filter

代码实现:

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    private AntPathMatcher PATCH_MATCHER=new AntPathMatcher();
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //放行不需要被检查的资源
        String requestURI = request.getRequestURI();
        boolean check = checkURI(requestURI);
        if(check){
            filterChain.doFilter(request,response);
            return;
        }

        //判断用户是否登录,登录则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            filterChain.doFilter(request,response);
            return;
        }

        if(request.getSession().getAttribute("user")!=null){
            filterChain.doFilter(request,response);
            return;
        }

        //如果未登录,则通过输出流方式向客户端响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOT LOGIN")));
    }

    private boolean checkURI(String requestURI){
        String[] uris=new String[]{
                "/employee/login",
                "/employee/logout",
                "/user/sendMsg",
                "/user/login",
                "/backend/**",
                "/front/**"
        };
        for(String uri:uris){
            boolean match = PATCH_MATCHER.match(uri, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

新增员工

对于新增员工,由于账号应该唯一不重复,所以如果账号重复会抛出异常:

【瑞吉外卖项目复写】基本部分复写笔记_第2张图片

可以编写全局异常处理器来解决这个问题:

 编写GlobalExceptionHandler

        1.@ControllerAdvice注解用于声明一个全局异常处理器类

                annotations属性指定了该全局异常处理器只处理带有@RestController或@Controller注解的控制器类(Controller)抛出的异常

        2.@ResponseBody注解,用于表示方法的返回值将直接作为响应体(Response Body)返回给客户端,而不会被视图解析器处理

          在全局异常处理器中,通过添加@ResponseBody注解,确保异常处理方法的返回值会被转换为JSON格式并返回给客户端

@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R handleCustomException(SQLIntegrityConstraintViolationException ex){
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

员工信息分页查询

第一步:添加mybatisplus分页器

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

}

第二步:编写Controller

    @GetMapping("/page")
    public R> getByPage(@RequestParam int page, @RequestParam int pageSize,@RequestParam String name){
        Page employeePage = new Page<>(page,pageSize);

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StrUtil.isNotEmpty(name),Employee::getName,name);
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        employeeService.page(employeePage);
        return R.success(employeePage);

    }

备注:加与不加@RequestParam的区别

①不加@RequestParam前端的参数名需要和后端控制器的变量名保持一致才能生效

②不加@RequestParam参数为非必传,加@RequestParam则参数为必传。但是@RequestParam可以通过@RequestParam(required=false)设置为非必传

③@RequestParam可以通过@RequestParam("userId")或者@RequestParam(value="userId")指定传入的参数名(最主要的作用)

④@RequestParam可以通过@RequestParam(defaultValue="0")指定参数默认值

⑤如果接口除了前端调用还有后端RPC调用,则不能省略@RequestParam,否则RPC会找不到参数报错

⑥Get方式请求,参数放在url中时:

        不加@RequestParam注解:url可带参数也可不带参数,输入localhost:8080/list1以及localhost:8080/list1?userId=xxx方法都能执行

        加@RequestParam注解:url必须带有参数。也就是说你直接输入localhost:8080/list2会报错,不会执行方法。只能输入localhost:8080/list2?userId=xxx才能执行相应的方法

员工启用和禁用

在员工启用和禁用功能中,虽然后台已经修改了员工的状态,但是前台却不会显示出来。这是因为前台将整型以数值型类型读出,出现了精度丢失,导致员工id与后台id不一致。

此外,前台对时间的读取不方便阅读,也可以通过自定义的JacksonObjectMapper进行自定义的序列化和反序列化。

第一步:编写JacksonObjectMapper

①在默认情况下,Jackson对象映射器(ObjectMapper)在进行反序列化时,会尝试根据需要自动将字符串类型转换为其他数据类型,包括Long类型。这个转换是基于目标属性的数据类型和字符串内容进行判断的。

例如,如果目标属性是Long类型,而JSON中的对应值是一个合法的表示长整型的字符串,那么Jackson会自动将该字符串转换为Long类型。

②this.configure(FAIL_ON_UNKNOWN_PROPERTIES,false);这个配置是针对整个ObjectMapper对象的,它会将整个ObjectMapper实例的FAIL_ON_UNKNOWN_PROPERTIES设置为false,意味着该ObjectMapper在进行序列化和反序列化时,都不会报告未知属性的异常。

this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);这个配置是在进行反序列化时,针对当前ObjectMapper实例的DeserializationConfig对象,将其中的 "FAIL_ON_UNKNOWN_PROPERTIES" 设置为 false。这样,仅针对当前的 ObjectMapper,反序列化操作在遇到未知属性时才不会抛出异常。

③区别:

如果你只配置 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 而不配置 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);,会产生如下影响:

  1. 序列化时的影响: 配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 后,在进行序列化时,无论是哪个 ObjectMapper 实例,都不会因为遇到未知属性而抛出异常。如果你的序列化操作中包含了未知属性,那么在序列化过程中,这些未知属性会被忽略,不会导致序列化失败。

  2. 反序列化时的影响: 配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 对反序列化的影响是不明显的。因为这个配置是针对整个 ObjectMapper 对象的,而在反序列化过程中,通常会使用局部的 DeserializationConfig 对象,例如 this.getDeserializationConfig(),而并不直接使用全局配置。所以,在反序列化时,未知属性是否会导致异常取决于局部的 DeserializationConfig 配置,而不是全局的配置。如果局部的 DeserializationConfig 也禁用了未知属性异常(即 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);),那么在反序列化时也会忽略未知属性,否则仍然可能抛出异常。

因此,如果你只配置了 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);,并没有配置 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);,那么在序列化时未知属性会被忽略,但在反序列化时未知属性可能仍然会导致异常,具体取决于反序列化时局部的 DeserializationConfig 配置。如果你希望在序列化和反序列化时都忽略未知属性,建议两个配置都使用。

ToStringSerializer.instance 是 Jackson 库中的一个特殊的序列化器对象,用于将对象的值以字符串形式进行序列化。

在默认情况下,Jackson 库会根据对象的实际类型进行序列化,并输出相应的 JSON 格式。例如,对于 Java 对象的整数属性,Jackson 会将其序列化为 JSON 中的数值类型(例如整数),而对于字符串属性,Jackson 会将其序列化为 JSON 中的字符串类型。

然而,有时候我们希望将某些属性以字符串形式进行序列化,而不是根据实际类型进行序列化。这时,可以使用 ToStringSerializer.instance 来达到这个目的。

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(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)
                .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);
    }

第二步:重写WebMvcConfig类的extendMessageConverters方法

记得将自定义的ObjectMapper对应的消息转换器放在第一个优先使用。

    @Override
    protected void extendMessageConverters(List> converters) {
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        converters.add(0,messageConverter);
    }

Day3 分类管理业务开发

公共字段填充

在后台系统的员工管理功能开发中,新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时,也需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段。

我们可以用mybatisplus提供的公共字段自动填充功能统一处理。

第一步:编写通用工具类封装ThreadLocal,用于存储登录用户的id

public class BaseContext {
    private static ThreadLocal threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

第二步:在LoginCheckFilter中为已登录的用户添加id到ThreadLocal

        //判断用户是否登录,登录则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            Long empId =(Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);
            filterChain.doFilter(request,response);
            return;
        }

        if(request.getSession().getAttribute("user")!=null){
            Long userId =(Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);
            filterChain.doFilter(request,response);
            return;
        }

第三步:自定义类实现接口MetaObjectHandler,实现公共字段自动填充

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());

        Long currentId = BaseContext.getCurrentId();
        metaObject.setValue("createUser", currentId);
        metaObject.setValue("updateUser", currentId);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime", LocalDateTime.now());

        Long currentId = BaseContext.getCurrentId();
        metaObject.setValue("updateUser", currentId);
    }
}

第四步:删除EmployeeController中创建时间、创建人、修改时间、修改人相关的冗余代码

删除分类

删除分类的时候需要检查该分类是否关联了菜品或者套餐,若关联应该抛出异常

第一步:自定义删除异常

public class CustomDeleteException extends RuntimeException{
    public CustomDeleteException(String message){
        super(message);
    }

}

第二步:注册自定义删除异常

    @ExceptionHandler(CustomDeleteException.class)
    public R handleCustomDeleteException(CustomDeleteException ex){
        return R.error(ex.getMessage());
    }

第三步:自定义删除方法

@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    @Override
    public void deleteCategory(Long ids) {
        LambdaQueryWrapper dishQueryWrapper = new LambdaQueryWrapper<>();
        dishQueryWrapper.eq(Dish::getCategoryId,ids);
        int countDish = dishService.count(dishQueryWrapper);
        if(countDish>0){
            throw new CustomDeleteException("该分类含菜品,无法删除");
        }

        LambdaQueryWrapper setmealQueryWrapper = new LambdaQueryWrapper<>();
        setmealQueryWrapper.eq(Setmeal::getCategoryId,ids);
        int countSetmeal = setmealService.count(setmealQueryWrapper);
        if(countSetmeal>0){
            throw new CustomDeleteException("该分类含套餐,无法删除");
        }

        this.removeById(ids);
    }
}

第四步:Controller调用自定义删除方法

    @DeleteMapping
    public R delete(Long ids){
        categoryService.deleteCategory(ids);
        return R.success("删除分类成功");
    }

Day4 菜品管理业务开发

文件上传下载

文件上传:

前端要求:①表单提交,method="post" ②enctype="multipart/form-data" ③type="file"

后端要求:使用MultipartFile作为形参类型接收上传的文件

file.transferTo()方法,将文件上传到服务器指定位置

文件下载:

图片以流的形式读出并写回网页

@RestController
@RequestMapping("/common")
public class CommonsController {
    @Value("${reggie.path}")
    private String basePath;

    @PostMapping("/upload")
    public R upload(MultipartFile file){
        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        String prefix = IdUtil.simpleUUID();
        String filename = prefix+suffix;

        File dir = new File(basePath);
        if(!dir.exists()){
            dir.mkdirs();
        }

        try {
            file.transferTo(new File(basePath+filename));
        } catch (IOException e) {
            e.printStackTrace();
        }

        return R.success(filename);

    }

    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            FileInputStream fileInputStream = new FileInputStream(basePath + name);
            ServletOutputStream outputStream = response.getOutputStream();

            response.setContentType("image/jepg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }

            fileInputStream.close();
            outputStream.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

新增菜品

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据

注意:因为要同时操作两张表,所以需要在方法上加上注解@Transactional,同时在启动类上加注解@EnableTransactionManagement

@Service
public class DishServiceImpl extends ServiceImpl implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;

    @Override
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {
        System.out.println(dishDTO.getId());
        this.save(dishDTO);
        System.out.println(dishDTO.getId());
        Long dishId = dishDTO.getId();
        List dishFlavors = dishDTO.getDishFlavors();
        dishFlavors = dishFlavors.stream().map(item -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        dishFlavorService.saveBatch(dishFlavors);
    }
}

我添加了两条打印菜品ID的语句:

【瑞吉外卖项目复写】基本部分复写笔记_第3张图片

 由此可见,尽管传递过来的数据菜品ID为空,但是在保存菜品到数据库以后,会将菜品ID返回至dishDTO实体类中,并可以通过dishDTO.getId()得到菜品的ID

菜品信息分页查询

注意不能在DishServiceImpl注入CategoryService,因为之前已经在CategoryService中注入过DishServiceImpl了。

【瑞吉外卖项目复写】基本部分复写笔记_第4张图片

解决方法:直接在DishController中写分页信息查询:

        因为页面需要的是CategoryName而非CategoryId,所以需要用categoryService查询

        返回的DishDTO里包含categoryName属性

        注意DishDTO作为一种传输手段,只需要满足需要的属性不为空即可,这里用不到DishFlavor,可以为空

    @GetMapping("/page")
    public R> getByPage(int page,int pageSize,String name) {
        Page dishPage = new Page<>(page, pageSize);
        LambdaQueryWrapper dishQueryWrapper = new LambdaQueryWrapper<>();
        dishQueryWrapper.like(name != null, Dish::getName, name);
        dishQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        dishService.page(dishPage, dishQueryWrapper);

        Page dishDTOPage = new Page<>();
        BeanUtils.copyProperties(dishPage, dishDTOPage, "records");

        List dishRecords = dishPage.getRecords();
        List dishDTOList = dishRecords.stream().map(item -> {
            DishDTO dishDTO = new DishDTO();

            BeanUtils.copyProperties(item, dishDTO);

            Long categoryId = item.getCategoryId();
            String categoryName = categoryService.getById(categoryId).getName();
            dishDTO.setCategoryName(categoryName);

            return dishDTO;
        }).collect(Collectors.toList());

        dishDTOPage.setRecords(dishDTOList);

        return R.success(dishDTOPage);
    }

修改菜品

第一步:菜品内容回显

    @Override
    public DishDTO editWithFlavor(Long id) {
        DishDTO dishDTO = new DishDTO();

        Dish dish = this.getById(id);
        BeanUtils.copyProperties(dish,dishDTO);

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,id);
        List dishFlavors = dishFlavorService.list(queryWrapper);

        dishDTO.setDishFlavors(dishFlavors);

        return dishDTO;
    }

        注意前后端内容传递与接收,前台需要用res.data.dishFlavors接收后台传递的dishFlavors,如果接收不到的话回显是会失败的

第二步:修改菜品信息

    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        this.updateById(dishDTO);

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dishDTO.getId());
        dishFlavorService.remove(queryWrapper);

        List dishFlavors = dishDTO.getDishFlavors();
        dishFlavors=dishFlavors.stream().map(item->{
            item.setDishId(dishDTO.getId());
            return item;
        }).collect(Collectors.toList());
        dishFlavorService.saveBatch(dishFlavors);
    }

Day5 套餐业务管理开发

删除套餐

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

    @Override
    @Transactional
    public void deleteWithDish(List ids) {
        LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.in(Setmeal::getId,ids).eq(Setmeal::getStatus,1);
        int count = this.count(setmealLambdaQueryWrapper);
        if(count>0){
            throw new CustomDeleteException("套餐正在售卖中,不能删除");
        }

        this.removeByIds(ids);

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(SetmealDish::getSetmealId,ids);
        setmealDishService.remove(queryWrapper);
    }

注意,当接收的参数不是基本类型也不是实体类的时候,应该使用@RequestParam注解

    @DeleteMapping
    public R delete(@RequestParam List ids){
        setmealService.deleteWithDish(ids);
        return R.success("删除套餐成功");
    }

手机验证码登录

第一步:发送验证码

第二步:登录

优化:存储“code”的时候,拼接了phone-code,这样就能避免传递过来code正确,而phone悄悄改了的问题

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/sendMsg")
    public R getCode(@RequestBody User user, HttpSession session){
        String phone = user.getPhone();
        String code = RandomUtil.randomNumbers(6);
        session.setAttribute("code", phone+"-"+code);
        return R.success(code);
    }

    @PostMapping("/login")
    public R login(@RequestBody UserDTO userDTO, HttpSession session){
        String phone = userDTO.getPhone();
        String code = userDTO.getCode();

        String testCode =(String) session.getAttribute("code");
        if(testCode==null){
            return R.error("验证码已失效");
        }
        code = phone+"-"+code;

        if(!testCode.equals(code)){
            return R.error("验证码或手机号有误");
        }

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User one = userService.getOne(queryWrapper);
        if(one == null){
            one=new User();
            one.setPhone(phone);
            userService.save(one);
        }

        session.setAttribute("user", one.getId());

        return R.success(one);

    }


}

Day6 菜品展示、购物车、下单

设置默认地址

第一步:将收件人的所有地址改为非默认

第二步:通过updateById()方法将指定收件地址改为默认

    @PutMapping("/default")
    public R setDefault(@RequestBody AddressBook addressBook){
        Long userId = BaseContext.getCurrentId();

        LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(AddressBook::getIsDefault,0).in(AddressBook::getUserId,userId);
        addressBookService.update(updateWrapper);

        addressBook.setIsDefault(1);
        addressBookService.updateById(addressBook);

        return R.success(addressBook);
    }

菜品展示

前端会根据返回的结果是否含有flavors做判断,从而对没有口味选择的菜品展示【+】,对有口味选择的菜品展示【选规格】。所以只需要改造listDishes,将返回值改为R>,并对每一个DishDTO填充flavors(如果有)即可。

菜品:

    @GetMapping("/list")
    public R> listDishes(Long categoryId){
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Dish::getCategoryId,categoryId);
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List dishList = dishService.list(queryWrapper);

        List dishDTOList = dishList.stream().map(item -> {
            DishDTO dishDTO = new DishDTO();
            BeanUtils.copyProperties(item, dishDTO);

            Category category = categoryService.getById(categoryId);
            if (category != null) {
                dishDTO.setCategoryName(category.getName());
            }

            LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(DishFlavor::getDishId, item.getId());
            List flavors = dishFlavorService.list(wrapper);

            dishDTO.setFlavors(flavors);
            return dishDTO;
        }).collect(Collectors.toList());
        return R.success(dishDTOList);
    }

套餐:

    @GetMapping("/list")
    public R> list(Setmeal setmeal){
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);
        List list = setmealService.list(queryWrapper);
        return R.success(list);
    }

将菜品/套餐添加至购物车

将菜品/购物车添加至购物车的时候需要判断是否为第一次添加,如果不是则只修改数量

要区分是哪个用户添加的

    @PostMapping("/add")
    public R save(@RequestBody ShoppingCart shoppingCart){
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,userId);
        Long dishId = shoppingCart.getDishId();
        if(dishId!=null){
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else{
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if(cartServiceOne!=null){
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number+1);
            shoppingCartService.updateById(cartServiceOne);
        }else{
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);
            cartServiceOne=shoppingCart;
        }

        return R.success(cartServiceOne);

    }

用户下单

@Service
public class OrdersServiceImpl extends ServiceImpl implements OrdersService {
    @Autowired
    private ShoppingCartService shoppingCartService;
    @Autowired
    private UserService userService;
    @Autowired
    private AddressBookService addressBookService;
    @Autowired
    private OrderDetailService orderDetailService;

    public OrdersServiceImpl() {
    }

    @Override
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long currentId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,currentId);
        List shoppingCarts = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
        if(shoppingCarts==null || shoppingCarts.size()==0){
            throw new CustomDeleteException("购物车为空,不能下单!");
        }

        //查询用户数据
        User user = userService.getById(currentId);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook==null){
            throw new CustomDeleteException("用户地址信息有误,不能下单!");
        }

        //向订单表插入数据,一条数据
        long orderId = IdWorker.getId();

        AtomicInteger amount=new AtomicInteger(0);
        List orderDetails=shoppingCarts.stream().map(item->{
            OrderDetail orderDetail=new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));
        orders.setUserId(currentId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName()==null?"":addressBook.getProvinceName())
                +(addressBook.getCityName()==null?"":addressBook.getCityName())
                +(addressBook.getDistrictName()==null?"":addressBook.getDistrictName())
                +(addressBook.getDetail()==null?"":addressBook.getDetail())
        );
        this.save(orders);

        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);
        //清空购物车数据
        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);

    }


}

【瑞吉外卖项目复写】基本部分复写笔记_第5张图片

 


复写部分基本完成~

你可能感兴趣的:(项目,笔记,springboot,后端)