当前位置: 首页 > news >正文

42-瑞吉外卖(SpingBoot+MyBatisPlus)

42-瑞吉外卖-heima-笔记


笔记内容来自黑马程序员视频内容

文章目录

  • 42-瑞吉外卖-heima-笔记
  • 一、瑞吉外卖项目概述
    • ①:软件开发整体介绍
      • 01. 软件开发流程
      • 02. 角色分工
      • 03. 软件环境
    • ②:瑞吉外卖项目介绍
      • 01. 项目介绍
      • 02. 产品原型展示
      • 03. 技术选型
      • 04. 功能架构
      • 05. 角色
    • ③:开发环境搭建
      • 01. 数据库环境搭建
      • 02. 创建SpringBoot项目
      • 03. 导入静态资源
    • ④:后台登录功能开发
      • 01. 需求分析
      • 02. 代码开发(Controller,Service,Mapper、实体类)
      • 03. 代码开发(导入返回结果类R )
      • 04. 代码开发(实现登录逻辑处理)
      • 05. 代码开发(功能测试)
    • ⑤:后台退出功能开发
      • 01. 需求分析
      • 02. 代码实现
      • 03. 退出功能测试
  • 二、 员工管理业务开发
    • ①:完善登录功能
      • 01. 问题分析
      • 02. 代码实现(创建过滤器)
      • 03. 代码实现(完善过滤器的处理逻辑)
      • 04. 功能测试
    • ②:新增员工
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发&测试
      • 04. 小结
    • ③:员工信息分页查询
      • 01. 需求分析
      • 02. 分析程序执行流程
      • 03. 功能测试
    • ④:启用/禁用员工账号
      • 01. 需求分析
      • 02. 分析程序执行过程
      • 03. 代码开发&测试
      • 04 . 代码修复&测试
    • ⑤:编辑员工信息
      • 01. 需求分析
      • 02. 分析程序执行流程
      • 03. 代码开发
      • 04. 功能测试
  • 三、分类管理业务开发
    • ①:公共字段自动填充
      • 01. 问题分析
      • 02. 代码实现
      • 03. 功能完善
      • 04. 功能测试
    • ②:新增分类
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发(Controller,Service,Mapper、实体类)
      • 04. 分析程序执行流程
    • ③:分类信息分页查询
      • 01. 需求分析
      • 02. 分析程序执行流程
      • 03. 代码实现
    • ④:删除分类
      • 01. 需求分析
      • 02. 分析程序执行流程
      • 03. 功能完善
      • 04. 关键代码
      • 05. 功能测试
    • ⑤:修改分类
      • 01. 需求分析
      • 02. 代码实现
  • 四、菜品管理业务开发
    • ①:文件上传下载
      • 01. 文件上传介绍
      • 02. 文件下载介绍
      • 03. 文件上传代码实现
      • 04. 文件下载代码实现
    • ②:新增菜品
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发(Controller,Service,Mapper、实体类)
      • 04. 代码开发
    • ③:菜品信息分页查询
      • 01. 需求分析
      • 02. 代码开发 &(梳理交互过程)
      • 03. 功能测试
    • ④:修改菜品
      • 01. 需求分析
      • 02. 代码开发 (数据回显)
      • 03. 代码开发(保存修改)
      • 04. 功能测试
    • ⑤:停售/起售菜品
      • 01. 代码实现
      • 02. 功能测试
    • ⑥:删除菜品
      • 01. 代码实现
      • 02. 功能测试
  • 五、套餐管理业务开发
    • ①:新增套餐
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发 (Controller,Service,Mapper、实体类)
      • 04. 代码开发(数据回显)
      • 05. 代码开发(保存数据)
    • ②:套餐信息分页查询
      • 01.需求分析
      • 02.代码开发
      • 03. 功能测试
    • ③:套餐停售 / 起售功能
      • 01.代码开发
    • ④:修改套餐信息
      • 01. 代码开发(数据回显)
      • 02. 代码开发(保存数据)
    • ⑤:删除套餐
      • 01. 需求分析
      • 02. 代码实现
  • 六、手机验证码登录
    • ①:短信发送
      • 01.短信服务介绍
      • 02. 阿里云短信服务-介绍
      • 03.阿里云短信服务-注册账号
      • 04.阿里云短信服务-添加权限
      • 05.阿里云短信服务-添加参数手机号
      • 06. 代码开发
    • ②:手机验证码登录
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发(交互过程)
  • 七、菜品展示、购物车、下单
    • ①:导入用户地址簿相关功能代码
      • 01. 需求分析
      • 02. 数据模型
      • 03. 导入功能代码
      • 04. 代码开发
    • ②:菜品展示
      • 01. 需求分析
      • 02. 代码开发
    • ③:购物车
      • 01. 需求分析
      • 02.数据模型
      • 03. 代码开发 (Controller,Service,Mapper、实体类)
      • 04. 代码开发(添加购物车)
      • 05. 代码开发(查看购物车)
      • 06. 菜品或套餐数量减一
      • 07. 清空购物车
    • ④:用户下单
      • 01. 需求分析
      • 02. 数据模型
      • 03. 代码开发 (Controller,Service,Mapper、实体类)
      • 04. 代码开发(功能实现)
    • ⑤:拓展功能
      • 01. 退出账号
      • 02. 最新订单和历史订单
      • 03.客户端订单详情
      • 04. 订单派送
  • 八、Git课程
  • 九、Linux课程
  • 十、Redis课程
  • 十一、缓存优化
    • ①:问题说明
    • ②:环境搭建
      • 01. 使用Git管理工程
      • 02.Maven坐标
      • 03.配置文件
      • 04. 配置类
      • 05. 提交并推送到远程仓库
    • ③:缓存短信验证码
      • 01. 实现思路
      • 02.功能实现
    • ④:缓存菜品数据
      • 01.缓存菜品数据(添加缓存)
      • 02. 功能测试
      • 03.缓存菜品数据(清理缓存)
      • 04. 功能测试
    • ⑤:将代码提交到本地并推送到远程仓库
    • ⑥:Spring Cache
      • 01. Spring Cache 介绍
      • 02.Spring Cache常用注解
      • 03. @CachePut注解 使用方式
        • 1. 数据准备
        • 2. 代码实现
        • 3. 功能测试
      • 04. @CacheEvict 注解使用方法
      • 05. @Cacheable注解使用方法
      • 06. Spring cache 使用方法
    • ⑦:缓存套餐数据
      • 01.实现思路
    • ⑧:将代码推送到Gir仓库并合并到主分支
  • 十二、读写分离(优化)
    • ①:Mysql主从复制
      • 01. 介绍
      • 02.配置-前置条件
      • 03. 配置-主库Master
      • 04.配置-从库Slave
      • 05. 测试MySQL主从复制
    • ②:读写分离案例
      • 01. 背景
      • 02. Sharding-JDBC介绍
      • 03.入门案例
        • 1.数据准备
        • 2. 测试
    • ③:项目实现读写分离
      • 01. 数据库环境准备(主从复制)
      • 02. 代码改造
      • 03. 测试
  • 十三、Nginx
    • ①:Nginx概述
    • ②:Nginx下载与安装
    • ③:Nginx目录结构
    • ④:Nginx命令
      • 01. 查看版本
      • 02. 检查配置文件正确性
      • 03. 启动和停止
      • 04. 重新加载配置文件
    • ⑤:Nginx配置文件结构
      • 01.整体结构介绍
    • ⑥Nginx具体应用
      • 01. 部署静态资源
      • 02. 反向代理
        • 1. 正向代理
        • 2.反向代理
      • 03.负载均衡
      • 04. 负载均衡配置
  • 十四、 前后端分离开发
    • ①:问题分析
    • ②:前后端分离开发
      • 01. 介绍
      • 02. 开发流程
    • ③:前端技术栈
    • ④:Yapi
      • 01.介绍
      • 02.使用
    • ⑤:Swagger
      • 01.介绍
      • 02.使用方式
      • 03. 常用注解
    • ⑥:项目部署
      • 01.部署架构
      • 02.部署环境说明
      • 03. 部署前端项目
      • 04.部署后端项目
      • 05. 部署后端项目(图片资源)


一、瑞吉外卖项目概述

①:软件开发整体介绍

01. 软件开发流程

在这里插入图片描述

02. 角色分工

在这里插入图片描述

03. 软件环境

在这里插入图片描述

②:瑞吉外卖项目介绍

01. 项目介绍

在这里插入图片描述

02. 产品原型展示

在这里插入图片描述

03. 技术选型

在这里插入图片描述

04. 功能架构

在这里插入图片描述

05. 角色

在这里插入图片描述

③:开发环境搭建

01. 数据库环境搭建

1. 创建数据库
在这里插入图片描述
2. 导入表结构(资料/数据模型/db_reggie.sql) (方式一:)
在这里插入图片描述在这里插入图片描述
3. 方式二:使用命令行执行sql文件
在这里插入图片描述

02. 创建SpringBoot项目

1. 创建项目
在这里插入图片描述 在这里插入图片描述
2. 导入依赖
 <dependency>
     <groupId>commons-lang</groupId>
     <artifactId>commons-lang</artifactId>
     <version>2.6</version>
 </dependency>

 <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>2.0.10</version>
 </dependency>

 <!--        mybatis-plus启动器-->
 <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.5.1</version>
 </dependency>
3. yml文件配置
server:
  # 配置端口号
  port: 8080
spring:
  application:
    # 应用的名称,可选
    name: reggie_take_out
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    username: root
    password: root

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 在映射实体或者属性时,将数据库中表名和字段名中的下创线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: assign_id
4. 创建启动类
在这里插入图片描述
@Slf4j
@SpringBootApplication
public class ReggieTakeOutApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReggieTakeOutApplication.class, args);

        log.info("项目启动成功~~~");
    }
}

03. 导入静态资源

1. 方式一:(直接放到static目录下)
在这里插入图片描述
2. 方式二:直接放到resource目录下
在这里插入图片描述
3. 解决方案(资源文件映射)
在这里插入图片描述
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始静态资源映射");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
}

④:后台登录功能开发

01. 需求分析

1. 登录页面展示(http://localhost:8080/backend/page/login/login.html)
在这里插入图片描述
2. 查看登录请求信息
在这里插入图片描述 在这里插入图片描述
3. 请求流程
在这里插入图片描述
4. 注意(响应数据)
在这里插入图片描述

02. 代码开发(Controller,Service,Mapper、实体类)

1. 创建实体类Employee,和employee表进行映射(entity包下)
package com.it.entity;

@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;//身份证号码

    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

}

2. 创建EmployeeMapper(mapper包下)
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
3. 创建EmployeeService(service包下)及实现类EmployeeServiceImpl(service.impl包下)
  • EmployeeService
public interface EmployeeService extends IService<Employee> {
}
  • EmployeeServiceImpl
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
4. 创建EmployeeController(controller包下)
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    
}

03. 代码开发(导入返回结果类R )

此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面

1. 创建common包 将返回结果类R 放到common包下
@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

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

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

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

}

04. 代码开发(实现登录逻辑处理)

在这里插入图片描述

1. 创建登录方法
在这里插入图片描述
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){
        // 1、将页面提交的密码password.进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        // 2、根据页面提交的用户名username查询数据库
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        // 例: eq("name", "老王")--->name = '老王'
        queryWrapper.eq(Employee ::getUsername,employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

        // 3、如果没有查询到则返回登录失败结果
        if (emp == null) {
            return R.error("账号不存在!");
        }

        // 4、密码比对,如果不一致则返回登录失败结果
        if (!emp.getPassword().equals(password)){
            return R.error("密码错误!");
        }

        // 5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if (emp.getStatus() == 0){
            return R.error("该账号已被禁用!");
        }

        // 6、登录成功,将员工id存入Session并返回登录成功结果
         request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }

}

05. 代码开发(功能测试)

1. 修改超时时间(修改后最好清除一下缓存)
在这里插入图片描述
2. 测试一:账号错误 密码正确
在这里插入图片描述
2. 测试二:账号正确 密码错误
在这里插入图片描述
3. 测试三:手动修改数据库 (账号状态修改为 0 禁用)
在这里插入图片描述
4. 测试四: 正常登录 (账号状态在修改为1)
在这里插入图片描述

⑤:后台退出功能开发

01. 需求分析

在这里插入图片描述

02. 代码实现

1. 在EmployeeController中创建退出方法
    /**
     * 员工退出
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        // 清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功!");
    }

03. 退出功能测试

1. 登录成功时信息
在这里插入图片描述
2. 退出登录后的信息
在这里插入图片描述

二、 员工管理业务开发

①:完善登录功能

01. 问题分析

前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:

用户如果不登录,直接访问系统首页面,照样可以正常访问

这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。

那么,具体应该怎么实现呢?

答案就是: 使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面

02. 代码实现(创建过滤器)

1. 在filter包下创建LoginCheckFilter类
/**
 * 检查用户是否已经完成了登录
 */

@Slf4j
 @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        log.info("拦截到请求:{}", request.getRequestURI());
        filterChain.doFilter(request,response);
    }
}
2. 在启动类上添加 @ServletComponentScan 注解
在这里插入图片描述
3. 测试
在这里插入图片描述

03. 代码实现(完善过滤器的处理逻辑)

过滤器具体的处理逻辑如下:

1、获取本次请求的URI

2、判断本次请求是否需要处理

3、如果不需要处理,则直接放行

4、判断登录状态,如果已登录,则直接放行

5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据

在这里插入图片描述

1. 注意:用户未登录时 后端进行响应数据
在这里插入图片描述
2. 代码实现
/**
 * 检查用户是否已经完成了登录
 */

@Slf4j
 @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    // 路径匹配器, 支持通配符
    public static final AntPathMatcher PATH_PATTERN = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //  1、获取本次请求的URI
        String requestUri = request.getRequestURI();

        log.info("拦截到请求:{}", requestUri);

        // 定义不需要拦截的请求路径
        String[] urls = new String[] {
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };

        //  2、判断本次请求是否需要处理
        boolean check = check(urls, requestUri);

        //  3、如果不需要处理,则直接放行
        if (check) {
            log.info("本次请求不需要处理:{}", requestUri);
            filterChain.doFilter(request,response);
            return;
        }

        //  4、判断登录状态,如果已登录,则直接放行
        Object employee = request.getSession().getAttribute("employee");
        if (employee != null){
            log.info("用户已登录,用户Id为:{}", employee);
            filterChain.doFilter(request,response);
            return;
        }

        log.info("用户未登录");

        //  5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

    /**
     * 路径匹配, 检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls, String requestURI){
        for (String url : urls) {
            boolean match = PATH_PATTERN.match(url, requestURI);
            if (match) {
                return true;
            }
        }
        return false;
    }
}

04. 功能测试

1. 测试:
在这里插入图片描述

②:新增员工

01. 需求分析

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击 添加员工 按钮跳转到新增页面,如下:
在这里插入图片描述

02. 数据模型

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的

03. 代码开发&测试

在开发代码之前,需要梳理一下整个程序的执行过程:

  • 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  • 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  • Service调用Mapper操作数据库,保存数据
1. 请求地址 方式 数据格式
在这里插入图片描述
2. 代码实现
    /**
     * 新增员工
     * @param request
     * @param employee
     * @return
     */
    @PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());

        // 设置初始密码 123456, 需要进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        // 获取当前用户的Id
        Long empId = (Long)request.getSession().getAttribute("employee");
        employee.setCreateUser(empId);
        employee.setUpdateUser(empId);
        employeeService.save(employee);

        return R.success("新增员工成功!");
    }
3. 添加测试
在这里插入图片描述
4. 再次新增(同样的账号)

前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan' for key 'employee.idx_username''

此时需要我们的程序进行异常捕获,通常有两种处理方式:

  • 1、在Controller方法中加入try.catch进行异常捕获

  • 2、使用异常处理器进行全局异常捕获

5. 捕获异常代码实现(在common包下创建GlobalExceptionHandler类)
/**
 * 全局异常处理
 */

@Slf4j
@ResponseBody
// 捕获定义以下注解的类
@ControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.info(ex.getMessage());
        return R.error("用户名已存在!");
    }
}
6. 再次测试 新增(同样的账号)
在这里插入图片描述
7. 完善全局异常处理器(代码实现)
/**
 * 全局异常处理
 */

@Slf4j
@ResponseBody
// 捕获定义以下注解的类
@ControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.info(ex.getMessage());
        // 如果异常信息 Duplicate entry 'zhangsan' for key 'employee.idx_username' 中包含Duplicate entry
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" "); // 按照空格进行分割
            String tipe = split[2] + " 已存在!";   // 获取到已经存在的账号并拼接错误提示信息
            return R.error(tipe);
        }
        return R.error("未知错误!");
    }
}

8. 再一次测试:新增(同样的账号)
在这里插入图片描述

04. 小结

  • 1、根据产品原型明确业务需求
  • 2、重点分析数据的流转过程和数据格式
  • 3、通过debug断点调试跟踪程序执行过程
在这里插入图片描述

③:员工信息分页查询

01. 需求分析

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

在这里插入图片描述

02. 分析程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

  • 页面发送ajax请求,将分页查询参数(page.pageSize、name)提交到服务端
  • 服务端Controller接收页面提交的数据并调用Service查询数据
  • Service调用Mapper操作数据库,查询分页数据
  • Controller将查询到的分页数据响应给页面
  • 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
1. 配置MP分页插件
/**
 * 配置MybatisPlus分页插件
 */
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}
2. 员工信息分页查询(代码实现)
    /**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name){
        log.info("page={}, pageSize={}, name={}",page,pageSize,name);

        // 构造分页构造器
        Page<Employee> pageInfo = new Page<Employee>(page, pageSize);

        // 构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();

        // 添加过滤条件
        queryWrapper.like(StringUtils.isNotBlank(name),Employee::getName,name);

        // 添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        // 执行查询
        employeeService.page(pageInfo, queryWrapper);

        return R.success(pageInfo);
    }
}

03. 功能测试

1. 测试一
在这里插入图片描述
2. 测试二
在这里插入图片描述

④:启用/禁用员工账号

01. 需求分析

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。

需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。

02. 分析程序执行过程

页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?

1. 判断用户是否为admin 否则不显示 禁用和启用按钮
在这里插入图片描述

在开发代码之前,需要梳理一下整个程序的执行过程:

1、页面发送ajax请求,将参数(id、 status)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service更新数据

3、Service调用Mapper操作数据库

1. 请求方式 路径 数据
在这里插入图片描述
2. 页面中的ajax请求是如何发送的?
在这里插入图片描述

03. 代码开发&测试

1. 根据id修改员工信息
    /**
     * 根据Id修改员工信息
     * @param request
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> updateById(HttpServletRequest request,@RequestBody Employee employee){
        // 更新修改时间和修改人
        employee.setUpdateTime(LocalDateTime.now());
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(empId);
        // 执行sql修改信息
        employeeService.updateById(employee);
        return R.success("员工信息已更新");
    }

测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。观察控制台输出的SQL:

2. SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同
在这里插入图片描述

04 . 代码修复&测试

通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?

  • 分页查询时服务端响应给页面的数据中id的值为19位数字,类型为long

  • 页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id就改变了

  • 前面我们已经发现了问题的原因,即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。

如何解决这个问题?

  • 我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。

具体实现步骤:

1. 提供对象转换器JacksonobjectMapper,基于Jackson进行Java对象到json数据的转换
(资料中已经提供,直接复制到项目中使用)(放在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);
    }
}

2. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
    /**
     * 扩展Mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展小写转换器");
        // 创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        // 设置对象转换器,底层使用Jackson将Java对象转为Json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        // 将上面的消息转换器对象追加到Mvc框架的转换器集合中
        converters.add(0,messageConverter);
    }
3. 测试(修改成功)
在这里插入图片描述

⑤:编辑员工信息

01. 需求分析

在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作

02. 分析程序执行流程

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

2、在add.html页面获取url中的参数[员工id]

3、发送ajax请求,请求服务端,同时提交员工id参数

4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7、服务端接收员工信息,并进行处理,完成后给页面响应

8、页面接收到服务端响应信息后进行相应处理

03. 代码开发

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
        log.info("根据id查询员工信息");
        Employee employee = employeeService.getById(id);
        if (employee != null) {
            return R.success(employee);
        }
        return R.error("没有查询到对应员工信息");
    }

04. 功能测试

1. 数据回显
在这里插入图片描述
2. 修改数据
在这里插入图片描述

三、分类管理业务开发

①:公共字段自动填充

01. 问题分析

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:
在这里插入图片描述
在这里插入图片描述

能不能对于这些公共字段在某个地方统一处理,来简化开发呢?答案就是使用Mybatis Plus提供的公共字段自动填充功能。

02. 代码实现

Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

1. 在实体类的属性上加入@TableField注解,指定自动填充的策略
    @TableField(fill = FieldFill.INSERT) // 插入时填充字段
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)  // 插入和更新时填充字段
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)  // 插入时填充字段
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)   // 插入和更新时填充字段
    private Long updateUser;
2. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口(在common包下)
/**
 * 自定义元数据对象处理器
 */

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段填充【insert~~】");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser",new Long(1));
        metaObject.setValue("updateUser",new Long(1));

    }

    /**
     * 更新操作自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段填充【update~~】");
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",new Long(1));
    }
}
3. 优化 添加 和修改方法(删除不需要的代码)
    /**
     * 新增员工
     * @param request
     * @param employee
     * @return
     */
    @PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());

        // 设置初始密码 123456, 需要进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

       /* employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());*/
        // 获取当前用户的Id
/*        Long empId = (Long)request.getSession().getAttribute("employee");
        employee.setCreateUser(empId);
        employee.setUpdateUser(empId);*/
        employeeService.save(employee);

        return R.success("新增员工成功!");
    }
    /**
     * 根据Id修改员工信息
     * @param request
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> updateById(HttpServletRequest request,@RequestBody Employee employee){
/*        // 更新修改时间和修改人
        employee.setUpdateTime(LocalDateTime.now());
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(empId);*/
        // 执行sql修改信息

        employeeService.updateById(employee);
        return R.success("员工信息已更新");
    }

03. 功能完善

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。

有的同学可能想到,用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?

注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。

可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类

在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

1、LoginCheckFilter的doFilter方法

2、EmployeeContraller的update方法

3、MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id):

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
在这里插入图片描述

什么是ThreadLocal?

ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

public void set(T value) 设置当前线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值

我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。

实现步骤:
1、编写BaseContext工具类,基于ThreadLocal封装的工具类
2、在LogincheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3、在MyMeta0bjectHandler的方法中调用BaseContext获取登录用户的id

1. 基于ThreadLocal封装的工具类BaseContext (在common包下)
/**
 * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
 */

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

    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}
2. 在LoginCheckFilter过滤器中存储当前用户id到ThreadLocal中
在这里插入图片描述
3. 在MyMetaObjectHandler中获取ThreadLocal中存储的id并进行自动填充
在这里插入图片描述

04. 功能测试

换一个用户登录 进行修改操作 看updateUser值能否自动填充
在这里插入图片描述

②:新增分类

01. 需求分析

后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

菜品分类套餐分类
在这里插入图片描述在这里插入图片描述

02. 数据模型

新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:
在这里插入图片描述

03. 代码开发(Controller,Service,Mapper、实体类)

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

1. 实体类Category(直接从课程资料中导入即可)
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 分类
 */
@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //类型 1 菜品分类 2 套餐分类
    private Integer type;
    
    //分类名称
    private String name;
    
    //顺序
    private Integer sort;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    
    //是否删除
   // private Integer isDeleted;
}
2. Mapper接口CategoryMapper
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
3. 业务层接口CategoryService
public interface CategoryService extends IService<Category> {
}
4. 业务层实现类CategoryServicelmpl
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
5. 控制层CategoryController
@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;
}

04. 分析程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存

3、Service调用Mapper操作数据库,保存数据

可以看到新增菜品分类新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可

在这里插入图片描述在这里插入图片描述
1. 菜品分类新增方法
@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

	 /**
     * 新增分类方法
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category){
        categoryService.save(category);
        return R.success("新增分类成功!");
    }

}

③:分类信息分页查询

01. 需求分析

系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

02. 分析程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

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

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

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

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

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

在这里插入图片描述

03. 代码实现

    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize){
        // 分页构造器
        Page<Category> categoryPage = new Page<>(page, pageSize);
        // 条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        // 添加排序条件,根据sort排序
        queryWrapper.orderByAsc(Category ::getSort);
        // 进行分页查询
        categoryService.page(categoryPage,queryWrapper);
        return R.success(categoryPage);
    }

④:删除分类

01. 需求分析

在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

02. 分析程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

1. 页面发送ajax请求,将参数(id)提交到服务端
在这里插入图片描述
2. 服务端Controller接收页面提交的数据并调用Service删除数据
3. Service调用Mapper操作数据库
    /**
     * 根据Id删除信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long ids){
        categoryService.removeById(ids);
        return R.success("分类信息删除成功!");
    }

03. 功能完善

前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。

要完善分类删除功能,需要先准备基础的类和接口:

1.实体类Dish和Setmeal (从课程资料中复制即可)
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 菜品
 */
@Data
public class Dish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    
    //菜品名称
    private String name;
    
    //菜品分类id
    private Long categoryId;
    
    //菜品价格
    private BigDecimal price;

    //商品码
    private String code;
    
    //图片
    private String image;

    //描述信息
    private String description;

    //0 停售 1 起售
    private Integer status;

    //顺序
    private Integer sort;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    //是否删除
    private Integer isDeleted;
}
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 套餐
 */
@Data
public class Setmeal implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    
    //分类id
    private Long categoryId;

    //套餐名称
    private String name;

    //套餐价格
    private BigDecimal price;

    //状态 0:停用 1:启用
    private Integer status;

    //编码
    private String code;

    //描述信息
    private String description;

    //图片
    private String image;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    //是否删除
    private Integer isDeleted;
}
2. 创建Mapper接口DishMapper和SetmealMapper
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}
3. 创建Service接口DishService和SetmealService
public interface DishService extends IService<Dish> {
}
public interface SetmealService extends IService<Setmeal> {
}
4. 创建Service实现类DishServicelmpl和SetmealServicelmpl
@Slf4j
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

}
@Slf4j
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
    
}

04. 关键代码

1. 注销CategoryController中根据Id删除信息的删除方法
    /**
     * 根据Id删除信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long ids){
       //  categoryService.removeById(ids);
        return R.success("分类信息删除成功!");
    }
2. 在CategoryService添加remove方法
public interface CategoryService extends IService<Category> {
    public void remove(Long id);
}
3. 自定义异常类(在common包下)
/**
 * 自定义异常类
 */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}
4. 在全局异常处理器GlobalExceptionHandler添加
    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex) {
        log.info(ex.getMessage());

        return R.error(ex.getMessage());
    }
5. 在CategoryController中添加根据Id删除信息的删除方法
    /**
     * 根据Id删除信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long ids){
       //  categoryService.removeById(ids);

        categoryService.remove(ids);
        return R.success("分类信息删除成功!");
    }
6. 在CategoryServicelmpl实现remove方法
@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. 判断是否关联菜品
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 1.2 添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish ::getCategoryId,id );
        long count1 = dishService.count(dishLambdaQueryWrapper);
        // 1.3 判断 >0 说明该分类下关联有菜品 不能删除 抛异常
        if (count1 > 0){
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }
        // 2. 判断是否关联套餐
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 2.2 添加查询条件,根据分类id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        long count2 = setmealService.count(setmealLambdaQueryWrapper);
        // 2.3 判断 >0 说明该分类下关联有套餐 不能删除 抛异常
        if (count2 > 0){
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }

        // 3. 正常删除
        super.removeById(id);
    }
}

05. 功能测试

1. 删除没有关联菜品和套餐的分类
在这里插入图片描述
2. 删除有关联菜品和套餐的分类
在这里插入图片描述

⑤:修改分类

01. 需求分析

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

02. 代码实现

1. 在CategoryController添加修改方法
    /**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @PutMapping
    public R<String> updateById(@RequestBody Category category){
        log.info("修改分类信息:{}",category);

        categoryService.updateById(category);
        return R.success("分类信息修改成功!");
    }

四、菜品管理业务开发

①:文件上传下载

01. 文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

1. 文件上传时,对页面的form表单有如下要求:
在这里插入图片描述

目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。例如ElementUI中提供的upload上传组件:

在这里插入图片描述

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件。

02. 文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

03. 文件上传代码实现

文件上传,页面端可以使用ElementuI提供的上传组件。

可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html

1. 将页面放在src/main/resources/backend/page/demo包下(需创建demo包)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件上传</title>
  <!-- 引入样式 -->
  <link rel="stylesheet" href="../../plugins/element-ui/index.css" />
  <link rel="stylesheet" href="../../styles/common.css" />
  <link rel="stylesheet" href="../../styles/page.css" />
</head>
<body>
   <div class="addBrand-container" id="food-add-app">
    <div class="container">
        <el-upload class="avatar-uploader"
                action="/common/upload"
                :show-file-list="false"
                :on-success="handleAvatarSuccess"
                :before-upload="beforeUpload"
                ref="upload">
            <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
    </div>
  </div>
    <!-- 开发环境版本,包含了有帮助的命令行警告 -->
    <script src="../../plugins/vue/vue.js"></script>
    <!-- 引入组件库 -->
    <script src="../../plugins/element-ui/index.js"></script>
    <!-- 引入axios -->
    <script src="../../plugins/axios/axios.min.js"></script>
    <script src="../../js/index.js"></script>
    <script>
      new Vue({
        el: '#food-add-app',
        data() {
          return {
            imageUrl: ''
          }
        },
        methods: {
          handleAvatarSuccess (response, file, fileList) {
              this.imageUrl = `/common/download?name=${response.data}`
          },
          beforeUpload (file) {
            if(file){
              const suffix = file.name.split('.')[1]
              const size = file.size / 1024 / 1024 < 2
              if(['png','jpeg','jpg'].indexOf(suffix) < 0){
                this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')
                this.$refs.upload.clearFiles()
                return false
              }
              if(!size){
                this.$message.error('上传文件大小不能超过 2MB!')
                return false
              }
              return file
            }
          }
        }
      })
    </script>
</body>
</html>
2. 创建CommonController,负责文件上传与下载
@Slf4j
@RequestMapping("/common")
@RestController
public class CommonController {

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
        log.info("上传的文件:{}",file.toString());
        return null;
    }

}
2. MultipartFile定义的file变量必须与name保持一致
在这里插入图片描述
3. 在yml文件中定义文件存储路径
reggie:
  path: D:\OOP\java\develop_idea\06_reggie\reggie_take_out\src\main\resources\static\img\
4. 完整代码 (CommonController)
@Slf4j
@RequestMapping("/common")
@RestController
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
        log.info("上传的文件:{}",file.toString());
        // 1. 获取原始文件名
        String originalFilename = file.getOriginalFilename(); // aaa.jpg
        // 2. 截取后缀
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));  // .jpg
        // 3. 使用UUID随机生成文件名,防止因为文件名相同造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;

        // 4. 创建一个目录对象
        File dir = new File(basePath);
        // 4.2 判断当前目录是否存在
        if (!dir.exists()){
            // 4.3 目录不存在就创建
            dir.mkdirs();
        }

        try {
            // 5. 将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
}

04. 文件下载代码实现

1. 文件下载,页面端可以使用标签展示下载的图片
在这里插入图片描述
2. 代码实现
    /**
     * 文件下载
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void downlaod(String name, HttpServletResponse response){
        try {
            // 输入流,通过输入流读取文件内容
            FileInputStream inputStream = new FileInputStream(basePath + name);
            // 输出流,通过输出流将文件写回浏览器,在浏览器中展示图片
            ServletOutputStream outputStream = response.getOutputStream();
            
            response.setContentType("image/jpeg");

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

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
3. 测试
在这里插入图片描述

②:新增菜品

01. 需求分析

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

02. 数据模型

1. dish(菜品表)
在这里插入图片描述
2. dish_flavor(菜品口味表)
在这里插入图片描述

03. 代码开发(Controller,Service,Mapper、实体类)

1. 实体类DishFlavor
(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
菜品口味
 */
@Data
public class DishFlavor implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //菜品id
    private Long dishId;
    
    //口味名称
    private String name;
    
    //口味数据list
    private String value;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    
    //是否删除
    private Integer isDeleted;
}
2. Mapper接口DishFlavorMapper
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}

3. 业务层接口DishFlavorService
public interface DishFlavorService extends IService<DishFlavor> {
}

4. 业务层实现类 DishFlavorServicelmpl
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}
5. 控制层 DishController
@RestController
@RequestMapping("/dish")
public class DishController {
    @Autowired
    private DishService dishService;
    
    @Autowired
    private DishFlavorService dishFlavorService;
}

04. 代码开发

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

2、页面发送请求进行图片上传,请求服务端将图片保存到服务器

3、页面发送请求进行图片下载,将上传的图片进行回显

4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

1. 菜品分类下拉框(在CategoryController添加)
    /**
     * 根据条件查询数据
     * @param category
     * @return
     */
    @GetMapping("/list")
    public R<List<Category>> list(Category category){
        // 条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        // 添加条件(根据type分类查询)
        queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
        // 添加排序条件
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        // 执行sql
        List<Category> categoryList = categoryService.list(queryWrapper);
        return R.success(categoryList);
    }
2. 导入DishDto(位置:资料/dto),用于封装页面提交的数据
import com.it.entity.Dish;
import com.it.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

注意: DTO,全称为Data Transfer object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

新增菜品同时插入菜品对应的口味数据,需要操作两张表:dish、dishflavor

3. 在DishService接口中添加方法saveWithFlavor,在DishServiceImpl实现
public interface DishService extends IService<Dish> {
    void saveWithFlavor(DishDto dishDto);
}
@Slf4j
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 保存菜品,同时保存对应的口味
     * @param dishDto
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        // 1. 保存菜品基本信息到菜品表dish
        this.save(dishDto);

        Long dishId = dishDto.getId();
        // 2. 获取菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        // 3. 给菜品口味设置id
        flavors = flavors.stream().map((item) ->{
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        // 保存菜品口味到菜品数据表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}
4. 由于以上代码涉及多表操作,在启动类上开启事务支持添加@EnableTransactionManagement注解
在这里插入图片描述
5. 在DishController中添加保存菜品方法
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 保存菜品,同时保存对应的口味
     * @param dishDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功!");
    }

}
6. 测试
在这里插入图片描述在这里插入图片描述

③:菜品信息分页查询

01. 需求分析

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在这里插入图片描述

02. 代码开发 &(梳理交互过程)

在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

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

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

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

1. 代码开发
    /**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name){
        // 构造分页构造器对象
        Page<Dish> dishPage = new Page<>(page,pageSize);
        // 条件构造器
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        // 添加过滤条件
        queryWrapper.like(StringUtils.isNotBlank(name),Dish::getName,name)
                // 添加排序条件
                .orderByDesc(Dish::getUpdateTime);
        // 执行分页查询
        dishService.page(dishPage,queryWrapper);

        List<Dish> records = dishPage.getRecords();

        List<DishDto> dishDtos = records.stream().map((item) ->{
            Long categoryId = item.getCategoryId(); // 分类id
            Category category = categoryService.getById(categoryId); // 根据分类Id查询分类对象
            String categoryName = category.getName(); // 获取分类对象中的分类名称
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto,"flavors"); // 对象拷贝
            dishDto.setCategoryName(categoryName);
            return dishDto;
        }).collect(Collectors.toList());

        Page<DishDto> dishDtoPage = new Page<>();
        dishDtoPage.setRecords(dishDtos);
        dishDtoPage.setTotal(dishPage.getTotal());

        return R.success(dishDtoPage);
    }

03. 功能测试

1. 测试
在这里插入图片描述在这里插入图片描述

④:修改菜品

01. 需求分析

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

02. 代码开发 (数据回显)

在开发代码之前,需要梳理一下修改菜品时前端页面( add.html)和服务端的交互过程:

1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示

2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显

1.在DishService中添加getDishWithFlavor方法
    // 根据菜品id查询 菜品分类和对应口味
    DishDto getDishWithFlavor(Long id);
2. 在DishServiceImpl添加getDishWithFlavor方法
    /**
     * 根据菜品id查询 菜品分类和对应口味
     * @param id
     * @return
     */
    @Override
    public DishDto getDishWithFlavor(Long id) {
        // 查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);
        DishDto dishDto = new DishDto();
        // 对象拷贝
        BeanUtils.copyProperties(dish,dishDto);

        // 查询当前菜品对应的口味信息,从dish_flavor表查询
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper);
        dishDto.setFlavors(dishFlavors);

        return dishDto;
    }
3. DishController处理Get请求
    /**
     * 根据菜品id查询 菜品分类和对应口味
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<DishDto> getDaDtoById(@PathVariable Long id){
        DishDto dishDto = dishService.getDishWithFlavor(id);

        return R.success(dishDto);
    }
4. 测试
在这里插入图片描述

03. 代码开发(保存修改)

点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

1. 在DishService中定义修改方法
    // 修改菜品分类及对应口味
    void updateWithFlavor(DishDto dishDto);
2. 在DishServiceImpl中实现修改方法
    /**
     * 修改菜品分类及对应口味
     * @param dishDto
     */
    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto){
        // 1. 更新dish表基本信息
        this.updateById(dishDto);
        // 2. 清理当前菜品对应口味数据  dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
        dishFlavorService.remove(queryWrapper);

        // 3. 添加当前提交过来的口味数据--dish_flavor表的insert操作
        // 3.2. 获取菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        // 3.3. 给菜品口味设置id
        flavors = flavors.stream().map((item) ->{
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());
        // 4. 保存菜品口味到菜品数据表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
3. 在DishController添加put方法
    /**
     * 修改菜品分类及对应口味
     * @param dishDto
     * @return
     */
    @PutMapping
    public R<String> updateWithFlavor(@RequestBody DishDto dishDto){
        dishService.updateWithFlavor(dishDto);
        return R.success("菜品信息修改成功!");
    }

04. 功能测试

修改前修改后
在这里插入图片描述在这里插入图片描述

⑤:停售/起售菜品

01. 代码实现

1. 在DishController添加updateStatus方法
    /**
     * 停售与起售
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> updateStatus(@PathVariable int status, String[] ids){
        for (String id : ids) {
            Dish dish = dishService.getById(id);
            dish.setStatus(status);
            dishService.updateById(dish);
        }
        return R.success("已" + (status == 0 ? "停售" : "起售"));
    }

02. 功能测试

1. 测试
在这里插入图片描述

⑥:删除菜品

01. 代码实现

1. 在DishController添加deleteById方法
    /**
     * 删除菜品
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> deleteById(String[] ids){
        for (String id : ids) {
            dishService.removeById(id);
        }
        return R.success("删除成功!");
    }

02. 功能测试

在这里插入图片描述

五、套餐管理业务开发

①:新增套餐

01. 需求分析

套餐就是菜品的集合。

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

02. 数据模型

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

套餐表套餐菜品关系表
在这里插入图片描述在这里插入图片描述

03. 代码开发 (Controller,Service,Mapper、实体类)

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面+ 课程中已经导入过了)
  • DTO SetmealDto(直接从课程资料中导入即可)
  • Mapper接口SetmealDishMapper
  • 业务层接口SetmealDishService
  • 业务层实现类SetmealDishServicelmpl
  • 控制层SetmealController
1. SetmealDish 实体类
/**
 * 套餐菜品关系
 */
@Data
public class SetmealDish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //套餐id
    private Long setmealId;

    //菜品id
    private Long dishId;

    //菜品名称 (冗余字段)
    private String name;

    //菜品原价
    private BigDecimal price;

    //份数
    private Integer copies;
    
    //排序
    private Integer sort;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    //是否删除
    private Integer isDeleted;
}
2. SetmealDto
@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}
3. 创建SetmealDishMapper接口
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
4. 创建SetmealDishService接口
public interface SetmealService extends IService<Setmeal> {
}
5. 创建SetmealDishServiceImpl实现类
@Slf4j
@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}

6. 创建SetmealController
/**
 * 套餐管理
 */
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishService setmealDishService;

}

04. 代码开发(数据回显)

在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:

  1. 页面(backend/ page/comboladd.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
在这里插入图片描述
  1. 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
  2. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
1. 在DishController添加list方法
    /**
     * 根据条件查询对应菜品的数据
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<Dish>> getDishById(Dish dish){
        // 构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
        // 添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
       // 添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);
        return R.success(list);
    }
2. 测试
在这里插入图片描述

05. 代码开发(保存数据)

1. 在SetmealService中添加保存接口
public interface SetmealService extends IService<Setmeal> {
    // 新增套餐,同时需要保存套餐和菜品的关联关系
    void saveWithDish(SetmealDto setmealDto);
}
2. 在SetmealServiceImpl中实现接口方法
@Slf4j
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {

    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Override
    public void saveWithDish(SetmealDto setmealDto) {
        // 1. 保存套餐的基本信息
        this.save(setmealDto);

        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes.stream().map((item) ->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        // 2. 保存套餐与菜品的关联信息
        setmealDishService.saveBatch(setmealDishes);
    }

}
3. 在SetmealController中实现保存功能
/**
 * 套餐管理
 */
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
        setmealService.saveWithDish(setmealDto);

        return R.success("套餐信息添加成功~");
    }

}
4. 测试
在这里插入图片描述在这里插入图片描述

②:套餐信息分页查询

01.需求分析

系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在这里插入图片描述

02.代码开发

1. 在SetmealService中添加接口方法
    // 套餐信息分页查询
    Page page(int page, int pageSize, String name);
2. 在SetmealServiceImpl中完成接口的实现
    /**
     * 套餐信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @Override
    @Transactional
    public Page page(int page, int pageSize, String name) {
        // 构造分页构造器
        Page<Setmeal> pageSetmeal = new Page<>(page,pageSize);
        Page<SetmealDto> dtoPage = new Page<>();

        // 构造查询构造器
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        // 根据name模糊查询
        queryWrapper.like(name != null,Setmeal::getName,name)
                // 按照创建时间排序
                .orderByDesc(Setmeal ::getUpdateTime);
        // 执行sql查询分页信息
        Page<Setmeal> setmealPage = setmealService.page(pageSetmeal, queryWrapper);
        List<Setmeal> records = setmealPage.getRecords();
        List<SetmealDto> collect = records.stream().map((item) -> {
            Long categoryId = item.getCategoryId(); 
            Category category = categoryService.getById(categoryId); // 根据id查询category信息
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(item, setmealDto); // 对象拷贝
            if (category != null) {
                setmealDto.setCategoryName(category.getName()); // 将分类名称添加到setmealDto类中
            }
            return setmealDto;
        }).collect(Collectors.toList());

        dtoPage.setTotal(pageSetmeal.getTotal());
        dtoPage.setRecords(collect); // 总信息条数
        return dtoPage;
    }
3. 在SetmealController中完成业务功能的开发
    /**
     * 套餐信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name){
        Page<SetmealDto> setmealDtoPage = setmealService.page(page, pageSize, name);
        return R.success(setmealDtoPage);
    }

03. 功能测试

1. 测试
在这里插入图片描述在这里插入图片描述

③:套餐停售 / 起售功能

在这里插入图片描述

01.代码开发

1. 在SetmealController中添加停售与起售方法
    /**
     * 套餐 停售与起售
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> status(@PathVariable int status, @RequestParam List<Long> ids){
        // 构造条件构造器
        LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper<>();
        // 构造条件
        updateWrapper.set(Setmeal::getStatus,status).in(Setmeal::getId,ids);
        // 执行sql
        setmealService.update(updateWrapper);
        return R.success("状态修改成功!");
    }

④:修改套餐信息

01. 代码开发(数据回显)

1. SetmealService定义接口方法
    // 套餐修改数据回显
    SetmealDto getByIdDish(Long id);
2. SetmealServiceImpl中完成接口实现
    /**
     * 套餐修改数据回显
     * @param id
     * @return
     */
    @Override
    public SetmealDto getByIdDish(Long id) {
        //查询套餐基本信息
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto = new SetmealDto();
        // 对象拷贝
        BeanUtils.copyProperties(setmeal, setmealDto);

        //查询 菜品信息
        LambdaQueryWrapper<SetmealDish> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
        List<SetmealDish> list = setmealDishService.list(queryWrapper);

        setmealDto.setSetmealDishes(list);
        return setmealDto;
    }
3. SetmealController完成业务功能
    /**
     * 套餐修改数据回显
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Setmeal> getById(@PathVariable Long id){
        SetmealDto setmealDto=setmealService.getByIdDish(id);
        return R.success(setmealDto);
    }

02. 代码开发(保存数据)

1. SetmealService中定义接口方法
    // 保存修改套餐,同时需要保存套餐和菜品的关联关系
    void saveWithSetmentAndDish(SetmealDto setmealDto);
2. SetmealServiceImpl中完成接口的实现
    /**
     * 保存修改套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Override
    public void saveWithSetmentAndDish(SetmealDto setmealDto) {
        // 1. 修改setmeal表基本信息
        this.updateById(setmealDto);
        // 2. 清理当前套餐对应菜品数据 setmeal_dish表的delete操作
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish ::getSetmealId,setmealDto.getId());
        setmealDishService.remove(queryWrapper);
        // 3. 添加页面提交过来的菜品信息
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        // 3.2 给菜品设置id
        setmealDishes = setmealDishes.stream().peek((item) -> item.setSetmealId(setmealDto.getId())).collect(Collectors.toList());
        setmealDishService.saveBatch(setmealDishes);
    } 
3. SetmealController中万层业务功能
    /**
     * 保存修改套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     * @return
     */
    @PutMapping
    public R<String> saveWithSetmentAndDish(@RequestBody SetmealDto setmealDto){
        setmealService.saveWithSetmentAndDish(setmealDto);
        return R.success("修改成功!");
    }

⑤:删除套餐

01. 需求分析

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

02. 代码实现

在这里插入图片描述

开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

1. SetmealService中定义接口方法
    // 删除套餐信息
    void deleteWithSetmentAndDish(List<Long> ids);
2. SetmealServiceImpl中完成接口的实现
    /**
     * 删除套餐信息
     * @param ids
     */
    @Override
    @Transactional
    public void deleteWithSetmentAndDish(List<Long> ids) {
        // 判断套餐状态是否停售状态
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal ::getId,ids)
                .eq(Setmeal ::getStatus,1);
        long count = this.count(queryWrapper);
        if (count > 0) {
            // 套餐正在售卖中不能,删除
            throw new CustomException("删除失败,套餐正在售卖中!");
        }
        // 删除套餐菜品表数据
        LambdaQueryWrapper<SetmealDish> queryWrapper1 = new LambdaQueryWrapper<>();
        queryWrapper1.in(SetmealDish::getDishId,ids);
        setmealDishService.remove(queryWrapper1);
        // 删除套餐表数据
        this.removeBatchByIds(ids);
    }
3. SetmealController中万层业务功能
    /**
     * 删除套餐信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> deleteWithSetmentAndDish(@RequestParam List<Long> ids) {
        setmealService.deleteWithSetmentAndDish(ids);
        return R.success("删除成功!");
    }

六、手机验证码登录

①:短信发送

01.短信服务介绍

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。

常用短信服务:

阿里云
华为云
腾讯云
京东
梦网
乐信

02. 阿里云短信服务-介绍

阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。

应用场景:

验证码
短信通知
推广短信

03.阿里云短信服务-注册账号

阿里云官网: https://www.aliyun.com/

短信签名是短信发送者的署名,表示发送方的身份。

1. 阿里云短信服务-设置短信签名
注册成功后,点击登录按钮进行登录。登录后进入短信服务管理页面,选择国内消息菜单:
在这里插入图片描述
2. 阿里云短信服务-设置短信模板(切换到【模板管理】标签页:)
在这里插入图片描述
3. 短信模板包含短信发送内容、场景、变量信息。
在这里插入图片描述
4. 阿里云短信服务-设置AccessKey
光标移动到用户头像上,在弹出的窗口中点击【AccessKey管理】∶
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

04.阿里云短信服务-添加权限

1.
在这里插入图片描述
2.
在这里插入图片描述
3. 选择服务
在这里插入图片描述在这里插入图片描述

05.阿里云短信服务-添加参数手机号

在这里插入图片描述

06. 代码开发

使用阿里云短信服务发送短信,可以参照官方提供的文档即可。
https://help.aliyun.com/document_detail/112148.html

1. 导入maven坐标
<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>aliyun-java-sdk-core</artifactId>
  <version>4.5.16</version>
</dependency>
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
    <version>1.1.0</version>
</dependency>

2. 调用API (项目资料中的SMSUtils)
package com.it.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
 * 短信发送工具类
 */
public class SMSUtils {

	/**
	 * 发送短信
	 * @param signName 签名
	 * @param templateCode 模板
	 * @param phoneNumbers 手机号
	 * @param param 参数
	 */
	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
		IAcsClient client = new DefaultAcsClient(profile);

		SendSmsRequest request = new SendSmsRequest();
		request.setSysRegionId("cn-hangzhou");
		request.setPhoneNumbers(phoneNumbers);
		request.setSignName(signName);
		request.setTemplateCode(templateCode);
		request.setTemplateParam("{\"code\":\""+param+"\"}");
		try {
			SendSmsResponse response = client.getAcsResponse(request);
			System.out.println("短信发送成功");
		}catch (ClientException e) {
			e.printStackTrace();
		}
	}

}

②:手机验证码登录

01. 需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。

手机验证码登录的优点:

方便快捷,无需注册,直接登录
使用短信验证码作为登录凭证,无需记忆密码
安全

登录流程:
输入手机号>获取验证码>输入验证码>点击登录>登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识。

02. 数据模型

通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:
在这里插入图片描述

03. 代码开发(交互过程)

在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:

1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信

2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求

开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

实体类User(直接从课程资料中导入即可)
Mapper接口UserMapper
业务层接口UserService
业务层实现类UserServicelmpl
控制层UserController
工具类SMSutils、 ValidateCodeutils(直接从课程资料中导入即可)
1.User实体类
/**
 * 用户信息
 */
@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //姓名
    private String name;

    //手机号
    private String phone;
    
    //性别 0 女 1 男
    private String sex;
    
    //身份证号
    private String idNumber;
    
    //头像
    private String avatar;

    //状态 0:禁用,1:正常
    private Integer status;
}
2. UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

3. UserService
public interface UserService extends IService<User> {
}

4. UserServiceImpl
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

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

前面我们已经完成了LogincheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。
在这里插入图片描述

6. LoginCheckFilter过滤器添加
//        4-2、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("user") != null) {
    log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("user"));

    Long userId= (Long) request.getSession().getAttribute("user");

    BaseContext.setCurrentId(userId);

    filterChain.doFilter(request, response);
    return;
}

7.由于资料中代码不全需要补充以下代码
  • login.html
 const regex = /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;
                    if (regex.test(this.form.phone)) {
                        this.msgFlag = false
                        this.form.code = (Math.random()*1000000).toFixed(0)
                        sendMsgApi({phone:this.form.phone})
                    }else{
                        this.msgFlag = true
                    }
  • login.js
function sendMsgApi(data) {
    return $axios({
        'url':'/user/sendMsg',
        'method':'post',
        data
    })
}
8. UserController处理post请求(发送验证码的请求)
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        // 获取手机号
        String phone = user.getPhone();
        if (StringUtils.isNotBlank(phone)){
            // 生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("手机号{}验证码:{}",phone,code);
            // 调用阿里云提供的短信服务API完成发送短信
            SMSUtils.sendMessage("阿里云短信测试","SMS_154950909",phone,code);
            // 需要将生成的验证码保存到Session
            session.setAttribute(phone,code);
            return R.success("短信发送成功!");
        }
        return R.error("短信发送失败!");
    }

}
9.修改login.html中的带代码
loginApi({phone:this.form.phone})改成loginApi(this.form)
 async btnLogin(){
                    if(this.form.phone && this.form.code){
                        this.loading = true
                        const res = await loginApi(this.form)
10. 在UserController编写login处理post请求
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session){
        // 1.获取手机号
        String phone = map.get("phone").toString();
        // 2. 获取验证码
        String code = map.get("code").toString();
        // 3.从session中获取保存的验证码
       Object codeInSession = session.getAttribute(phone);
        // 4. 进行验证码的比对(页面提交的验证码和Session中保存的验证码对比)
        if (codeInSession!=null && codeInSession.equals(code)){
            // 判断用户是否位新用户
            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("user",user.getId());
            return R.success(user);
        }
        log.info(map.toString());
        return R.error("登录失败!");
    }
11. 测试
在这里插入图片描述

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

①:导入用户地址簿相关功能代码

01. 需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
在这里插入图片描述

02. 数据模型

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
在这里插入图片描述

03. 导入功能代码

功能代码清单:

实体类AddressBook(直接从课程资料中导入即可)
Mapper接口AddressBookMapper
业务层接口AddressBookService
业务层实现类AddressBookServicelmpl
控制层AddressBookController(直接从课程资料中导入即可)
1. 实体类
/**
 * 地址簿
 */
@Data
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //用户id
    private Long userId;

    //收货人
    private String consignee;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //省级区划编号
    private String provinceCode;

    //省级名称
    private String provinceName;

    //市级区划编号
    private String cityCode;
    
    //市级名称
    private String cityName;

    //区级区划编号
    private String districtCode;

    //区级名称
    private String districtName;

    //详细地址
    private String detail;

    //标签
    private String label;

    //是否默认 0 否 1是
    private Integer isDefault;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    //是否删除
    private Integer isDeleted;
}
2. AddressBookMapper
@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {
}
3. AddressBookService
public interface AddressBookService extends IService<AddressBook> {
}
4. AddressBookServiceImpl
@Slf4j
@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
}
5. AddressBookController
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController{
    @Autowired
    private AddressBookService addressBookService;
}

04. 代码开发

@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增地址
     * @param addressBook
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody AddressBook addressBook){
        // 保存当前用户的id
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info(addressBook.toString());
        addressBookService.save(addressBook);
        return R.success("保存成功!");
    }

    /**
     * 查询指定用户的全部地址
     * @return
     */
    @GetMapping("/list")
    public R<List<AddressBook>> list() {
        // 获取当前用户的Id
        Long currentId = BaseContext.getCurrentId();
        // 条件构造器
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(null!=currentId,AddressBook::getUserId,currentId)
                .orderByDesc(AddressBook::getUpdateTime);
        // 执行SQl
        List<AddressBook> addressBookList = addressBookService.list(queryWrapper);
        return R.success(addressBookList);
    }

    /**
     * 设置默认地址
     * @param addressBook
     * @return
     */
    @PutMapping("/default")
    public R<AddressBook> defaultValue(@RequestBody AddressBook addressBook){
        // 1. 先将该用户的所有地址都重置为非默认地址
        LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        wrapper.set(AddressBook::getIsDefault, 0);
        //SQL:update address_book set is_default = 0 where user_id = ?
        addressBookService.update(wrapper);

        // 2. 将当前地址设为默认地址
        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        addressBookService.updateById(addressBook);
        return R.success(addressBook);
    }


    /**
     * 根据id查询地址(回显数据)
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<AddressBook> get(@PathVariable Long id){
        AddressBook addressBook = addressBookService.getById(id);
        if (null != addressBook){
            return R.success(addressBook);
        }
        return R.error("没有查询到该对象");
    }

    /**
     * 修改地址
     * @param addressBook
     * @return
     */
    @PutMapping
    public R<String> put(@RequestBody AddressBook addressBook){
        addressBookService.updateById(addressBook);
        return R.success("修改成功!");
    }

    /**
     * 根据id删除地址
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> remove(@RequestParam List<Long> ids) {
        addressBookService.removeBatchByIds(ids);
        return R.success("删除成功!");
    }

    /**
     * 查询默认地址
     */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        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);
        }
    }

}

②:菜品展示

01. 需求分析

用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息需要展示 [选择规格] 按钮,否则显示 [+] 按钮。
在这里插入图片描述

02. 代码开发

在开发代码之前,需要梳理一下前端页面和服务端的交互过程:

1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)

2、页面发送ajax请求,获取第一个分类下的菜品或者套餐

开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

注意: 首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:

1. front/api/main.js中做以下修改
//获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        // 'url': '/shoppingCart/list', 原来的地址
        'url': '/front/cartData.json',
        'method': 'get',
        params:{...data}
    })
}
2. 添加front/cartData.json文件
json文件内容如下
{"code": 1,"msg": null,"data": [],"map": {}}
3. 测试
在这里插入图片描述
3. 修改DishController中的方法
    /**
     * 根据条件查询对应菜品的数据
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<DishDto>> getDishById(Dish dish){
        // 构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
        // 添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
       // 添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);
        DishDto dishDto = new DishDto();
        // 获取分类名称
        List<DishDto> dishDtoList = list.stream().map((item) -> {
            BeanUtils.copyProperties(item, dishDto);
            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);
            if (category == null) {
                dishDto.setCategoryName(category.getName());
            }
            // 获取口味数据
            Long dishId = item.getId();
            LambdaQueryWrapper<DishFlavor> queryWrapper1 = new LambdaQueryWrapper<>();
            queryWrapper1.eq(DishFlavor::getDishId,dishId).orderByDesc(DishFlavor::getUpdateTime);
            List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper1);
            dishDto.setFlavors(dishFlavors);
            return dishDto;
        }).collect(Collectors.toList());

        return R.success(dishDtoList);
    }
4. 测试
在这里插入图片描述
5. 展示套餐下的菜品(SetmealController中添加以下方法)
    /**
     * 根据分类id查询套餐信息
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    public R<List<Setmeal>> getSetmealDto(Setmeal setmeal){
        // 查询套餐基本信息
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null, Setmeal ::getStatus,1 );
        List<Setmeal> setmealList = setmealService.list(queryWrapper);
        return R.success(setmealList);
    }
6. 测试
在这里插入图片描述

③:购物车

01. 需求分析

移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击 [+] 将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。在这里插入图片描述

02.数据模型

购物车对应的数据表为shopping_cart表,具体表结构如下:
在这里插入图片描述

03. 代码开发 (Controller,Service,Mapper、实体类)

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

1、点击 [加入购物车] 或者 [+] 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车

2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐

3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

实体类ShoppingCart(直接从课程资料中导入即可)
Mapper接口ShoppingCartMapper
业务层接口ShoppingcartService
业务层实现类ShoppingCartServicelmpl
控制层ShoppingCartController

04. 代码开发(添加购物车)

@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 菜品或者套餐信息保存到购物车
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        // 1. 获取用户id
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        // 2. 判断当前是菜品还是套餐
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart ::getUserId,currentId);

        if (shoppingCart.getDishId() != null){
            // 2.2 菜品信息
            queryWrapper.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
        }else{
            // 2.3 套餐信息
            queryWrapper.eq(ShoppingCart ::getSetmealId,shoppingCart.getSetmealId());
        }
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
        // 2.4 查询菜品或套餐是否已存在
        if (cartServiceOne != null){
            // 菜品已存在
            cartServiceOne.setNumber(cartServiceOne.getNumber() + 1);
            shoppingCartService.updateById(cartServiceOne);
        }else {
            // 菜品不存在
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);
        }
        return R.success(shoppingCart);
    }

}

05. 代码开发(查看购物车)

1. 将前端假数据修改回来
在这里插入图片描述
    /**
     * 获取购物车全部信息
     * @return
     */
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
        Long currentId = BaseContext.getCurrentId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(currentId != null,ShoppingCart ::getUserId,currentId);
        List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
        return R.success(list);
    }

06. 菜品或套餐数量减一

    /**
     * 菜品或套餐数量减一
     * @param shoppingCart
     * @return
     */
    @PostMapping("/sub")
    public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart){
        // 获取用户id
        Long currentId = BaseContext.getCurrentId();
        Long dishId = shoppingCart.getDishId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(currentId != null,ShoppingCart::getUserId,currentId);
        // 判断减去的是菜品信息还是套餐信息
        if (dishId != null) {
            // 菜品信息
            queryWrapper.eq(ShoppingCart ::getDishId,dishId);
        }else {
            // 套餐信息
            queryWrapper.eq(ShoppingCart ::getSetmealId,shoppingCart.getSetmealId());
        }
        // 查看购物车信息数量
        ShoppingCart cart = shoppingCartService.getOne(queryWrapper);
        Integer number = cart.getNumber();
        if (number > 1){
            // 数量 > 1 则减一
            cart.setNumber(number - 1);
            shoppingCartService.updateById(cart);
        }else if (number == 1){
            // 数量 = 1 则删除改条信息
            shoppingCartService.remove(queryWrapper);
        }
    return R.success(cart);
    }

07. 清空购物车

    /**
     * 清空购物车信息
     * @return
     */
    @DeleteMapping("/clean")
    public R<String> clean(){
        // 获取当前用户id
        Long currentId = BaseContext.getCurrentId();
        // 构造条件构造器
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(currentId != null, ShoppingCart ::getUserId,currentId);
        shoppingCartService.remove(queryWrapper);
        return R.success("购物车已清空!");
    }

④:用户下单

01. 需求分析

移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 【去结算】 按钮,页面跳转到订单确认页面,点击 【去支付】 按钮则完成下单操作。

02. 数据模型

orders表 order_detail表
在这里插入图片描述在这里插入图片描述

03. 代码开发 (Controller,Service,Mapper、实体类)

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:

1、在购物车中点击 【去结算】 按钮,页面跳转到订单确认页面

2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址

3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据

4、在订单确认页面点击 【去支付】 按钮,发送ajax请求,请求服务端完成下单操作

开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
代码开发-准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

实体类Orders、OrderDetail(直接从课程资料中导入即可)
Mapper接口OrderMapper、OrderDetailMapper
业务层接口OrderService、OrderDetailService
业务层实现类OrderServicelmpl、OrderDetailServicelmpl
控制层OrderController、OrderDetailController

04. 代码开发(功能实现)

1. OrderServiceImpl
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper,Orders> implements OrderService {

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private OrderDetailService orledDetailService;

    /**
     * 用户下单
     * @param orders
     */
    @Transactional
    @Override
    public void submit(Orders orders) {
        // 获取当前用户id
        Long currentId = BaseContext.getCurrentId();
        // 查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(currentId != null,ShoppingCart ::getUserId,currentId);
        List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

        // 判断购物车是否为空
        if (list == null || list.size() == 0) {
            throw new CustomException("购物车为空,不能下单");
        }
        // 查询用户数据
        User user = userService.getById(currentId);
        // 查询用户地址
        AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
        if (addressBook == null) {
            throw new CustomException("地址信息有误!不能下单!");
        }
        // 向订单表插入数据,一条数据
        long orderId = IdWorker.getId(); //订单号

        AtomicInteger amount=new AtomicInteger(0);

        List<OrderDetail> orderDetails=list.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.setNumber(String.valueOf(orderId));
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));//计算总金额
        orders.setUserId(currentId);
        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);

        // 向订单表插入多条数据
        orledDetailService.saveBatch(orderDetails);
        // 清空购物车数据
        shoppingCartService.remove(queryWrapper);
    }
}
2. OrderController
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 用户下单
     * @param orders
     * @return
     */
    @PostMapping("/submit")
    public R<String> submit(@RequestBody Orders orders){
        orderService.submit(orders);
        return R.success("下单成功");
    }
}

3. 测试
在这里插入图片描述

⑤:拓展功能

若读者发现bug或者更好的方法,欢迎评论(一起交流学习😉😉)

01. 退出账号

1. 在UserController添加loginout方法
    /**
     * 退出登录
     * @param session
     * @return
     */
    @PostMapping("/loginout")
    public R<String> loginout(HttpSession session){
        Long currentId = BaseContext.getCurrentId();
        User user = userService.getById(currentId);
        session.removeAttribute(user.getPhone());
        return R.success("账号已退出!");
    }

02. 最新订单和历史订单

导入OrderDto

1. 两者请求方式都一样,故写一个方法即可
在这里插入图片描述
2. 在OrderController添加userPage方法
    /**
     *  最新订单和历史订单
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/userPage")
    public R<Page<OrdersDto>> userPage(int page, int pageSize){
        // 获取用户id
        Long currentId = BaseContext.getCurrentId();
        Page<Orders> ordersPage = new Page<>(page, pageSize);
        // 构造条件构造器
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        // 根据用户id查询订单并排序
        queryWrapper.eq(Orders ::getUserId,currentId).orderByDesc(Orders ::getCheckoutTime);
        Page<Orders> ordersPage1 = orderService.page(ordersPage, queryWrapper);

        List<Orders> ordersList = ordersPage1.getRecords();
        List<OrdersDto> ordersDtoList = ordersList.stream().map((item) -> {
            OrdersDto ordersDto = new OrdersDto();
            BeanUtils.copyProperties(item, ordersDto);
            Long orderId = item.getId();
            LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(orderId != null, OrderDetail::getOrderId, orderId);
            List<OrderDetail> detailList = orderDetailService.list(wrapper);
            ordersDto.setOrderDetails(detailList);
            return ordersDto;
        }).collect(Collectors.toList());

        Page<OrdersDto> ordersDtoPage = new Page<>();
        ordersDtoPage.setRecords(ordersDtoList);

        return R.success(ordersDtoPage);
    }
3. 测试
在这里插入图片描述

03.客户端订单详情

需要接收5个参数
(int page, int pageSize, Long number, String beginTime, String endTime)

在这里插入图片描述

1. 在OrderController添加pcPage方法
    /**
     * 客户端订单详情
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime
     * @param endTime
     * @return
     */
    @GetMapping("/page")
    public R<Page<OrdersDto>> pcPage(int page, int pageSize, Long number, String beginTime, String endTime){
        Page<Orders> ordersPage = new Page<>(page, pageSize);
        // 构造条件构造器
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        // 根据用户id查询订单并排序
        queryWrapper.like(number != null, Orders::getId,number)
                .between(StringUtils.isNotBlank(beginTime) || StringUtils.isNotBlank(endTime),Orders::getOrderTime,beginTime,endTime)
                .orderByDesc(Orders ::getOrderTime);
        // 分页查询
        Page<Orders> ordersPage1 = orderService.page(ordersPage, queryWrapper);

        List<Orders> ordersList = ordersPage1.getRecords();
        List<OrdersDto> ordersDtoList = ordersList.stream().map((item) -> {
            OrdersDto ordersDto = new OrdersDto();
            BeanUtils.copyProperties(item, ordersDto);
            Long orderId = item.getId();
            LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(orderId != null, OrderDetail::getOrderId, orderId);
            List<OrderDetail> detailList = orderDetailService.list(wrapper);
            ordersDto.setOrderDetails(detailList);
            ordersDto.setUserName(item.getConsignee());
            return ordersDto;
        }).collect(Collectors.toList());

        Page<OrdersDto> ordersDtoPage = new Page<>();
        ordersDtoPage.setTotal(ordersPage1.getTotal());
        ordersDtoPage.setRecords(ordersDtoList);

        return R.success(ordersDtoPage);
    }
2. 测试
在这里插入图片描述

04. 订单派送

1. 在OrderController处理post请求
    /**
     * 订单派送
     * @param orders
     * @return
     */
    @PutMapping
    public R<String> order(@RequestBody Orders orders){
        Long ordersId = orders.getId();
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ordersId != null,Orders::getId, ordersId)
                .eq(Orders::getStatus,orders.getStatus());
        orderService.updateById(orders);
        return R.success("派送完成!");
    }
2. 测试
在这里插入图片描述

八、Git课程

Git课程笔记:https://blog.csdn.net/cygqtt/article/details/126217207

九、Linux课程

Linux课程笔记:https://blog.csdn.net/cygqtt/article/details/124359613

十、Redis课程

Redis课程笔记:http://t.csdn.cn/jQlfx

十一、缓存优化

①:问题说明

在这里插入图片描述

②:环境搭建

01. 使用Git管理工程

1. gitee上新建一个仓库与本地工程关联
在这里插入图片描述
2. 将本地代码push到远程仓库
在这里插入图片描述
3. 创建一个新的分支并推送到远程仓库(优化程序在分支中进行)
在这里插入图片描述在这里插入图片描述

02.Maven坐标

1. 导入Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

03.配置文件

spring:
# Redis配置
  redis:
    host: localhost
    port: 6379
#    password: root
    database: 0

04. 配置类

  • 在项目中加入配置类RedisConfig:
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        // 默认的key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

05. 提交并推送到远程仓库

在这里插入图片描述

③:缓存短信验证码

01. 实现思路

前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的。现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:

1. 在服务端UserController中注入RedisTemplate对象,用于操作Redis
  @Autowired
  private RedisTemplate redisTemplate;
2. 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
// session.setAttribute(phone,code);  // 注销这行代码验证码不存在session中了

// 将生成的验证码缓存到Redis中,并且设置有效时间为5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
3. 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
        // 3.从session中获取保存的验证码
//       Object codeInSession = session.getAttribute(phone); 注释这行代码不在从session中获取验证码了

       // 从Redis中获取缓存的验证码
        Object codeInSession =  redisTemplate.opsForValue().get(phone);
session.setAttribute("user",user.getId());

// 如果用户登录成功,删除Redis中缓存的验证码
redisTemplate.delete(phone);
return R.success(user);

02.功能实现

1. 启动服务测试 用户端登录(验证码情况)
在这里插入图片描述
2. 登录成功后验证码确实被删除了
在这里插入图片描述

④:缓存菜品数据

01.缓存菜品数据(添加缓存)

1. 修改DishController中的 @GetMapping("/list") 请求
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据条件查询对应菜品的数据
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<DishDto>> getDishById(Dish dish){
        List<DishDto> dishDtoList = null;
        // 动态构建Redis的key
        String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();
        // 先从Redis中获取缓存数据
        dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

        // 如果存在,直接返回,无需查询数据库
        if (dishDtoList != null) {
            return R.success(dishDtoList);
        }

        // 如果不存在,许需要查询数据库

        // 构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
        // 添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
       // 添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);

        // 获取分类名称
        dishDtoList = list.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item, dishDto);
            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);
            if (category == null) {
                dishDto.setCategoryName(category.getName());
            }
            // 获取口味数据
            Long dishId = item.getId();
            LambdaQueryWrapper<DishFlavor> queryWrapper1 = new LambdaQueryWrapper<>();
            queryWrapper1.eq(DishFlavor::getDishId,dishId).orderByDesc(DishFlavor::getUpdateTime);
            List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper1);
            dishDto.setFlavors(dishFlavors);
            return dishDto;
        }).collect(Collectors.toList());
        // 将查询到的菜品数据缓存到Redis,存活时间为60分钟
        redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);

        return R.success(dishDtoList);
    }
2. 修改 SetmealController中 @GetMapping("/list") 请求方法
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据分类id查询套餐信息
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    public R<List<Setmeal>> getSetmealDto(Setmeal setmeal){
        List<Setmeal> setmealList = null;

        // 动态构建Redis的key
        String key = "dish_" + setmeal.getCategoryId() + "_" + setmeal.getStatus();
        // 先从Redis中获取缓存数据
        setmealList = (List<Setmeal>) redisTemplate.opsForValue().get(key);

        // 如果存在,直接返回,无需查询数据库
        if (setmealList != null) {
            return R.success(setmealList);
        }

        // 如果不存在,许需要查询数据库

        // 查询套餐基本信息
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null, Setmeal ::getStatus,1 );
        setmealList = setmealService.list(queryWrapper);

        // 将查询到的菜品数据缓存到Redis,存活时间为60分钟
        redisTemplate.opsForValue().set(key,setmealList,60, TimeUnit.MINUTES);

        return R.success(setmealList);
    }

02. 功能测试

1. 测试
在这里插入图片描述

03.缓存菜品数据(清理缓存)

在这里插入图片描述

1. 清理SetmealController 中的保存 修改 操作都需要清理缓存
  • 保存和修改操作都需要调用该方法
    /**
     * 清理当前分类对应的缓存数据
     */
    private  void  clearDate(Long categoryId, int status){
        String key = "dish_" + categoryId + "_" + status;
        redisTemplate.delete(key);
    }
2. 清理DishController中的保存 修改 操作都需要清理缓存
    /**
     * 查询当前菜品对应的分类信息
     */
    private Long getDishToCategory(Object id){
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Category ::getId,id);
        Category category = categoryService.getOne(queryWrapper);
        return category.getId();
    }
    /**
     * 清理当前分类对应的缓存数据
     */
    private   void  clearDate(Object categoryId, int status){
        String key = "dish_" + categoryId + "_" + status;
        redisTemplate.delete(key);
    }

04. 功能测试

1. 测试
在这里插入图片描述

⑤:将代码提交到本地并推送到远程仓库

1. 推送到远程仓库
在这里插入图片描述
2. 切换到主分支,合并v1.0分支的代码
在这里插入图片描述
3. 在切换回v1.0分支继续优化代码
在这里插入图片描述

⑥:Spring Cache

01. Spring Cache 介绍

Spring cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。

CacheManager是Spring提供的各种缓存技术抽象接口。

针对不同的缓存技术需要实现不同的CacheManager:
在这里插入图片描述

02.Spring Cache常用注解

在这里插入图片描述
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

03. @CachePut注解 使用方式

1. 数据准备

  • 创建一个springBoot工程
1.准备数据库
create database cache_demo;

use cache_demo;

create table user(
    id bigint not null primary key ,
    name varchar(50),
    age int,
    address varchar(100)
);
2. Maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.10</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
3. yml文件配置
server:
  port: 8080
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/cache_demo?serverTimezone=Asia/Shanghai&useUnicode=true&charsetEncoding=utf-8
      username: root
      password: root

mybatis-plus:
  configuration:
    # 在映射实体类或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命令法映射
    use-actual-param-name: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: assign_id

4. 启动类
@SpringBootApplication
@EnableCaching
public class SpringBoot05CacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBoot05CacheApplication.class, args);
    }
}
5. User实体类
//  要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis
@Data
public class User implements Serializable{
  // 程序序列化ID
  private static final Long serialVersionUID = 1L;

  private Long id;
  private String name;
  private long age;
  private String address;
}
6. UserMapper & UserService & UserServiceImpl
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

2. 代码实现

1. UserController
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private UserService userService;

    /**
     *CachePut: 将方法返回值放入缓存
     * value:缓存的名称,每个缓存名称下面可以有多个key
     * key:缓存的key
     */
    @CachePut(value = "userCache",key = "#result.id")
    @PostMapping
    public User save(User user){
        userService.save(user);
        return user;
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id){
        userService.removeById(id);
    }

    @PutMapping
    public User update(User user){
        userService.updateById(user);
        return user;
    }

    @GetMapping("/{id}")
    public User getById(@PathVariable Long id){
        User user = userService.getById(id);
        return user;
    }

    @GetMapping("/list")
    public List<User> list(User user) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(user.getId() != null,User::getId,user.getId());
        queryWrapper.eq(StringUtils.isNotBlank(user.getName()),User::getName,user.getName());
        List<User> list = userService.list(queryWrapper);
        return list;
    }
}

3. 功能测试

1. 使用postman发送请求测试
在这里插入图片描述

04. @CacheEvict 注解使用方法

1. 在 delete 方法添加天机清理缓存注解
    /**
     *CacheEvict: 清除指定缓存
     * value:缓存的名称,每个缓存名称下面可以有多个key
     * key:缓存的key
     */
    @CacheEvict(value = "userCache",key ="#id" )
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id){
        userService.removeById(id);
    }
2. 测试
在这里插入图片描述
3. 在 update方法添加天机清理缓存注解
    @CacheEvict(value = "userCache",key ="#user.id" )
    @PutMapping
    public User update(User user){
        userService.updateById(user);
        return user;
    }

05. @Cacheable注解使用方法

1. 在 getById 和 list 方法添加list注解
    /**
     *Cacheable: 在方法执行前spring:先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;
     * 若没有数据,调用方法并将方法返回值放到缓存中
     * value:缓存的名称,每个缓存名称下面可以有多个key
     * key:缓存的key
     * condition: 条件满足条件才缓存(result != null)返回结果不为null才缓存
     * unless: 满足条件则不缓存
     */
    @Cacheable(value = "userCache",key = "#id",unless = "#result == null ")
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id){
        User user = userService.getById(id);
        return user;
    }


    @Cacheable(value = "userCache",key = "#user.id + '_' + #user.name",condition = "result != null ")
    @GetMapping("/list")
    public List<User> list(User user) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(user.getId() != null,User::getId,user.getId());
        queryWrapper.eq(StringUtils.isNotBlank(user.getName()),User::getName,user.getName());
        List<User> list = userService.list(queryWrapper);
        return list;
    }

06. Spring cache 使用方法

1. 导入坐标
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.7.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>
2. 配置yml文件
spring:
  cache:
    redis:
      time-to-live: 1800000 #设置缓存有效期
1. 测试
在这里插入图片描述

⑦:缓存套餐数据

01.实现思路

1. 导入Maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.7.0</version>
</dependency>
2. yml文件中配置缓存数据的过期时间
spring:
  cache:
    redis:
      time-to-live: 1800000 #设置缓存有效期
3. 在启动类上加入@EnableCaching注解,开启缓存注解功能
4. 返回结果类R要实现序列化接口 Serializable
@Data
public class R<T> implements Serializable
5. 在SetmealController的list方法上加入@Cacheable注解
    /**
     * 根据分类id查询套餐信息
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    @Cacheable(value = "setmealCache",key = "#setmeal.getCategoryId() + '_' + #setmeal.getStatus()")
    public R<List<Setmeal>> getSetmealDto(Setmeal setmeal){
        // 查询套餐基本信息
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null, Setmeal ::getStatus,1 );
        List<Setmeal>  setmealList = setmealService.list(queryWrapper);
        return R.success(setmealList);
    }
6. 在SetmealController的save和delete方法上加入CacheEvict注解
    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     * @return
     */
    @PostMapping
    @CacheEvict(value = "setmealCache",allEntries = true) // 新增套餐时,清理所有的套餐缓存
    public R<String> save(@RequestBody SetmealDto setmealDto){
        setmealService.saveWithDish(setmealDto);

        return R.success("套餐信息添加成功~");
    }
    /**
     * 删除套餐信息
     * @param ids
     * @return
     */
    @DeleteMapping
    @CacheEvict(value = "setmealCache",allEntries = true) // 删除套餐时,清理所有的套餐缓存
    public R<String> deleteWithSetmentAndDish(@RequestParam List<Long> ids) {
        setmealService.deleteWithSetmentAndDish(ids);
        return R.success("删除成功!");
    }

⑧:将代码推送到Gir仓库并合并到主分支

1. 提交推送
在这里插入图片描述
2. 切换到master分支 并合并
在这里插入图片描述
3. 在切换回v1.0分支
在这里插入图片描述

十二、读写分离(优化)

  • 问题分析

在这里插入图片描述在这里插入图片描述

①:Mysql主从复制

01. 介绍

MysSQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台AysQL数据库(slave,即从库)从另一台MysQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MysQL数据库自带功能,无需借助第三方工具。

MysQL复制过程分成三步:

master将改变记录到二进制日志( binary log)
slave将master的binary log拷贝到它的中继日志(relay log)
slave重做中继日志中的事件,将改变应用到自己的数据库中

在这里插入图片描述

02.配置-前置条件

提前准备好两台服务器,分别安装Mysql并启动服务成功

主库Master 192.168.100.200
从库slave 192.168.100.192

在这里插入图片描述

03. 配置-主库Master

第一步: 修改Mysq1数据库的配置文件/etc/my.cnf

[mysqld] # 这一行默认是有的(可以不配)添加以下两行代码
log-bin=mysql-bin #[必须]启用二进制日志
server-id=100 #[必须]服务器唯一ID
1. vim /etc/my.cnf
在这里插入图片描述

第二步: 重启Mysql服务
重启命令systemctl restart mysqld
在这里插入图片描述

第三步: 登录Mysql数据库,执行下面SQL
SQL: GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';
在这里插入图片描述

:上面SQL的作用是创建一个用户xiaoming,密码为Root@123456,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。

如果是 MySQL8
第1步:create user xiaoming identified by ‘Root@123456’
第2步:grant replication slave on . to xiaoming

第四步: 登录Mysql数据库,执行下面SQL,记录下结果中File和Position的值
SQL命令:show master status;
在这里插入图片描述

注意:上面SQL的作用是查看Master的状态,执行完此SQL后不要再执行任何操作

04.配置-从库Slave

第一步: 修改Mysq1数据库的配置文件/etc/my.cnf

[mysqld] # 这一行默认是有的(可以不配)添加以下两行代码
server-id=101  #[必须]服务器唯一ID
1. vim /etc/my.cnf
在这里插入图片描述

第二步: 重启Mysql服务
重启命令systemctl restart mysqld
在这里插入图片描述

第三步: 登录Mysq1数据库,执行下面SQL
在这里插入图片描述

change master to master_host='192.168.100.200',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=433;

start slave;
1. 执行sql
在这里插入图片描述

第四步: 在从库中,执行下面SQL,查看从数据库的状态
SQL: show slave status;

1. 执行sql
在这里插入图片描述

05. 测试MySQL主从复制

1. 测试创建数据库
在这里插入图片描述
2. 测试创建一张 user 表
在这里插入图片描述
3. 添加数据
在这里插入图片描述

②:读写分离案例

01. 背景

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
在这里插入图片描述

02. Sharding-JDBC介绍

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。

适用于任何基于JDBC的ORM框架,如: JPA, Hibernate,Mybatis, Spring JDBC Template或直接使用JDBC。
支持任何第三方的数据库连接池,如:DBCP,C3PO,BoneCP, Druid, HikariCP等。
支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>

03.入门案例

在这里插入图片描述

1.数据准备

1. 数据库 数据表准备
create database rw;

use rw;

create table user(
    id bigint not null primary key ,
    name varchar(50),
    age int,
    address varchar(100)
);

在这里插入图片描述

2. Maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.10</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>
3. yml文件配置
server:
  port: 8080
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
spring:
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.100.200:3306/rw?characterEncoding=utf-8
        username: root
        password: root
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.100.192:3306/rw?characterEncoding=utf-8
        username: root
        password: root
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin #轮询
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false
# 允许bean定义覆盖配置项
  main:
    allow-bean-definition-overriding: true
4. 启动类
@SpringBootApplication
public class SpringBoot05CacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBoot05CacheApplication.class, args);
    }
}
5. User实体类
//  要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis
@Data
public class User implements Serializable{
  // 程序序列化ID
  private static final Long serialVersionUID = 1L;

  private Long id;
  private String name;
  private long age;
  private String address;
}
6. UserMapper & UserService & UserServiceImpl
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
7. UserController
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserService userService;
    
    @PostMapping
    public User save(User user){
        userService.save(user);
        return user;
    }
    
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id){
        userService.removeById(id);
    }
    
    @PutMapping
    public User update(User user){
        userService.updateById(user);
        return user;
    }

   @GetMapping("/{id}")
    public User getById(@PathVariable Long id){
        User user = userService.getById(id);
        return user;
    }
    
    @GetMapping("/list")
    public List<User> list(User user) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(user.getId() != null,User::getId,user.getId());
        queryWrapper.eq(StringUtils.isNotBlank(user.getName()),User::getName,user.getName());
        List<User> list = userService.list(queryWrapper);
        return list;
    }
}

2. 测试

1. 发送查询请求
在这里插入图片描述
2. 添加信息
在这里插入图片描述

③:项目实现读写分离

01. 数据库环境准备(主从复制)

1. 在主库表中创建 reggie 数据库
在这里插入图片描述
2. 将原有数据库中的数据 导出 转入到主库中 (导入数据时报错了)
在这里插入图片描述

使用navicate12运行sql文件出错

报错:

[ERR] 1273 - Unknown collation: 'utf8mb4_0900_ai_ci'

报错原因:
生成转储文件的数据库版本为8.0,要导入sql文件的数据库版本为5.6,因为是高版本导入到低版本,引起1273错误

  • 解决方法:
    • 打开sql文件,将文件中的所有
    • utf8mb4_0900_ai_ci替换为utf8_general_ci
    • utf8mb4替换为utf8
    • 保存后再次运行sql文件,运行成功

在这里插入图片描述

02. 代码改造

在项目中加入Sharding-JDBC实现读写分离步骤:

1、导入maven坐标

2、在配置文件中配置读写分离规则

3、在配置文件中配置允许bean定义覆盖配置项
1. 在v1.0的分支上创建一个新的分支v1.1 (优化代码在v1.1中开发)
2. 导入maven坐标
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>
3. 在配置文件中配置读写分离规则
4. 在配置文件中配置允许bean定义覆盖配置项
  • 3 4 步骤都是修改yml文件
server:
  # 配置端口号
  port: 8080
spring:
  application:
    # 应用的名称,可选
    name: reggie_take_out
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.100.200:3306/reggie?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: root
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.100.192:3306/reggie?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
        username: root
        password: root
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin #轮询
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false
  main:
    allow-circular-references: true
    # 允许bean定义覆盖配置项
    allow-bean-definition-overriding: true
  # Redis配置
  redis:
    host: localhost
    port: 6379
#    password: root
    database: 0
  cache:
    redis:
      time-to-live: 1800000  #设置缓存有效期


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 在映射实体或者属性时,将数据库中表名和字段名中的下创线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: assign_id

reggie:
  path: D:\OOP\java\develop_idea\06_reggie\reggie_take_out\src\main\resources\static\img\

03. 测试

1. 启动项目测试
在这里插入图片描述在这里插入图片描述
2. 测试完成后将代码推送到远程仓库 并 合并到主分支

十三、Nginx

①:Nginx概述

Nginx是一款轻量级的web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。

Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler .ru站点(俄文: Paw6nep)开发的,第一个公开版本0.1.e发布于2004年10月4日。
官网: https://nginx.org/

②:Nginx下载与安装

官方下载Nginx的安装包 https://nginx.org/en/download.html

网盘链接
链接:https://pan.baidu.com/s/17HZ1bnT6bMijsPtFQ3u0fg
提取码:Coke

安装过程:

  • 1、安装依赖包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel

  • 2、下载Nginx安装包 wget https://nginx.org/download/nginx-1.16.1.tar.gz (需要先yum install wget)

  • 3、解压 tar -zxvf nginx-1.16.1.tar.gz

  • 4、cd nginx-1.16.1

  • 5、./ configure --prefix=/usr/local/nginx

  • 6、make && make install

1. 安装依赖包
命令:yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
在这里插入图片描述
2. 下载Nginx安装包
命令:wget https://nginx.org/download/nginx-1.16.1.tar.gz
(需要先yum install wget)
在这里插入图片描述在这里插入图片描述
3. 解压
命令:tar -zxvf nginx-1.16.1.tar.gz
在这里插入图片描述
4. cd nginx-1.16.1
在这里插入图片描述
5. ./ configure --prefix=/usr/oop/nginx
在这里插入图片描述
6. make && make install
在这里插入图片描述在这里插入图片描述

③:Nginx目录结构

1. 安装完Nginx后,我们先来熟悉一下Nginx的目录结构
在这里插入图片描述

重点目录/文件:

conf/nginx.conf nginx配置文件
html
存放静态文件(html、css、Js等)
logs
日志目录,存放日志文件
sbin/nginx
二进制文件,用于启动、停止Nginx服务

④:Nginx命令

01. 查看版本

1. 进入目录:/usr/oop/nginx/sbin
执行命令:./nginx -v
在这里插入图片描述

02. 检查配置文件正确性

在启动Nginx服务之前,可以先检查一下conf/nginx.conf文件配置的是否有错误

2. 进入目录:/usr/oop/nginx/sbin
执行命令:./nginx -t
在这里插入图片描述

03. 启动和停止

在sbin目录下。

启动Nginx服务使用如下命令: ./nginx

停止Nginx服务使用如下命令: ./nginx -s stop

启动完成后可以查看Nginx进程: ps -ef | grep nginx

1. 启动Nginx服务
在这里插入图片描述
2. 查看Nginx进程
在这里插入图片描述
3. 访问Nginx
进入目录:/usr/oop/nginx/nginx-1.16.1/html可以看到有两个html页面
在这里插入图片描述
4. 停止Nginx服务使用如下命令: ./nginx -s stop
在这里插入图片描述

04. 重新加载配置文件

1. 修改运行的进程数目:
vim /usr/oop/nginx/conf/nginx.conf

worker_processes  2;

在这里插入图片描述

可以通过修改profile文件配置环境变量, 在/目录下可以直接使用nginx命令
vim /etc/profile

PATH=/usr/oop/nginx/sbin:$JAVA_HOME/bin:$PATH

使配置文件生效:source /etc/profile

重启Nginx:nginx -s reload

停止Nginx:nginx -s stop

启动Nginx:nginx

在这里插入图片描述

⑤:Nginx配置文件结构

01.整体结构介绍

Nginx配置文件(conf/nginx.conf)整体分为三部分:

  • 全局块
    和Nginx运行相关的全局配置
  • events块
    和网络连接相关的配置
  • http块
    代理、缓存、日志记录、虚拟主机配置
    • http全局块
    • Server块
      • Server全局块
      • location块

注意 : http块中可以配置多个Server块,每个Server块中可以配置多个location块

在这里插入图片描述

⑥Nginx具体应用

01. 部署静态资源

Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。

相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。

将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。

server {
  listen 80;                #监听端口
  server_name localhost;    #服务器名称
  location/{                #匹配客户端请求url
    root html;              #指定静态资源根目录
    index index.html;       #指定默认首页(可以配置多个)
}

02. 反向代理

1. 正向代理

是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。

正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。
在这里插入图片描述

2.反向代理

反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。

用户不需要知道目标服务器的地址,也无须在用户端作任何设定。
image
在这里插入图片描述

1. 在二号服务器中运行jar包 & 访问测试
在这里插入图片描述 在这里插入图片描述
2. 在一号服务器中 进行反向代理配置
server {
  listen       82;
  server_name  localhost;

  location / {
          proxy_pass http://192.168.100.192:8088; #反向代理配置
  } 
}
3. 进入到该目录下:/usr/oop/nginx/conf
编辑配置文件:vim nginx.conf
在这里插入图片描述在这里插入图片描述
4. 开放 82 端口 :
firewall-cmd --zone=public --add-port=82/tcp --permanent
5. 访问代理服务器
在这里插入图片描述

03.负载均衡

早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。

  • 应用集群: 将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
  • 负载均衡器: 将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理

在这里插入图片描述

1. 在二号服务器中运行两个Spring程序
在这里插入图片描述在这里插入图片描述
upstream targetserver{    #upstream指令可以定义一组服务器
  server 192.168.100.192:8088;
  server 192.168.100.192:8081;
}

server {
  listen  8080;
  server_name     localhost;
  location / {
          proxy_pass http://targetserver;
  }
}
2. 在一号服务器中配置负载均衡
在这里插入图片描述
3. 检查文件配置是否正确,重启nginx服务
在这里插入图片描述
4. 访问测试
在这里插入图片描述

04. 负载均衡配置

在这里插入图片描述

upstream targetserver{    #upstream指令可以定义一组服务器
  server 192.168.100.192:8088 weight=10;
  server 192.168.100.192:8081 weight=5;
}

server {
  listen  8080;
  server_name     localhost;
  location / {
          proxy_pass http://targetserver;
  }
}

十四、 前后端分离开发

①:问题分析

在这里插入图片描述

  • 开发人员同时负责前端和后端代码开发,分工不明确
  • 开发效率低
  • 前后端代码混合在一个工程中,不便于管理
  • 对开发人员要求高,人员招聘困难

②:前后端分离开发

01. 介绍

前后端分离开发,就是在项目开发过程中,对于前端代码的开发由专门的前端开发人员负责,后端代码则由后端开发人员负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。目前,前后端分离开发方式已经被越来越多的公司所采用,成为当前项目开发的主流开发方式。

前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程。

在这里插入图片描述

02. 开发流程

前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?可以按照如下流程进行:

在这里插入图片描述
在这里插入图片描述接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容

③:前端技术栈

开发工具

  • Visual Studio Code
  • hbuilder

技术框架

  • nodejs
  • VUE
  • ElementUI
  • mock
  • webpack

④:Yapi

01.介绍

YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。

源码地址: https://github.com/YMFE/yapi

要使用YApi,需要自己进行部署。

02.使用

使用YApi可以执行下面操作

添加项目
添加分类
添加接口
编辑接口
查看接口

⑤:Swagger

01.介绍

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。

官网: https://swagger.io/

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。

02.使用方式

1. 在项目中创建新的分支 v1.2
2. 导入knife4j的maven坐标
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>
3. 导入knife4j相关配置类
  • 添加到 WebMvcConfig 类中
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
  @Bean
  public Docket createRestApi() {
      //文档类型
      return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .select()
              .apis(RequestHandlerSelectors.basePackage("com.it.controller"))
              .paths(PathSelectors.any())
              .build();
  }
  private ApiInfo apiInfo() {
      return new ApiInfoBuilder()
              .title("瑞吉外卖")
              .version("1.0")
              .description("瑞吉外卖接口文档")
              .build();
  }
}
4.设置静态资源,否则接口文档页面无法访问(addResourceHandlers方法)
  • 添加到 WebMvcConfig 类的 addResourceHandlers方法中
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
5. 在LoginCheckFilter中设置不需要处理的请求路径
String[] urls = new String[]{
        "/employee/login",
        "/employee/logout",
        "/backend/**",
        "/front/**",
        "/common/**",
        "/user/sendMsg",
        "/user/login",

        "/doc.html",
        "/webjars/**",
        "/swagger-resources",
        "/v2/api-docs"
};
6. 测试 (启动项目后)
访问 :
在这里插入图片描述

03. 常用注解

注解说明
@Api用在请求的类上,例如Controller,表示对类的说明
@ApiModel用在类上,通常是实体类,表示一个返回响应数据的信息
@ApiModelProperty用在属性上,描述响应类的属性
@ApiOperation用在请求的方法上,说明方法的用途、作用
@ApilmplicitParams用在请求的方法上,表示一组参数说明
ApilmplicitParam用在@ApilmplicitParams注解中,指定一个请求参数的各个方面

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

⑥:项目部署

01.部署架构

在这里插入图片描述

02.部署环境说明

服务器:

192.168.100.200(一号服务器)

Nginx:部署前端项目、配置反向代理

Mysql:主从复制结构中的主库

Redis:缓存中间件 (一号 服务器)

192.168.100.192(二号服务器)

jdk:运行Java项目

git:版本控制工具

maven:项目构建工具

jar: Spring Boot项目打成jar包基于内置Tomcat运行

Mysql:主从复制结构中的从库

03. 部署前端项目

1. 在一号服务器中安装Nginx,将课程资料中的dist目录上传到Nginx的html目录下
在这里插入图片描述 在这里插入图片描述
2. 修改Nginx配置文件nginx.conf
server{
  listen 80;
  server_name localhost;

  location /{
    root html/dist;
    index index.html;
  }

  location ^~ /api/{
          rewrite ^/api/(.*)$ /$1 break;
          proxy_pass http://192.168.100.192:8080;
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html{
      root html;
  }
}

在这里插入图片描述

04.部署后端项目

1. 合并v1.2 分支 推送
2. 在服务器B中安装jdk、git、maven、MySQL,使用git clone命令将git远程仓库的代码克隆下来
在这里插入图片描述
3. 将资料中提供的reggieStart.sh文件上传到服务器B,通过chmod命令设置执行权限
在这里插入图片描述
#!/bin/sh
echo =================================
echo  自动化部署脚本启动
echo =================================

echo 停止原来运行中的工程
APP_NAME=java

tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Stop Process...'
    kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Kill Process!'
    kill -9 $tpid
else
    echo 'Stop Success!'
fi

echo 准备从Git仓库拉取最新代码
cd /usr/oop/java/git/reggie

echo 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成

echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`

cd target

echo 启动项目
nohup java -jar reggie_take_out-0.0.1-SNAPSHOT.jar &> helloworld.log &
echo 项目启动完成

4. 执行reggieStart.sh脚本文件,自动部署项目
在这里插入图片描述
5. 查看jar包是否运行
在这里插入图片描述
6. 访问测试
在这里插入图片描述

05. 部署后端项目(图片资源)

1. 在 /usr/oop/java/git 目录下创建目录 reggie_img
在这里插入图片描述
2. 修改yml文件 图片资源路径
(提交 推送到远程仓库 )
reggie:
  path: /usr/oop/java/git/reggie_img
3. 将图片资源上传到 Linux服务器中重新执行reggieStart.sh 脚本
在这里插入图片描述
4. 访问测试
在这里插入图片描述
在这里插入图片描述
5. 在reggie_img 后加/
reggie:
  path: /usr/oop/java/git/reggie_img/
6. 重新测试访问
在这里插入图片描述

相关文章:

  • Docker(4)Docker镜像
  • 同义词/近义词查询易语言代码
  • Python Tkinter 教程(四)—— 子模块 messagebox、colorchooser 以及 filedialog 的使用及技巧(万字详解)
  • C++画图 => 蓝桥杯青少组C++ => 信奥 学习路线图
  • 微信公众号的附件链接怎么弄
  • 概率论与梳理统计学习:随机变量(二)——知识总结与C语言案例实现
  • python学习—第一步—Python小白逆袭大神(第二天)
  • SAP ABAP ALV 的一些总结:Custom container 和 Splitter container
  • 由从零开始的神经网络理解torch的几个模块
  • R语言进行数据分组聚合统计变换(Aggregating transforms)、计算dataframe数据的分组独特值的个数(distinct)
  • Linux入门之使用 ifconfig 命令配置网络连接
  • Day 1 BUUCTF——特殊的 BASE64 1
  • 大数据分析案例-用RFM模型对客户价值分析(聚类)
  • Linux入门之管理 Wi-Fi 连接
  • 结构体作业等
  • 【Amaple教程】5. 插件
  • 【知识碎片】第三方登录弹窗效果
  • 〔开发系列〕一次关于小程序开发的深度总结
  • 2017 前端面试准备 - 收藏集 - 掘金
  • Apache Spark Streaming 使用实例
  • CSS居中完全指南——构建CSS居中决策树
  • JavaScript服务器推送技术之 WebSocket
  • java小心机(3)| 浅析finalize()
  • Linux后台研发超实用命令总结
  • niucms就是以城市为分割单位,在上面 小区/乡村/同城论坛+58+团购
  • Redis 懒删除(lazy free)简史
  • Vue官网教程学习过程中值得记录的一些事情
  • webgl (原生)基础入门指南【一】
  • 简析gRPC client 连接管理
  • 设计模式(12)迭代器模式(讲解+应用)
  • 手机端车牌号码键盘的vue组件
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 微服务框架lagom
  • 学习JavaScript数据结构与算法 — 树
  • 在Unity中实现一个简单的消息管理器
  • ​卜东波研究员:高观点下的少儿计算思维
  • ​批处理文件中的errorlevel用法
  • ###C语言程序设计-----C语言学习(6)#
  • #define、const、typedef的差别
  • #前后端分离# 头条发布系统
  • $emit传递多个参数_PPC和MIPS指令集下二进制代码中函数参数个数的识别方法
  • (12)Linux 常见的三种进程状态
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (二)pulsar安装在独立的docker中,python测试
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (附源码)ssm经济信息门户网站 毕业设计 141634
  • (论文阅读30/100)Convolutional Pose Machines
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (论文阅读40-45)图像描述1
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (南京观海微电子)——COF介绍
  • (十六)Flask之蓝图
  • (小白学Java)Java简介和基本配置
  • (转)【Hibernate总结系列】使用举例
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .