• 项目笔记-瑞吉外卖(全)


    1.业务开发

    1.对后端返回请求值的分析

    2.对不同种请求参数的分析

    3.事务管理

    day01

    1.软件开发整体介绍

    image-20230418160022895

    2.项目整体介绍⭐️

    • 后端:管理菜品和员工信息
    • 前台:通过手机端,可以浏览菜品和添加客户端

    开发项目流程:

    1. 实现基本需求,用户能在手机浏览器访问
    2. 对移动端应用改进,使用微信小程序实现
    3. 对系统进行优化升级,提高系统的使用性能

    技术选型:

    image-20230418161113532

    功能架构:

    image-20230418161313098

    角色:

    image-20230418161548324

    3.开发环境搭建

    • 涉及数据库 + maven
    1. 数据库表介绍:

      image-20230418162508678

    2. Maven项目搭建

      • 第一步,先创建一个maven空项目,然后设置好pom.xml文件和application.yml文件
      • 第二步,配置springboot环境,启动测试
      • 第三部,导入前端静态资源,加入配置类来将浏览器路径和本地项目文件路径做匹配
      @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

      image-20230419150520685

      image-20230423173134325

    4.登录功能⭐️

    查看项目代码的一般逻辑:

    ​ 前端html界面,找到响应的元素,找到对应的js动态方法,分析发送(Ajax)请求到后端的过程,处理好后端代码,返回处理的R对象给前端来判断使用(判断运用是否正确),最后前端再决定跳转到哪一个界面

    1. 需求分析

      image-20230419150858041

      image-20230419150914903

      前端代码

      image-20230419152731239

    2. 功能结构

      image-20230419154734461

    4.1代码实现
    1. 导入通用返回结果类R类

      ​ 前端代码与R类关系

      image-20230423162923815

      image-20230423172106131

      R类

      image-20230423163304568

    2. 梳理登录方法逻辑

      image-20230423163746913

    3. 代码实现

      image-20230423165934138

    5.退出功能

    1. 功能逻辑

      image-20230423172351107

      image-20230423172709483

    6.页面效果出现

    • index.html

      menuList属性值封装了不同页面的信息

      image-20230423175130747

      image-20230423175323900

      image-20230423175420479

      image-20230423175447538


    day02

    完成功能:

    image-20230424112008865

    1.完善登录功能

    1. 问题分析

      使用过滤器或者拦截器实现

      image-20230424112108845

    2. 代码实现步骤

      image-20230424112250325

      image-20230424113952813

    3. 具体实现

      • 1.拦截器用原生的Servlet拦截,因此主加载类要加上@ServletComponentScan注解拦截

        2.加上日志注解,能够使用日志输出

      image-20230424113338495

      image-20230424113552766

      • 2.具体逻辑

      前端处理部分

      image-20230424132741858

      前端处理响应拦截器:如果是这个状态那么自动跳转回登录页面

      image-20230424130031089

      后端部分:

      package com.itiheima.reggie.filter;
      
      import com.alibaba.fastjson.JSON;
      import com.itiheima.reggie.entity.R;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.util.AntPathMatcher;
      
      import javax.servlet.*;
      import javax.servlet.annotation.WebFilter;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      
      /**
       * @author qin start
       * @create 2023-04-24-11:28
       */
      @WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")//拦截所有路径
      @Slf4j
      public class LoginCheckFilter implements Filter {
      
          //spring路径匹配器
          public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
      
          @Override
          public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
              //转成http格式的Servlet
              HttpServletRequest request = (HttpServletRequest) servletRequest;
              HttpServletResponse response = (HttpServletResponse) servletResponse;
      
      
      //        1、获取本次请求的URI
              //定义不需要处理的请求路径
              String url = request.getRequestURI();
              String[] urls = new String[]{
                      "/employee/login",
                      "/employee/logout",
                      "/backend/**",//静态资源放行
                      "/front/**"
              };
      //        2、判断本次请求是否需要处理
              boolean check = check(url, urls);
      //        3、如果不需要处理,则直接放行
              if(check){
                  log.info("拦截到的请求:{}",url);
                  filterChain.doFilter(request,response);
                  return;
              }
      //        4、判断登录状态,如果已登录,则直接放行
              //通过判断session存储的数据
              if(request.getSession().getAttribute("id") != null){
                  log.info("登陆成功!用户id为:{}",request.getSession().getAttribute("id"));
                  filterChain.doFilter(request,response);
                  return;
              }
      //        5、如果未登录则返回未登录结果
      //
              //这里要用输出流,因为不是控制器自动返回json格式对象
              log.info("登陆失败!跳转回登录界面");
              response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));//不放行
              return;
          }
      
          /**
           * 判断请求路径是否在不需要处理的路径里
           * @param url
           * @param urls
           * @return
           */
          public boolean check(String url,String[] urls){
              for (String pattern : urls) {
                  //这里顺序不能搞反,第一个参数为匹配模式
                  if(PATH_MATCHER.match(pattern,url)){
                      return true;
                  }
              }
              return false;
          }
      }
      
      
      • 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

    2.新增员工功能

    1. 功能
    • 数据模型中,employee字段要唯一

    image-20230424190955264

    image-20230424191103342

    1. 执行流程

      image-20230424191315458

    2. 代码实现

      @PostMapping
      public R<String> addEmployee(HttpServletRequest request,@RequestBody Employee employee){
      
          //        log.info(employee.toString());
      
          //设置初始密码
          employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes(StandardCharsets.UTF_8)));
      
          employee.setCreateTime(LocalDateTime.now());
          employee.setUpdateTime(LocalDateTime.now());
      
          employee.setCreateUser((long)request.getSession().getAttribute("id"));
          employee.setUpdateUser((long)request.getSession().getAttribute("id"));
      
          employeeService.save(employee);
      
          return R.success("新增员工成功");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      1. 处理数据库插入重复名字异常

        image-20230424195410588

        全局异常处理器来处理异常

        关键点在@ControllerAdvice和@ExceptionHandler,一个用来拦截方法,一个用来处理异常

        @ControllerAdvice捕获方法后,有异常就处理

        @ControllerAdvice(annotations = {RestController.class, Controller.class})
        @ResponseBody//java对象转为json格式的数据
        @Slf4j
        public class GlobalExceptionHandler {
        
            //用来捕获插入重复数据异常
            @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
            public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
                log.error(exception.getMessage());
                return R.error("failed");
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        //用来捕获插入重复数据异常
        @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
        public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
            if (exception.getMessage().contains("Duplicate entry")){
                String[] split = exception.getMessage().split(" ");//根据空格符分割数组
                String msg = split[2] + "已存在";
                return R.error(msg);
            }
            return R.error("unknown error");
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
      2. 小结:

        image-20230424202223370

    3.启用禁用员工信息⭐️(自定义消息转换器使用)

    1. 需求分析
    image-20230426184700255

    image-20230426184740200

    1. 启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法

      @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
    2. 出现问题:

      image-20230426184944890

      原因:js对后端传过来的数据long类型精度丢失,因为Java对象默认通过SpringMVC消息转换器传递过来的数据默认是一般的json格式,"id"字段会被当做整型数据处理,而js中Long型精度和后端不匹配。

      **解决:**对SpringMVC配置自定义的消息处理器,将"id"对应的json格式数据转为字符串值

    3. 具体解决步骤:

      ①提供对象转换器Jackson0bjectMapper,基于Jackson进行Java对象到json数据的转换
      ②在WebMcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

      //JacksonObjectMapper
      package com.itzq.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)
                      .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
      /**
           * 扩展mvc框架的消息转换器
           * @param converters
           */
      @Override
      protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
      
          //创建消息转换器对象
          MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
          //设置对象转化器,底层使用jackson将java对象转为json
          messageConverter.setObjectMapper(new JacksonObjectMapper());
          //将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)
          converters.add(0,messageConverter);
      
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

    4.编辑员工信息

    1. 功能分析image-20230426185729748

      image-20230426185809573

    2. 后端功能代码实现

      回显数据

      @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

      修改功能

      前面在禁用员工的时候使用到了修改数据,所以这里不用配置

    day03

    1.公共字段自动填充

    • 问题分析:

      设置修改时间和修改人等字段在每张表中基本上都有,而且属于多条记录共有的具有相似功能的字段,因此可以每次修改或者插入的时候自动处理

    • 用到技术:①Mybatis Plus公共字段自动填充ThreadLocal线程内部属性

    • 技术详解:

      ThreadLocal线程内部属性:客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程,因此一次请求中ThreadLocal对象也应该相同

      image-20230426190703133

      因此,可以用ThreadLocal用于保存登录用户id,解决实现公共字段填充类无法在方法上自动装配HttpServletRequest的困境,做到会话间数据共享

    1. 实现步骤

      image-20230426190333181

    2. 具体代码实现

      第一版

      package com.itzq.reggie.common;
      
      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;
      
      @Component
      @Slf4j
      public class MyMetaObjectHandler implements MetaObjectHandler {
      
          @Override
          public void insertFill(MetaObject metaObject) {
              log.info("公共字段自动填充【insert】");
              log.info(metaObject.toString());
              metaObject.setValue("createTime", LocalDateTime.now());
              metaObject.setValue("updateTime", LocalDateTime.now());
              metaObject.setValue("createUser", new Long(1));
              metaObject.setValue("updateUser", new Long(1));
          }
      
          @Override
          public void updateFill(MetaObject metaObject) {
              log.info("公共字段自动填充【update】");
              log.info(metaObject.toString());
          }
      }
      
      • 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

      第二版

      @Component
      @Slf4j
      public class MyMetaObjectHandler implements MetaObjectHandler {
      
          /**
           * 插入时对公共字段赋值
           * @param metaObject
           */
          @Override
          public void insertFill(MetaObject metaObject) {
              log.info("公共字段自动填充【insert】");
              //1.直接给公共字段设置值
              metaObject.setValue("createTime", LocalDateTime.now());
              metaObject.setValue("updateTime", LocalDateTime.now());
              metaObject.setValue("createUser", BaseContext.get());
              metaObject.setValue("updateUser", BaseContext.get());
          }
      
          /**
           * 更新时对公共字段赋值
           * @param metaObject
           */
          @Override
          public void updateFill(MetaObject metaObject) {
              log.info("公共字段自动填充【update】");
      
              metaObject.setValue("updateTime", LocalDateTime.now());
              metaObject.setValue("updateUser", new Long(1));
      
      
          }
      }
      
      /**
       * 工具类,用来获取当前登录用户的id
       * 这里设置为静态的可以,因为每个线程的ThreadLocal值不同,这样声明成静态的时候不同线程会赋予不同的ThreadLocal值
       * @author qin start
       * @create 2023-04-26-17:34
       */
      public class BaseContext {
      
          private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
      
          public static void set(Long id){
              threadLocal.set(id);
          }
      
          public static Long get(){
              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
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51

    2.新增分类

    1. 功能分析

      image-20230429144232838

    2. 代码实现
      image-20230429150433051

      @RestController
      @RequestMapping("/category")
      @Slf4j
      public class CategoryController {
      
          @Autowired
          private CategoryService categoryService;
          @PostMapping
          public R<String> save(@RequestBody Category category){
              log.info("新增菜品:{}",category);
              categoryService.save(category);
      
              return R.success("1");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    3.分类的分页查询

    1. image-20230429153209251

    2. 代码实现

      @GetMapping("/page")
      public R<Page> page(int page,int pageSize){
      
          //1.定义分页构造器
          Page<Category> pageInfo = new Page<>(page,pageSize);
      
          //2.定义条件构造器
          LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
          queryWrapper.orderByDesc(Category::getSort);
      
          //3.进行查询
          categoryService.page(pageInfo,queryWrapper);
      
          return R.success(pageInfo);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    4.分类删除⭐️

    1. image-20230429153552438

      image-20230429154245516

    2. 简单代码开发(第一版)

      @DeleteMapping
      public R<String> delete(long ids){//这里一定要为long类型
          log.info("删除菜品id:{}",ids);
      
          categoryService.removeById(ids);
      
          return R.success("1");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    3. 第二版代码开发思路+实现⭐️

      因为菜品``和套餐都有关联到分类的可能性,因此如果删除分类时,分类里有相应的菜品和套餐,那么要判断不能删除

      ①页面发送Ajax请求,传过来要删除菜品分类的id

      ②根据id去菜品表套餐表去查询有几条数据,如果有数据的话抛出一个自定义业务异常,提示不能删除

      ③没有业务异常的话,进行正常的删除操作

      1.增加一个业务方法

      @Service
      public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
          @Autowired
          private DishService dishService;
          @Autowired
          private SetMealService setMealService;
          @Override
          public void remove(long id) {
      
      
              //1.根据id去查询菜品表
              LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(Dish::getCategoryId,id);
      
              int count = dishService.count(queryWrapper);
              //抛出自定义业务处理异常
              if(count > 0){
                  throw new CustomException("无法删除分类,该分类下存在菜品信息");
              }
      
      
              //2.根据id去查询套餐表
              LambdaQueryWrapper<Setmeal> queryWrapper1 = new LambdaQueryWrapper<>();
              queryWrapper1.eq(Setmeal::getCategoryId,id);
      
              int count1 = setMealService.count(queryWrapper1);
              //抛出自定义业务处理异常
              if(count1 > 0){
                  throw new CustomException("无法删除分类,该分类下存在套餐信息");
              }
              //3.如果没有关联,那么调用父类ServiceImpl的方法删除
              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
      • 32
      • 33
      • 34

      2.自定义异常类和全局异常类方法

      public class CustomException extends RuntimeException{
          public CustomException(String message){
              super(message);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      //用来处理删除菜品分类信息
          @ExceptionHandler(CustomException.class)
          public R<String> exceptionHandler (CustomException ex){
      
              return R.error(ex.getMessage());
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    3.修改分类

    1. 需求分析

      image-20230429170033875
    2. 代码实现

      /**
           * 修改分类信息
           * @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
      • 12

    day04

    菜品管理相关内容

    image-20230429170227222

    1.文件上传⭐️

    如果部署上去之后,无法打开页面,先clean一下,再打开项目

    1. 需求分析

      image-20230429171112327

      image-20230429171701362

      image-20230429171908971

      • 文件过滤器会先拦截下来请求,直接返回“未登录”的信息

      image-20230429173319133

    2. 具体实现

      • MutipartFile文件名必须为file,与前端标签名保持一致

      • 文件上传后转存问题:如果文件上传后不转存到指定位置,那么默认存储在一个本地的临时文件中,程序运行后就会删除,因此要文件转存

        image-20230429174734929
      • 代码:

        注意点:①读取路径用@Value注解从配置文件中读取

        ​ ②文件名的拼接

        ​ ③文件路径的判别合法问题

        ​ ④返回文件名称

        image-20230429193358068
        @RestController
        @RequestMapping("/common")
        @Slf4j
        public class CommonController {
        
        
            //@Value注解用来取值
            @Value("${reggie.path}")
            private String basePath;
        
            /**
             * 上传文件
             * @param file  文件参数名必须为file
             * @return
             */
            @RequestMapping("/upload")
            public R<String> upload(@RequestBody MultipartFile file){
        
                log.info("上传图片:{}",file.toString());
        
                //1.生成文件名字,实现文件的保存功能,目前先保存到本地上
                //获取文件类型
                String originalFilename = file.getOriginalFilename();
                String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
                //使用UUID生成自定义文件名
                String fileName = UUID.randomUUID().toString() + 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);
        
            }
        
        }
        
        • 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

    2.文件下载

    1. 前端分析

      image-20230429194744794

    2. 代码实现

      • 注意name字段由前端传过来的参数/common/download?name=${response.data}自动赋值
      • 注意这里读取是从服务端读,发送是HttpServletResponse获取的输出去,传送回去需要先设置响应头数据格式
      @GetMapping("/download")
      //这的name由前端自动赋值
      public void downLoad(String name, HttpServletResponse response){
          //获取文件名称,从本地数据来创建一个input流进行读取,output流进行输出
          File file = new File(basePath + name);
      
          try {
              FileInputStream is = new FileInputStream(file);
      
              OutputStream outputStream = response.getOutputStream();
              response.setContentType("image/jpeg");
              //文件读写操作
              int len = 0;
              byte[] bytes = new byte[1024];
      
              while((len = is.read(bytes)) != -1){
                  outputStream.write(bytes);
                  //所储存的数据全部清空
                  outputStream.flush();
              }
              is.close();
              outputStream.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

    3.新增菜品⭐️

    1. 需求分析

      image-20230430101529774

      image-20230430101649390

      image-20230430102024179

      image-20230430102112742

      image-20230430103015854

    2. 功能实现

      • 回显菜品分类信息

        因为一进去发送地址在category下,因此要在category下编写相应的请求

        image-20230430104205431

        /**
             * 根据条件查询分类信息,并返回json数据
             * @param category
             * @return
             */
        @GetMapping("/list")
        public R<List<Category>> list(Category category){
        
            LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        
            queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
        
            queryWrapper.orderByDesc(Category::getSort).orderByDesc(Category::getUpdateTime);
        
            List<Category> list = categoryService.list(queryWrapper);
        
            return R.success(list);
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
      • 处理前端发过来的请求

        DTO继承实体类,扩展实体类

        image-20230430110544507 image-20230430111654341 image-20230430112055353
      • 将DTO中的数据保存到两张表中

        image-20230430114753750
        @Service
        @Transactional
        public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
        
            @Autowired
            DishFlavorService dishFlavorService;
        
            /**
             * 根据菜品信息,保存数据到菜品表和菜品口味表
             * @param dishDto
             */
            @Override
            public void saveWithFlavor(DishDto dishDto) {
                //保存菜品的基本信息到菜品的基本表中,如果有的菜品的属性对不上,那么不保存
                super.save(dishDto);
        
                //这里保存之后,会将表中的数据重新填回到dishDto,因此也就获取了此时的菜品id
        
                //获取新增的菜品id,为口味表中的每一个口味增加相应的菜品id
                Long id = dishDto.getId();
                //获取菜品口味
                List<DishFlavor> flavors = dishDto.getFlavors();
        
                for (DishFlavor flavor : flavors) {
                    flavor.setDishId(id);
                }
        
                //批量保存菜品口味数据到菜品口味表
                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
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
        • 30
        • 31
        • 32
        image-20230430115331270
    3. 总结分析

      • 处理前端请求
      • 后端返回值识前端需求的数据为准,后端定义了数据模型之后只要根据前端需求,将相应的需求放在R.data属性中即可

    4.菜品信息分页查询

    1. 需求分析

      • 处理分页查询中的难点在于:如何将dish表中的categoryId字段切换为categoryName,因为前端页面需要菜品分类而不是菜品ID。所以我们就要用到dishDto这个类扩展dish类,① 先从dish表中查数据然后封装到page类中 ② 将page类中的分页信息数据数据拷贝到Page这个分页信息类 ③ 将page中的records信息映射到page的分页信息类
      image-20230430150712133 image-20230430150948577 image-20230430153133938

      封装Dish数据

      image-20230430154608398 image-20230430160745688
    2. 代码实现

      • BeanUtils工具类属于org.springframework.beans.BeanUtils,是spring框架提供的工具类,简化数据封装,用于封装JavaBean,避免了大量的get,set方法进行拷贝赋值
      @GetMapping("/page")
      public R<Page> page(int page,int pageSize,String name){
      
          //1.根据名字查询分页信息
          //首先保证名字不为空
          Page<Dish> dishPageInfo = new Page<>(page,pageSize);
          Page<DishDto> dtoPageInfo = new Page<>();
      
          LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
          queryWrapper.like(name != null,Dish::getName,name);
          dishService.page(dishPageInfo,queryWrapper);
      
          //2.对dish分页信息进行拷贝
          BeanUtils.copyProperties(dishPageInfo,dtoPageInfo,"records");
      
          List<Dish> records = dishPageInfo.getRecords();
      
          //3.返回页面的新records数据,就是data数据
          List<DishDto> list = records.stream().map((item) -> {
      
              //用来将每一个item变为dishDto,然后返回给Page
              DishDto dishDto = new DishDto();
              BeanUtils.copyProperties(item,dishDto);
      
              Long categoryId = item.getCategoryId();
      
              Category category = categoryService.getById(categoryId);
              if(category != null){
                  String categoryName = category.getName();
                  dishDto.setCategoryName(categoryName);
              }
      
              return dishDto;
      
          }).collect(Collectors.toList());
      
          dtoPageInfo.setRecords(list);
      
          return R.success(dtoPageInfo);
      }
      
      • 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

    5.菜品修改信息⭐️

    • 凡是涉及对数据库数据的多次增删改(>=2次),都需要事务控制,来防止一次修改出错而接着照常执行的错误
    1. 需求分析

      image-20230430162747669
      • 保存修改数据的时候发送的请求
      image-20230430165544550
    2. 代码实现

      • DishServiceImpl —> 回显功能

        进行简单的查询两个表dish和Flavor,中间桥梁是dishID;查询数据封装到DishDto,返回给页面

        dish中有dishID,而Flavor表中有dishID,可根据dishId查询口味表

        /**
             * 根据id查询查询dishDto来赋值
             * 回显信息
             * @param id
             * @return
             */
        @Override
        public DishDto getByIdWithFlavors(Long id) {
        
            //查询dish表信息,获取对应菜品
            Dish dish = this.getById(id);
            DishDto dishDto = new DishDto();
            //拷贝到拓展对象中
            BeanUtils.copyProperties(dish,dishDto);
        
            Long dishId = dish.getId();
            LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(dishId != null,DishFlavor::getDishId,dishId);
            List<DishFlavor> list = dishFlavorService.list(queryWrapper);
        
            dishDto.setFlavors(list);
        
            //返回主角数据
            return dishDto;
        }
        
        • 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
      • 保存修改数据:

        操作:清理当前菜品口味信息,然后批量插入口味信息 —> 避免了还需要判断是清楚还是增加的麻烦

        ①更新dish表②更新口味表(先清除之间口味信息,再批量插入当前口味信息)

        DishServiceImpl

        /**
             * 修改菜品信息
             * @param dishDto
             */
        @Override
        @Transactional//保证事务一致性
        public void updateWithFlavor(DishDto dishDto) {
            //1.更新dish表中的信息
            this.updateById(dishDto);
        
        
            //清理当前菜品,然后批量插入口味信息
            //2.根据id清除相关口味信息
            Long id = dishDto.getId();
            LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(DishFlavor::getDishId,id);
        
            dishFlavorService.remove(queryWrapper);
        
            //3.批量插入口味信息,重新设置flavor信息,因为传递过来的flavor的dishId属性没赋上值
            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
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29

    day05

    1.概述

    image-20230507160706076 image-20230507160907847

    image-20230507161008896

    数据模型

    注意套餐关系表中存储的数据,一个套餐对应多个菜品,分开存储

    image-20230507161242026

    image-20230507161346118

    image-20230507161444244

    image-20230507171350454

    2.新增套餐

    1. 分析⭐️

      和上一个开发类似

      1. 前端提交过来的信息包含套餐基本信息套餐与菜品关联信息

        因此需要设置一个setmealDto,Dto中包含套餐基本信息和套餐与菜品关联信息

      2. 后端在setmealController中接收这个Dto,然后新增业务方法去处理Dto

      3. 业务方法:

        ①将dto基本信息传入到套餐基本信息表

        ②将套餐id这个对象中的list集合中的数据添加到套餐菜品表

        ③涉及操作两张表,需要加入@transactional注解,要么同时成功,要么同时失败

      image-20230507163137679

    2. 功能实现一:回显添加菜品

      根据分类categoryId,来去相应dish表中查询信息,进而回显信息

      /**
           * 根据categoryId,回显对应分类下的菜品信息
           * @param dish
           * @return
           */
      @GetMapping("/list")
      public R<List<Dish>> list(Dish dish){
      
          LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
          //两个eq信息
          queryWrapper.eq(dish != null,Dish::getCategoryId,dish.getCategoryId());
          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
      • 20

      功能实现二:

      实现添加菜品功能

      • 这里要注意setMealId在前端传过来的数据没有,需要将前端基本信息添加到SetMeal表中,才能得到相应的Id,然后为套餐菜品对象赋值上值

      SetMealServiceImpl

      @Service
      public class SetMealServiceImpl extends ServiceImpl<SetMealMapper, Setmeal> implements SetMealService {
      
          @Autowired
          private SetMealDishService setMealDishService;
          /**
           * 新增菜品套餐
           * @param setmealDto
           */
          @Override
          public void saveWithDish(SetmealDto setmealDto) {
              //1.调用setMeal本有的功能,顺便得到套餐id
              this.save(setmealDto);
      
              //2.将从数据库得到的套餐id封装回setmealDishes对象中
              List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
      
              setmealDishes.stream().map((item) -> {
                  item.setSetmealId(setmealDto.getId());
                  return item;
              }).collect(Collectors.toList());
      
      
              //3.
              setMealDishService.saveBatch(setmealDishes);
          }
      }
      
      • 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

    3.套餐分页查询

    • 功能与day04的菜品信息分类查询相似
    • 在套餐管理界面,套餐分类字段显示的是categoryId对应的中文,但在数据库里查询到的是categoryId,因此需要利用categoryId查询到categoryName,并赋值给数据传输对象SetmealDto
    /**
         * 套餐分页查询
         * @param page
         * @param pageSize
         * @param name
         * @return
         */
    @GetMapping("/page")
    public R<Page> list(int page, int pageSize, String name){
        //分页构造器对象
        Page<Setmeal> pageInfo = new Page<>(page, pageSize);
        Page<SetmealDto> dtoPage = new Page<>();
    
        //构造查询条件对象
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(name != null, Setmeal::getName, name);
    
        //操作数据库
        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);
            //获取categoryId
            Long categoryId = item.getCategoryId();
            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

    4.删除套餐信息

    1. 需求分析

      image-20230507194247399

      提供一个方法处理删除一个和删除多个请求

      image-20230507194519874

    2. 代码开发

      注意点:

      ①接受前端ids数据,传过来的数据本身是数组形式,所以加不加注解无所谓,但是List是列表,所以要加注解@RequestParam

      ②根据id删除套餐,不仅删除套餐,也删除关联套餐表中的信息

      业务逻辑:(SetMealServiceImpl

      ​ 1.查询套餐状态,确定是否可用删除
      ​ 2.如果不能删除,抛出一个业务异常,提示在售卖中
      ​ 3.如果可以删除,先删除套餐表中的数据
      ​ 4.删除关系表中的数据

    /**
         * 根据ids删除套餐信息
         * @param ids
         */
    @Override
    @Transactional
    public void removeWithDish(List<Long> ids) {
    
        //        1.查询套餐状态,确定是否可用删除
        //SQL语句: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);
        int count = this.count(queryWrapper);
        //        2.如果不能删除,抛出一个业务异常,提示**在售卖中**
        if(count > 0){
            throw new CustomException("商品还在销售,不能删除");
        }
        //        3.如果可以删除,先删除套餐表中的数据
        this.removeByIds(ids);
        //        4.删除关系表中的数据
        //根据套餐id去关系表中去查数据,然后匹配删除
        //delete from setMealDish where setmealId in ids
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
        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
    • 28
    1. 起售,停售操作(SetMealServiceImpl)

      • 这里采用遍历操作实现批量停售,起售不太好,应该用mp的具体更新方法操作,等学了mp之后再来补吧,希望还记得
    /**
         * 更改售卖状态
         * @param ids
         * @param status 1表示启售,0表示停售
         */
    @Override
    public void changeStatus(List<Long> ids, int status) {
    
        //改变售卖状态
        for (int i = 0; i < ids.size(); i++) {
            Long id = ids.get(i);
            //根据id得到每个dish菜品。
            Setmeal setmeal = this.getById(id);
            setmeal.setStatus(status);
            this.updateById(setmeal);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5.短信发送

    1. 概述

      image-20230508202550636

      image-20230508202652752

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zEC2oEPD-1685267442440)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305082027150.png)]

    2. 代码开发

      image-20230508204048569

    6.手机验证码登录

    1. 需求分析

      image-20230508204424222 image-20230508204524912 image-20230508204643066
    2. 代码开发(前期准备)

      ①对短信服务进行放行,否则会自动跳回到登录页面

      ②改写全局过滤器,对移动端用户进行验证码访问,进行放行

      image-20230508205401840 image-20230508210103884
    3. 代码开发(发送验证码)

      image-20230508211024925

      避坑:

      这个测试的时候,前端页面有问题,login.html不发送ajax请求,解决办法:把day05代码中的所有前端代码替换到自己的项目中就行了

    /**
         * 发送手机验证码
         * @param user
         */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        //1.获取手机号,并进行检验
        String phone = user.getPhone();
    
        //2.生成验证码,采用短信发送服务进行发送短信
        if(phone != null){
            String code = String.valueOf(ValidateCodeUtils.generateValidateCode(4));
            log.info("验证码为:{}",code);
    
            //            SMSUtils.sendMessage("瑞吉外卖","SMS_460725810",phone,code);
    
            //3.将发送的验证码保存在session中,便于之后查验
            session.setAttribute(phone,code);
    
            return R.success("发送验证码成功,等待查收");
    
        }
    
        //4.短信发送错误,返回错误信息;成功,返回成功信息
        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
    1. 代码开发(验证码登录)

      1. 因前端传过来的对象,后端没有相应的实体类与其对应

        这时可以采取拓展实体类dto或者是map集合的方式接收

      2. 登录方法返回值为User对象,这样让浏览器也保存一份用户信息

    /**
         * 发送手机验证码
         * @param map
         */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session){
    
        log.info("登录操作:{}",map);
        //1.获取手机号和验证码
        String phone = map.get("phone").toString();
        String code = map.get("code").toString();
        //2.进行手机号和验证码比对,如果成功进行登录的逻辑
        Object codeInSession = session.getAttribute("phone");
        if(codeInSession != null && codeInSession.equals(code)){
    
            //3.匹配结果对上之后,如果手机号在表中不存在自动完成祖册
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
    
            User user = userService.getOne(queryWrapper);
    
            if(user == null){
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
    
            session.setAttribute("id",user.getId());
    
            return R.success(user);
        }
    
        //4.短信发送错误,返回错误信息;成功,返回成功信息
        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
    • 32
    • 33
    • 34
    • 35
    • 36

    day06

    这一天都是移动端开发

    1.地址簿相关功能

    1. 需求分析
    image-20230509144211007 image-20230509144350441
    /**
     * 地址簿管理
     */
    @Slf4j
    @RestController
    @RequestMapping("/addressBook")
    public class AddressBookController {
    
        @Autowired
        private AddressBookService addressBookService;
    
        /**
         * 新增
         */
        @PostMapping
        public R<AddressBook> save(@RequestBody AddressBook addressBook) {
            addressBook.setUserId(BaseContext.get());
            log.info("addressBook:{}", addressBook);
            addressBookService.save(addressBook);
            //把返回的信息,交给前端存起来
            //每次前端已查询
            return R.success(addressBook);
        }
    
        /**
         * 设置默认地址
         */
        @PutMapping("default")
        public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
            log.info("addressBook:{}", addressBook);
            LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
            wrapper.eq(AddressBook::getUserId, BaseContext.get());
            wrapper.set(AddressBook::getIsDefault, 0);
            //SQL:update address_book set is_default = 0 where user_id = ?
            addressBookService.update(wrapper);
    
            addressBook.setIsDefault(1);
            //SQL:update address_book set is_default = 1 where id = ?
            addressBookService.updateById(addressBook);
            return R.success(addressBook);
        }
    
        /**
         * 根据id查询地址
         */
        @GetMapping("/{id}")
        public R get(@PathVariable Long id) {
            AddressBook addressBook = addressBookService.getById(id);
            if (addressBook != null) {
                return R.success(addressBook);
            } else {
                return R.error("没有找到该对象");
            }
        }
    
        /**
         * 查询默认地址
         */
        @GetMapping("default")
        public R<AddressBook> getDefault() {
            LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(AddressBook::getUserId, BaseContext.get());
            queryWrapper.eq(AddressBook::getIsDefault, 1);
    
            //SQL:select * from address_book where user_id = ? and is_default = 1
            AddressBook addressBook = addressBookService.getOne(queryWrapper);
    
            if (null == addressBook) {
                return R.error("没有找到该对象");
            } else {
                return R.success(addressBook);
            }
        }
    
        /**
         * 查询指定用户的全部地址
         */
        @GetMapping("/list")
        public R<List<AddressBook>> list(AddressBook addressBook) {
            addressBook.setUserId(BaseContext.get());
            log.info("addressBook:{}", addressBook);
    
            //条件构造器
            LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
            queryWrapper.orderByDesc(AddressBook::getUpdateTime);
    
            //SQL:select * from address_book where user_id = ? order by update_time desc
            return R.success(addressBookService.list(queryWrapper));
        }
    }
    
    • 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

    2.菜品展示

    1. 需求分析

      image-20230509151017659

      ①因为发送两次请求,第二次失败,所以展示信息有误,这里将第二次的改用假数据

      ②存在问题:页面发送的请求为http://localhost:8080/dish/list?categoryId=1397844263642378242&status=1,这个list请求在Controller中只设置了返回List类型,而Dish实体类中没有相应的口味信息,因此在前端页面上不会显示口味信息,所以要拓展dish实体类,返回dishDto

      image-20230509151305523
      1. 修改list方法,返回dishDto的代码
      
      /**
           * 根据categoryId,回显对应分类以及菜品口味信息
           * @param dish
           * @return
           */
      @GetMapping("/list")
      public R<List<DishDto>> list(Dish dish){
      
          LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
          //两个eq信息
          queryWrapper.eq(dish != null,Dish::getCategoryId,dish.getCategoryId());
          queryWrapper.eq(Dish::getStatus,1);
      
          //添加排序条件
          queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
      
      
      
      
      
          List<Dish> list = dishService.list(queryWrapper);//查出来所有菜品信息
      
          //将每一条信息都
          List<DishDto> dishDtoList = list.stream().map((item) ->{
              DishDto dishDto = new DishDto();
      
              //1.对象拷贝(每一个list数据)
              BeanUtils.copyProperties(item,dishDto);
              Long categoryId = item.getCategoryId();  //分类id
              //通过categoryId查询到category内容
              Category category = categoryService.getById(categoryId);
              //判空
              if(category != null){
                  String categoryName = category.getName();
                  dishDto.setCategoryName(categoryName);
              }
      
              //2.将菜品口味信息赋值给dto
      
              Long id = item.getId();
              LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
              dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,id);
      
              List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
      
              dishDto.setFlavors(dishFlavors);
              return dishDto;
          }).collect(Collectors.toList());
      
          return R.success(dishDtoList);
      }
      
      • 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
      1. 设置setMeal方法,用于展示套餐
      /**
           * 用于移动端展示数据
           * @param setmeal
           * @return
           */
      @GetMapping("/list")
      public R<List<Setmeal>> list(Setmeal setmeal){
          //创建条件构造器
          LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
          //添加条件
          queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
          queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
      
          //排序
          queryWrapper.orderByDesc(Setmeal::getUpdateTime);
      
          List<Setmeal> list = setMealService.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
      • 20

    3.加入购物车

    1. 数据分析

      image-20230514190548926

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-btcgqkdD-1685267442442)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305141908129.png)]

    2. 代码开发

      image-20230514191402298

      设置userId,确定哪个用户点的,然后判断number数据类型

      @PostMapping("/add")
      public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
      
          log.info("购物车数据:{}",shoppingCart);
          //1.将当前用户的id设置进数据库中
          Long currentId = BaseContext.get();
          shoppingCart.setUserId(currentId);
      
          LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
          queryWrapper.eq(ShoppingCart::getUserId,currentId);
      
          //2.判断当前传过来的是菜品信息还是套餐信息。用于后续判断当前菜品或者套餐是否在购物车中
          if(shoppingCart.getDishId() != null){
              queryWrapper.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
          }else{
              queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
          }
      
          //select * from shoppingCart where userId = ? and ...
      
          ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
          //3.进行查询记录,如果能查到记录,那么说明数据库中有,需要在原有基础上+1
          if(cartServiceOne != null){
              Integer number = cartServiceOne.getNumber();
              number += 1;
              cartServiceOne.setNumber(number);
      
              shoppingCartService.updateById(cartServiceOne);
          }else{
              shoppingCart.setNumber(1);
              shoppingCartService.save(shoppingCart);
              cartServiceOne.setNumber(1);
          }
      
          return R.success(cartServiceOne);
      }
      
      • 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
      1. 展示购物车信息 /list

        根据userId,去查购物车返回list集合就可

        按照登录id去数据库中查询信息
        不同用户的userid不同,所以购物车信息不同

    4.用户下单⭐️

    用到了很多张表,具有可学习性

    1. 订单表插入数据 — 从购物表中算出总金额 查询用户信息
    2. 订单明细表中插入多条数据 — 从购物表得出
    1. 需求分析

      image-20230514200754788

      image-20230514200949626

      订单明细表

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMTzZb4b-1685267442445)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305142010371.png)]

      image-20230514201311038
    2. 代码开发

      image-20230514202902569

      用户下单分析一:(下单之后,存入订单表订单明细表中)

      ​ 传递参数:

      image-20230514203123913

      ​ 不需要传递过来购物车信息用户id,因为在登陆过程中已经知道用户id且购物车信息可根据用户id查询出来

      用户下单分析二:

      ​ 获得当前用户id

      ​ 查询当前用户的购物车数据
      ​ 向订单表插入数据,一条数据
      ​ 向订单明细表插入数据,多条数据
      ​ 清空购物车数据

    2.项目优化

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3E0jDZj-1685267442447)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305262033884.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wz3p48bs-1685267442447)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305262034094.png)]

    day01

    1.引入版本控制

    1. 先创建一个远程的空仓库,复制链接
    2. 本地先创建仓库,然后连接远程仓库推送
    3. 创建两个分支,masterv1.0v1.0用于开发缓存内容,开发完成后,合并到master分支

    2.环境搭建

    1. springboot-redis-starter

    2. redis文件配置

    3. 设置redis配置类,便于观察

      image-20230526205721492

    3.短信验证码(Redis)

    1. 实现思路

      image-20230527145138807

    4.缓存菜品数据

    1. 实现思路

      主从一致

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAWU5g3p-1685267442449)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271513707.png)]

    2. 实现代码

      • 查询菜品
      1. 先根据前端传递过来的categoryId和status构造一个key,从缓存数据库中获取,如果能获取到,那么直接返回
      2. 获取不到dishDto,进行1去数据库查询,然后放入缓存数据库,然后返回
      //查询方法
      @GetMapping("/list")
      public R<List<DishDto>> list(Dish dish){
      
      
          List<DishDto> dishDtoList = null;
      
          //1.第一次访问构造key,存入缓存数据库中
          String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();
      
          //2.从缓存数据库中拿数据
          dishDtoList = (List<DishDto>)redisTemplate.opsForValue().get(key);
      
          //3.判断缓存数据库中有没有,有的话直接去拿数据
          if(dishDtoList != null){
              return R.success(dishDtoList);
          }
          //4.如果没有的话,去数据库查询
          //.....
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      //修改方法
      @PostMapping
      public R<String> add(@RequestBody DishDto dishDto){
      
          log.info(dishDto.toString());
      
          dishService.saveWithFlavor(dishDto);
      
          //添加后后删除数据库中对应的key  删除dish开头+dishid
          String key = "dish_" + dishDto.getCategoryId() + "_1";
          redisTemplate.delete(key);
      
          return R.success("新增菜品成功");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    5.SpringCache

    1. 概述

      image-20230527161919195

      常用注解

      • 使用spring cache

        使用哪种缓存技术,就导入对应的包,然后开启注解即可

      • 使用jar包

        使用基础功能的缓存,导入web包即可

        使用基于redis等缓存技术,那么要导入spring-boot-starter-cache

      image-20230527162123446

    2. 普通cache使用

      • 普通的缓存在加注解之后放在一个线程安全的map中,基于内存

      • 初始化缓存方式

        ①注入cachemanager,加上注解缓存操作(获取方法内参数,通过SPEL),缓存的对象的类要实现序列化接口

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrJVSACv-1685267442450)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271636494.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-icyXONac-1685267442450)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271638756.png)]

      删除缓存

      image-20230527164258799

      image-20230527164659100

      查询数据

      image-20230527171111789

    3. redis使用

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gUFBCIaN-1685267442452)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271707236.png)]

    6.使用springCache缓存套餐数据

    image-20230527173236005

    缓存的数据:分类 + 不同种类

    image-20230527173222161

    day02

    1.MySQL主从复制

    1. 介绍

      实现读写分离,减轻单台数据库压力和防止主数据库数据损毁

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZYOYNoS-1685267442454)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271739718.png)]

    2. 原理—从库和主库通过日志做一样的操作

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgG6J8Kg-1685267442454)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271745822.png)]

    3. 操作步骤

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLGSubZH-1685267442454)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271859784.png)]

      第二步:重启MySQL

      image-20230527190321174

      #mysql8第三步执行
      create user xiaoming identified by 'Root@123456';
      grant replication slave on *.* to xiaoming;
      
      • 1
      • 2
      • 3

      MySQL主从复制略过了。。。之后有时间再来补吧

    2.Nginx

    1.概述

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gebXbH18-1685267442455)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271928408.png)]

    image-20230527192940107

    image-20230527193143181

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0j5WVZVf-1685267442456)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271939160.png)]

    image-20230527194046874
    2.基本命令

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s0W8kKqq-1685267442457)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305271942072.png)]

    ./nginx -v查看版本

    ./nginx -t检查配置文件是否正确

    image-20230527194733965 image-20230527194947935
    3.配置文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KB1hPHBa-1685267442458)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281346785.png)]

    4.niginx具体应用⭐️
    image-20230528135048897
    1.部署静态资源
    1. 配置信息在config文件里
    2. 服务可开多个

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mk1fM9XZ-1685267442458)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281352592.png)]

    2.反向代理

    正向代理是在客户端设置,反向代理是在服务端设置

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGfUbUtV-1685267442459)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281401642.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KvR5Nqlq-1685267442459)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281404563.png)]

    实现反向代理 ----转发

    image-20230528140925690
    3.负载均衡

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZuNYVVu-1685267442460)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281413916.png)]

    实现

    image-20230528141438705

    负载均衡算法

    • 默认轮询算法

      image-20230528141758552

    day03

    1.前后端分离

    1.概述

    image-20230528142351102

    image-20230528142402008

    2.前后端分离开发

    变化:前后端代码不再混合在一个工程中

    image-20230528142655203
    1. 开发流程

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgWOZ1cB-1685267442461)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281432025.png)]

    3.YApi

    提供API接口数据

    4.Swagger(生成接口文档)
    1. 介绍

      image-20230528144310779

    2. 具体实现

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehhnnKDk-1685267442463)(https://weifengqin-image.oss-cn-nanjing.aliyuncs.com/img/202305281443013.png)]

      image-20230528153404988

      image-20230528153510796
    3. 功能

      因为包扫描,所以能真正地controller

      image-20230528154200657
    4. 常用注解

      目的是为了生成接口的时候,个性化定制

      image-20230528154826832

      image-20230528154915644

    2.项目部署

    部署架构

    • 前端项目
    image-20230528163421208 image-20230528163525532
    1. 部署静态资源nginx上

      将前端静态资源文件放在nginx的html文件夹下,然后修改nginx配置信息,映射静态文件

      image-20230528165109869
    2. 配置反向代理

      image-20230528165556650

      image-20230528165542289

    • 后端项目

      1. image-20230528170753241

      2. 打包项目 部署工程

        (如果访问后端项目,访问超时,可能是因为数据库的问题,可能远端没有配置数据库)

    • 总结

      之后再启动项目,先启动前端项目,再启动后端项目
      .aliyuncs.com/img/202305281417603.png" alt=“image-20230528141758552” style=“zoom:33%;” />

    day03

    1.前后端分离

    1.概述

    [外链图片转存中…(img-RglKcxwc-1685267442460)]

    [外链图片转存中…(img-gqfy4Vyd-1685267442461)]

    2.前后端分离开发

    变化:前后端代码不再混合在一个工程中

    image-20230528142655203
    1. 开发流程

      [外链图片转存中…(img-MgWOZ1cB-1685267442461)]

    3.YApi

    提供API接口数据

    4.Swagger(生成接口文档)
    1. 介绍

      [外链图片转存中…(img-OcSGU8NN-1685267442462)]

    2. 具体实现

      [外链图片转存中…(img-ehhnnKDk-1685267442463)]

      [外链图片转存中…(img-qddnjUoD-1685267442464)]

      image-20230528153510796
    3. 功能

      因为包扫描,所以能真正地controller

      image-20230528154200657
    4. 常用注解

      目的是为了生成接口的时候,个性化定制

      [外链图片转存中…(img-OkPdI6g0-1685267442464)]

      image-20230528154915644

    2.项目部署

    部署架构

    • 前端项目
    image-20230528163421208 image-20230528163525532
    1. 部署静态资源nginx上

      将前端静态资源文件放在nginx的html文件夹下,然后修改nginx配置信息,映射静态文件

      image-20230528165109869
    2. 配置反向代理

      image-20230528165556650

      [外链图片转存中…(img-mFWI408T-1685267442465)]

    • 后端项目

      1. [外链图片转存中…(img-sitC0vrN-1685267442465)]

      2. 打包项目 部署工程

        (如果访问后端项目,访问超时,可能是因为数据库的问题,可能远端没有配置数据库)

    • 总结

      之后再启动项目,先启动前端项目,再启动后端项目

  • 相关阅读:
    基于VHDL的专业略缩词
    CSS函数-BFC介绍-原理详解
    现在的湖仓一体像是个伪命题
    el-menu-item使用自定义图标、使用图片做图标
    AnolisOS升级SSH,不升级SSL
    页面打印功能,单选框多选框选中后,打印时不显示选中效果
    python 练习--更新
    考研408,1000道精心打磨的计算机考研题,选它就对了!
    深入了解Linux内核MMU管理机制
    基于C++的朴素贝叶斯分类器
  • 原文地址:https://blog.csdn.net/m0_63941306/article/details/130915347