瑞吉外卖学习笔记

一、项目概述

新手入门的SpringBoot+SSM企业级项目

1、软件开发整体介绍

1.1软件开发流程

陈
我们一般在编码层

1.2角色分层

在这里插入图片描述

1.3软件环境

①开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
②测试环境(testing):专门给测试人员使用的环境,用于测试项目,
一般外部用户无法访问
③生产环境(production):即线上环境,正式提供对外服务的环境

2、瑞吉外卖项目介绍

本项目共分为3期进行开发:
①第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。
②第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
③第三期主要针对系统进行优化升级,提高系统的访问性能。

2.1产品原型展示

①产品经理在需求分析后制作,一般都是网页。
②产品原型主要用于展示项目的功能,并不是最终的页面效果。

2.2技术选型

在这里插入图片描述

2.3功能架构

在这里插入图片描述

2.4角色

后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限
后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理
C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等

二、项目实操

Ⅰ、员工管理

1、开发环境搭建

在这里插入图片描述

第一步
数据库环境搭建,导入sql文件

第二步
maven项目搭建:骨架创建maven。
pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--parent : 继承的意思
    此处继承父工程 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>reggie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

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

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>

</project>

第三步
导入SpringBoot配置文件applidation.yml
resources包下导入application.yml

server:
  port: 8080 #tomcat端口号
spring:
  application:
    name: reggie_take_out #指定应用的名称 非必须
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
mybatis-plus:
  configuration:
    #address book---->AddressBook
    #user_name---->userName
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

第四步
编写启动类,可尝试启动

@Slf4j
//可以使用log方法,打印info级别日志
@SpringBootApplication
//引导类or启动类
@ServletComponentScan//扫描webFilter注解 进一步创建过滤器
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功。。。。");//打印info级别日志
    }
}

注意:静态资源一般放在webapp/static或者webapp/resources等目录下。默认寻找这个目录下的静态资源。不在直接报错。
决::配置映射文件

@Slf4j
//可以使用log方法,打印info级别日志
@Configuration//声明该类是配置类
public class WebMvcConfig extends WebMvcConfigurationSupport {
    /*
    静态资源文件应该都在webapp目录下
    本项目没有webapp目录
    需要设置静态资源映射
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射。。。");
      registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
      registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
      /*
      tomcat服务器访问路径映射到相应的静态文件下
      pathPatterns:服务器访问路径
      classpath:(指定类文件的路径)此处指读取静态资源的路径resources目录
       */
    }

2、功能开发

1、后台登录功能开发

第一步:
创建实体类Employee,和employee表进行映射
开发习惯:数据库一张表对应一个实体类
从资料直接导入

第二步:
创建Controller、Service、 Mappe

//mapper接口
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {//泛型实体
}
//EmployeeService接口 
@Mapper
public interface EmployeeService extends IService<Employee> {//继承IService泛型实体
}
//EmployeeServiceImpl继承serviceImpl(对应mapper类,对应实体类)继承 EmployeeService接口
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
//EmployeeController类
@Slf4j
@RestController
@RequestMapping("/employee")//路径
public class EmployeeController {

    //注入
    @Autowired
    private EmployeeService employeeService;

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

/*
通用返回结果,限务端响应的数据最终部会封装成此对练

 */
@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;
    }
}

第四步:
在Controller中创建登录方法
处理逻辑:
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果
在这里插入图片描述

@Slf4j
@RestController
@RequestMapping("/employee")//路径
public class EmployeeController {

    //注入
    @Autowired
    private EmployeeService employeeService;
/*
员工登录
 */
    @PostMapping("/login")
    //json在接收时,要有注解@RequestBody 分装成emplouee对象
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
        //Employee的id存在Session中,表示成功。获取当前用户:request来get
/**
 *         1、将页面提交的密码password进行md5加密处理,已经封装到Employee中
 *         密码已经封装到employee中
 */
        String password = employee.getPassword();//拿到密码
        password = DigestUtils.md5DigestAsHex(password.getBytes());//md5加密处理
//        2、根据页面提交的用户名username查询数据库
        //employee是泛型
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername, employee.getUsername());//指定sql语句条件
        Employee emp = employeeService.getOne(queryWrapper);
        //getOne 这个用户名是唯一的用getOne调用,调用后分装成employee对象
        //3、如果没有查询到则返回登录失败结果
        if (emp == null) {
            //返回结果封装成R对象
            return R.error("登录失败");
        }
        //4、密码比对,如果不一致则返回登录失败结果
        //数据库查的密码和处理后的密码对比
        if (!emp.getPassword().equals(password)) {
            return R.error("登录失败");
        }
        // 5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if (emp.getStatus() == 0) {
            //1账户可用。0账号不可用
            return R.error("账户已禁用");
        }
//        6、登录成功,将员工id存入Session并返回登录成功结果
        request.getSession().setAttribute("employee", emp.getId());

        return R.success(emp);
    }

2、后台退出功能开发

直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面

用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。

我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑;
1、清理Session中的用户id
2、返回结果

    /**
     * 员工退出页面
     */
        @PostMapping("/logout")
        public R<String> logout(HttpServletRequest request){
//            清理Session中保存的当前登录员工的id
            request.getSession().removeAttribute("employee");
            //把employee属性移除
            return R.success("退出成功");
    }

3、员工管理业务开发

3.1完善登录功能
3.1.1问题

用户如果不登录,直接访问系统页面,可以直接访问

3.1.2方案

使用过滤器或者拦截器。没有登录跳转到登录界面

3.1.3代码开发

实现步骤
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@Servletcomponentscan
3、完善过滤器的处理逻辑
创建filter包。给启动类添加扫描注解@ServletComponentScan

/*
检查用户是否已将登陆
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    //路径匹配器 支持通配符
    public static final AntPathMatcher PATH_MATCHER = 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);//{}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、判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }
        log.info("用户未登录");
//        5、如果未登录则返回未登录结果通,过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }
    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * 遍历要放行的数组。和请求的数组对比
     * @param requestURL
     * @return
     */
    public boolean check(String[] urls,String requestURL) {
        for (String url : urls){
            boolean match = PATH_MATCHER.match(url,requestURL);
            if (match){
                return true;//放回true表示匹配上
            }
        }
        return false;
    }
}
@Slf4j
//可以使用log方法,打印info级别日志
@SpringBootApplication
//引导类or启动类
@ServletComponentScan//扫描webFilter注解 进一步创建过滤器
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功。。。。");//打印info级别日志
    }
}

4、新增员工

4.1数据模型

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

4.2代码开发

整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
在这里插入图片描述
前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯
一约束,此时程序会抛出异常:

java sql.SQLIntegrityConstraintviolationException: Duplicate entry
zhangsan for key idx_username

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

1、在Controller方法中加入try、catch进行异常捕获
2、使用异常处理器进行全局异常捕获

全局异常处理器
*关键在@@ControllerAdvice和@ExceptionHandler两个注解
详解看注释

/**
 * 全局异常处理器
 * 关键在@@ControllerAdvice和@ExceptionHandler两个注解
 */
//
//拦截 类上加restController和controllee类
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody//封住成JSON数据
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 异常处理方法
     */
    //@ExceptionHandler声明要处理的类
    //此处处理的是SQLIntegrityConstraintViolationException的异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHamdler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        //异常有多种
        //判断异常信息里边是否有需要的关键字
        //用户名唯一,重复报一下错误
        //Duplicate entry 'zhangsan' for key'idx username
        //关键字=Duplicate entry

        if (ex.getMessage().contains("Duplicate entry")){
            //冬天截取重复的用户名  用空格隔开
            String[] split = ex.getMessage().split(" ");
            //split[2]  “zhangsan的下标”
            String msg = split[2] + "已经存在";
            //通用返回结果
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

总结:
1、根据产品原型明确业务需求
2、重点份析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
+++++++++++++++++++++++++++++++++++++++
其他功能流程和新增功能流程相同。数据格式不同+
+++++++++++++++++++++++++++++++++++++++

在这里插入图片描述

5、员工信息分页查询

5.1、程序的执行过程:

1、页面发送ajax请求,将分页查询参数(page、pagesize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面 5、页面接收到分页数据并通过Elementul的Table组件展示到页面上

5.2、代码开发

第一步:
config包下MP分页插件配置类

/**
 * 配置MP的分页插件
 */
@Configuration//声明这是配置类
public class MybatisPlusConfig {
    @Bean//bean注解 spring来管理
    //通过拦截器的方式把插件加载进来
    public MybatisPlusInterceptor mybatisPlusConfigInterceptor(){
        //创建拦截器对象
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //加入插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

第二步:
controller层EmployeeController代码

    /**
     * 分页处理的数据 用page接受
     * 员工信息查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")//get请求 所以getmapping
    //设置默认页数和单页数据条数 name:查询的用户
    public R<Page> page(int page,int pageSize,String name){
        log.info("page ={},pageSize = {},name = {}",page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //添加过滤添条件
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);
    }

6、启用/禁用员工账号

只有管理员能进后台和设置账号的状态 。
某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”

6.1、代码开发

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

在这里插入图片描述

6.2、执行过程

1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
在这里插入图片描述

页面中的ajax请求是如果发送的呢?
在这里插入图片描述

第一步:
controller层EmployeeController代码

    /**
     * 根据id修改员工信息
     * @return
     */
    @PutMapping
    public R<String> updata(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());
        //获取修改人信息
        Long empId = (long)request.getSession().getAttribute("employee");
        employee.setUpdateTime(LocalDateTime.now());//当前修改时间
        employee.setUpdateUser(empId);//修改人信息。当前登录的人可以修改,所修改人=当前登陆人
        employeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

数据库id和禁用启动操作id不同(id 19位)
原因:Long(1前6位)类型丢失精度
在这里插入图片描述

第二步:
解决上述问题:
在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
具体实现步骤:
1)提供对象转换器jacksonobjectMapper,基于jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)。放common包
2)在WebMvcConfig配置类中扩展Springmvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换


    /**
     * 扩展mve框架的消息转换器
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //创建消息转换器对象
//  我们自己new 的转换器放入 converters中。默认8个转换器,现在+1
        MappingJackson2CborHttpMessageConverter messageConverter = new MappingJackson2CborHttpMessageConverter();
        //设置对象转换器,底层使用Jackson将java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
//        将上面的消息转换器对象追加到mvc框架的转换器集合中
        //注意add导入index类。 0:优先使用自己的转换器
            converters.add(0,messageConverter );
    }

7、编辑员工信息

7.1、执行流程:

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
http://localhost:8000/backend/page/member/add.htmlid=1644891700080074753

2、在add.html页面获取url中的参数员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作

7.2、代码开发

controller层EmployeeController代码

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

Ⅱ、分类管理

1、公共字段自动填充

1.1、问题分析

员工管理功能开发时,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段
> 多张表都有的字段

1.2、代码实现

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

实现步骤:
1、在实体类的属性上加入***@TableField***注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口

    /**
    第一步:1、在实体类的属性上加入***@TableField***注解,指定自动填充的策略
     * 那些是公共字段
     * 就加上@TableField注解
     * fill :填充  =号后边是填充策略
     * 详细策略看FieldFill类
     */
    @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;

1.3、功能完善

注意:①当前我们设置createUser和updateUser为固定值 后面我们需要进行改造,改为动态获得当前登录用户的id。
②可以使用ThreadLocal来解决此问题,它是DK中提供的一个类。

在学习ThreadLocal之前,先确认一个事情:客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LogincheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFi方法

/**
 * 测试
 * 可以在上面的三个方法中分别加入下面代码(获取当前线程id):
 * long id = Thread。currentThread().getId();
 * log.info("线程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、在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id

package com.itheima.reggie.common;
/**
 * 1、编写Basecontext工具类,基于ThreadLocal封装的工具类
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    //id 是lang类型 所以泛型是long
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);//保存值
    }
    public static Long getCurrentId(){
        return threadLocal.get();//取值
    }
}
// 第二步:在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
//4、判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));//获取id
        //获取id
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }
        log.info("用户未登录");
3、在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id        metaObject.setValue("updateUser",new Long(1));
metaObject.setValue("createUser",BaseContext.getCurrentId());

2、新增分类

菜品分类和套餐分类。在后台系统中添加菜品时选择一个菜品分类

在开发业务功能前,先将需要用到的类和接口基本结构创建好
实体类Category(直接从课程资料中导入即可)
Mapper接口CategoryMapper
业务层接口Categorysenvice
业务层实现类Categoryservicelmpl
控制层Categorycontroller

2.1代码开发

2.2程序的执行过程:

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

在这里插入图片描述

package com.itheima.reggie.controller;
/**
 * 分类管理
 */
@RestController
@RequestMapping("/categoty")
@Slf4j
public class CategoryController {
    @Autowired
    private CaregoryService caregoryService;
    /**
     * 新增分类成功
     */
    @PutMapping
    public R<String> save(@RequestBody Category category){
        log.info("category:{}",category);
        caregoryService.save(category);
        return R.success("新增分类成功");
    }
}

功能测试:可以新增菜品分类

3、分类信息分页查询

3.1执行过程:

1、页面发送ajax请求,将分页查询参数(page、pagesize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过Elementul的Table组件展示到页面上

package com.itheima.reggie.controller;
    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize){
        //分页构造器
        //页数和一页的条件
        Page<Category> pageInfo = new Page<>(page,pageSize);
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);

        //进行分页查询
        categoryService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

4、删除分类

需求分析

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

代码开发

1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库

CategoryController层
   /**
     * 根据id删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long id){
        log.info("删除分类,id为:{}",id);

        categoryService.removeById(id);
        return R.success("分类信息删除成功");
    }

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

要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish(菜单实体类)和Setmeal(套餐实体类)(从课程资料中复制即可
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和Setmealservice
4、Service实现类DishServicelmpl和SetmealServicelmpl


service包下impl

@Service
public class CategoryServiceImpl extends ServiceImpl<CateGoryMapper,Category> implements CaregoryService {
    //注入,
    //菜品分类id   private Long categoryId;
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;
    /**
     * 根据id删除分类,删除之前需要经进行判断
      * @param id
     */
    @Override
    public void remove(Long id) {
        //构造查询条件
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加查询条件,根据分类categoryId 数据库是:category_Id 进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        //count 统计数据量
        int count1 = dishService.count(dishLambdaQueryWrapper);

//        查询当前分类是否关联了菜品,如果已经关联、抛出一个业务异常
        if (count1 > 0){
            //已经关联菜品,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }

//        查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdQueryWrapper = new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        setmealLambdQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2= setmealService.count();
        if (count2 > 0){
            //已经关联套餐,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        //菜品和套餐都没有关联
        //可以正常删除分类   调用删除方法
        super.removeById(id);
    }
}

common包下自定义异常类


/**
 * 自定义业务异常
 */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

common包下全局异常类

    /**
     * 异常处理方法
     */
    //@ExceptionHandler声明要处理的类
    //此处处理的是SQLIntegrityConstraintViolationException的异常
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHamdler(CustomException ex){
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }

5、修改分类

service层impl下CategoryServiceImpl

    /**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Category category){
        log.info("修改分类信息:{}",category);

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

Ⅲ、菜品管理业务开发

1、文件上传下载

文件上传介绍
在这里插入图片描述

  • 服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
    ①commons-fileupload
    ②commons-io
    /**
     * 文件上传
     * Spring框架在spring-web包中对文件上传进行了封装,简化服务端代码
     * 我们只需要在Controller的方法中声明
     * 一个MultipartFile类型的参数即可接收上传的文件,如下:
     * 本质还是上边的两种方式
     * @param file
     * @return
     */
    @PostMapping(value = "/upload")
    public R<String> upload (MultipartFile file){
        System.out.println(file);
        return null;
    }
}

文件下载介绍:
文件下载,download,指将文件从服务器传输到本地计算机的过程。


通过浏览器进行文件下载,通常有两种表现形式:
①以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
②直接在浏览器中打开

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

1.1、文件上传代码实现

文件下载代码实现
第一步:在这里插入图片描述

第二步:controller层创建CommonController菜品管理类

/**
 * 文件上传和下载
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
    //读取配置文件中转存位置
    //application.yml文件
    //reggie:
    //  path: D:\
    @Value("${reggie.path}")
    private String basePath;
    /**
     * 文件上传
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());
        /**
         * 防止总是被拦截器拦截,在拦截器类设置不需要拦截直接放行
         *         String[] urls = new String[]{
         *                 //菜品管理
         *                 "/common"
         *         };
         */
        //原始文件名
        String originalFilename = file.getOriginalFilename();//abc.jpg
        //动态截取文件后缀  suffix接受新的文件名 .之后的
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖  新文件 + 后缀(.jpg)
        String fileName = UUID.randomUUID().toString() + suffix;//qwertt.jpg

        //判断application.yml设置的指定位置目录是否存在
        File dir = new File(basePath);
        //判断当前目录是否存在
        if (!dir.exists()){
            //目录不存在 ,需要创建
            dir.mkdirs();
        }

        try {
            //将临时文件转存到指定位置.
            //转存位置可在application.ym中设置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        //文件名称需要放在数据库中  返回fileaName
        return R.success(fileName);
    }

    /**
     * 文件下载
     * @param name
     * @param response
     */
    //通过流写回数据  不需要返回值
//    输出流需要respinse获得
    @GetMapping("/download")
    public void download(String name , HttpServletResponse response){
        try {
            //输入流,通过输入流读取文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片了  输出流需要respinse获得
            ServletOutputStream outputStream = response.getOutputStream();

//            设置想要返回什么类型文件
                    response.setContentType("image/jpeg");

//            将读到的内容,放到数组中去
            int len = 0;
            byte[] bytes = new byte[1024];
            //-1 说明没有读完
            while ((fileInputStream.read(bytes)) != -1){
//                用过输出流向浏览器写。从第一个写 写len这么长
                outputStream.write(bytes,0,len);
                outputStream.flush();//刷新
            }
//            关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2、新增菜品

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

准备工作:
实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
Mapper接口DishFlavorMapper
业务层接口DishFlavorService
业务层实现类DishFlavorservicelmpl
控制层Dishcontroller

1、Mapper接口DishFlavorMapper
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
2、业务层接口DishFlavorService
public interface DishFlavorService extends IService<DishService> {
}
3、业务层实现类DishFlavorservicelmpl
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> {
}
4、控制层Dishcontroller
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
public class DishController {
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;
}

2.1梳理交互过程

1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端


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

   /**
   控制层CategoryController.java
     * 根据条件查询分类数据
     * @param category
     * @return
     */
        @GetMapping("/list")
    //list方法,根据条件查询方法
    public R<List<Category>> list(Category category){
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //动态添加条件
        queryWrapper.eq(category.getType() != null,Category::getType,category.getType());

        //添加排序条件
        //第一个:根据sort排序
        //第二个:根据创建时间排序
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }

DTO,全称为DataTransferObject,即数据传输对象,一般用于展示层与服务层之间的数据传输。
dish中的数据和实体类不是一一对应
导入DTO

//导入DTO需要单独建包
@Data
public class DishDto extends Dish {
    //继承dish中的属性。扩展一些属性
//    flavors接受页面传过来的数据
    private List<DishFlavor> flavors = new ArrayList<>();
    private String categoryName;
    private Integer copies;
}
//controller层
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
    //插入两张表
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    //加@RequestBody接收JSON数据
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());

        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }
}

表操作
service层创建DishService接口

public interface DishService extends IService<Dish> {
    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavol
    public void saveWithFlavor(DishDto dishDto);
}

Service层impl包创建DishServiceImpl时间DishService接口

@Slf4j
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    //控制口味表
    private DishFlavorService dishFlavorService;
    /**
     * 新增菜品,同时保存对应的口味数据
     * @param dishDto
     */
    @Transactional//事务控制(数据库操作)的注解
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到莱品表dish
        this.save(dishDto);
        Long dishId = dishDto.getId();//菜品id
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        //遍历集合flavors 遍历DishFlavor实体类中的数据.此处使用流
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}

Controller层创建DishController


/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
    //插入两张表
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    //加@RequestBody接收JSON数据
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());

        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }
}

3、菜品信息分页查询

3.1交互过程

1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pagesize、name)
提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求。

页面放回的是name(菜品名称),无菜品分类。
菜品分类无法展示
解决:①Dto中创建菜品分类属性categoryName
②Dto继承实体类category=给实体类加了一个属性(菜品分类)
categoryName

3.2代码开发

controller层dishcontroller分页查询代码

   /**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public  R<Page> page(int page,int pageSize, String name){

        //构造器分页构造器对象
        Page<Dish> pageInfo = new Page<>(page, pageSize);
        //将dish中属性的值拷贝给DishDto
        Page<DishDto> dishDtoPage = new Page<>();

        //条件构造器
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //根据name 通过like进行模糊查询。添加过滤条件
        queryWrapper.like(name != null,Dish::getName,name);
        //添加排序条件。根据更新时间updateTime 降序排序
        queryWrapper.orderByDesc(Dish::getUpdateTime);

//     执行分页查询  调用page方法 ,传入pageInfo queryWrapper
        dishService.page(pageInfo,queryWrapper);

//        拷贝属性  使用BeanUtils类  pageInfo拷贝到dishDtoPage,忽略records属性
        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
//        处理pageInfo中忽略的records属性为DishDto属性

//        遍历records 最后手机起来
        List<Dish> records = pageInfo.getRecords();
        List<DishDto> list = records.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            //Dish实体类中其他属性都是空的,需要拷贝
            BeanUtils.copyProperties(item,dishDto);

            Long categoryId  = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);

            if (category != null) {
                //获取分类名称  需要一个Dto并给Dto赋值
                String categoryName = category.getName();
                //需要一个Dto并给Dto赋值
                dishDto.setCategoryName(categoryName);
//            此处categoryName包含菜品分类(categoryName)和其他普通属性
            }
            return dishDto;
        }).collect(Collectors.toList());
        
        dishDtoPage.setRecords(list);
        return R.success(dishDtoPage);
    }

4、修改菜品

4.1交互过程

1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求

4.2代码开发

4.2.1根据id获取当前菜品信息

service层DishServiceImpl


    /**
     * 根据:id查询菜品信息和对应的口味信息
     * @param id
     * @return
     */
    public DishDto getByIdWithFlavor(Long id) {
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);
//第一部分:拷贝普通用户值
        //拷贝
        DishDto dishDto = new DishDto();//创建dishDto对象
        BeanUtils.copyProperties(dish,dishDto);

//查询当前菜品对应的口味信息,从dish_flavor表查询
//        第二部分:拷贝口味的值
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); //构造条件过滤器   <对应的实体>
        //添加条件 调用DishFlavor中dishId,dish.getId:获取到id值
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        //调用dishFlavorService查询
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

//        单独给dishDto赋值
        dishDto.setFlavors(flavors);

        //查询完之后需要返回DishDto类型需要拷贝
//        把第一个和第二个的属性值放回给dishDto类型
        return dishDto;
    }

controller调用service中的修改菜品方法getByIdWithFlavor

    /**
     * 根据id查询菜品信息和对用的口味信息
     * Service层写完 调用controller层
     * @param id
     * @return
     */
    //口味不是普通属性,需要勇DishDto类型数据
    @GetMapping("/{id}")
    //@PathVariable 获取id注解
    public R<DishDto> get(@PathVariable Long id){
        //此处需要查两张表dish 和口味DISH_Flavor。在service层DishService接口中创建方法.并在dishserviceImpl实现接口方法
        //调用service层方法
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        //直接返回
        return R.success(dishDto);
    }

4.2.2修改菜品

修改代码开发

service业务层DishService接口中创建修改方法updateWithFlavor。并实现接口方法

public interface DishService extends IService<Dish> {
    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavol
    public void saveWithFlavor(DishDto dishDto);

    //根据id查询菜品信息以及他所对应的口味信息
    public DishDto getByIdWithFlavor(Long id);
    //更新菜品信息,同时更新对应的口味信息
    public void updateWithFlavor(DishDto dishDto);
}

DishServiceImpl实现接口中修改updateWithFlavor方法

 /**
     * 修改菜品信息和口味信息 
     * @param dishDto
     */
    @Override
    @Transactional//开启事务注解,保证事务一致性
    public void updateWithFlavor(DishDto dishDto) {
        //1、更新dish表基本信息
            //dishDto继承dish ,此处继承调用dishDto == 更新dish中的方法,也就是普通属性
        this.updateById(dishDto);

        //2、清理当前菜品对应口味数据--dish__flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();//创建对象
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());//添加条件

        dishFlavorService.remove(queryWrapper);//执行sql

        //3、添加当前提交过来的口味数据--dish__flavor表的insert操作
            //插入来自页面的信息,在dishDto中已经封装
        List<DishFlavor> flavors = dishDto.getFlavors();
            //DishFlavor属性不全,
                // 解决:遍历flavors,把每一项拿出来,从dishDto中重新setDishId
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;//处理之后重新付给flavors
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);

    }

controller层DishController

    /**
     * 修改菜品
     * @param dishDto
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());

        dishService.updateWithFlavor(dishDto);
        return R.success("修改菜品成功");
    }

Service业务层DishServiceimpl


    /**
     * 修改菜品信息
     * @param dishDto
     */
    @Override
    @Transactional//开启事务注解,保证事务一    致性
    public void updateWithFlavor(DishDto dishDto) {
        //1、更新dish表基本信息
            //dishDto继承dish ,此处继承调用dishDto == 更新dish中的方法,也就是普通属性
        this.updateById(dishDto);

        //2、清理当前菜品对应口味数据--dish__flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();//创建对象
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());//添加条件

        dishFlavorService.remove(queryWrapper);//执行sql

        //3、添加当前提交过来的口味数据--dish__flavor表的insert操作
            //插入来自页面的信息,在dishDto中已经封装
        List<DishFlavor> flavors = dishDto.getFlavors();
            //DishFlavor属性不全,
                // 解决:遍历flavors,把每一项拿出来,从dishDto中重新setDishId
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;//处理之后重新付给flavors
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);

    }

Ⅳ、套餐管理业务开发

1、新增套餐

需求分析

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

数据模型

新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。
————————————————————————————

涉及的表:
setmeat 套餐表
setmeaLdish 套餐菜品关系表

准备工作

实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了
DTOSetmealDto(直接从课程资料中导入即可)
Mapper接口SetmealDishMapper
业务层接口SetmealDishservice
业务层实现类SetmealDishServicelmpl
控制层Setmealcontroller

SetmealDishMapper

@Mapper
//<>里的是实体
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

SetmealDishservice

//<>里是实体类
public interface SetmealDishservice extends IService<SetmealDish> {
}

SetmealDishServicelmpl

@Slf4j
@Service
//ServiceImpl两个泛型类型<对应的mapper,对应的实体类>
public class SetmealDishServicelmpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishservice {
}

Setmealcontroller

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/setmeal")
public class Setmealcontroller {
//    注入
    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishservice setmealDishservice;
}

1.1交互过程

1、页面(backend/page/combo/add.htmD)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。

controller层
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中

    /**
     * 根据条件查询对应的菜品数据
     * @param dish
     * @return
     */
    @GetMapping("/lsit")
    public R<List<Dish>> list(Dish dish){

        //构造查询条件  泛型Dish
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //添加条件
        queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

        //添加条件,查询状态为1(起售状态) 的菜品
        queryWrapper.eq(Dish::getStatus,1);
//        添加排序条件, 根据实体类Dish类sort排序 desc降序排序
        queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

        //调用dishService 把queryWrapper分装成list类型
        List<Dish> list = dishService.list(queryWrapper);
        return R.success(list);
    }

保存数据到对应表
service层SetmealService接口和实现接口方法SemealServiceImpl
SemealServiceImpl实现接口方法,controller层调用service方法

SetmealService

public interface SetmealService extends IService<Setmeal> {
    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * 将套餐基本信息和关联的菜品信息保存,所以泛型是SetmealDto
     */
    public void saveWithDish(SetmealDto setmealDto);
}

SemealServiceImpl

    public class SemealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
    @Autowired
    private SetmealDishservice setmealDishservice;
    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     */
    @Transactional//事务注解 要么全成功要么全失败
    public void saveWithDish(SetmealDto setmealDto) {
//        保存套餐的基本信息,操作setmeal(套餐表),执行insert操作
        this.save(setmealDto);

        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes.stream().map((item) ->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());
//        保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
        //saveBatch批量保存
        setmealDishservice.saveBatch(setmealDishes);
    }
}

controller层调用service层

/**
 * 套餐管理
 */
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class Setmealcontroller {
//    注入
    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishservice setmealDishservice;

    /**
     * 新增套餐
     * 除了setmel属性还有其他属性(setmealDto)的属性
     * 所以不能用setmel当泛型
     * @RequestBody接受Json格式数据
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> save (@RequestBody SetmealDto setmealDto){
        log.info("套餐信息:{}",setmealDto);

        setmealService.saveWithDish(setmealDto);

        return R.success("新增套餐成功");
    }
}

2、套餐信息分页查询

1.1交互过程

1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pagesizename)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
> 开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求

    /**
     * 套餐分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize,String name){
//        分页构造器
        Page<Setmeal> pageInfo = new Page<>(page,pageSize);//Setmeal实体类没有套餐分类
        //SetmealDto继承Setmeal实体类,并添加套餐分类属性categoryName.
//        但SetmealDto中普通属性(Setmeal)没有值--》把分页查询的结果拷贝给dtoPage
        Page<SetmealDto> dtoPage = new Page<>();

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//        添加查询条件,根据(参数)name进行1ike模糊查询
        queryWrapper.like(name != null,Setmeal::getName,name);
//        Setmeal实体类中有    updateTime
        //添加排序条件,根据更新时间降序排列
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        //查询结果:调用setmealService page方法查询
        setmealService.page(pageInfo,queryWrapper);

        //拷贝对象(将pageInfo拷贝给dtoPage,recoreds是不用拷贝)
        //recoreds是集合,把查询的数据封装进去,他的泛型是T,此处泛型是SetmealDto或Setmeal
        BeanUtils.copyProperties(pageInfo,dtoPage,"recoreds");

        //recoreds忽略,需要赋值
        List<Setmeal> recoreds = pageInfo.getRecords();

        //直接勇list集合收集

        List <SetmealDto> list = recoreds.stream().map((item) -> {
            //定义SetmealDto类型的List,此处定义一个SetmealDto
            SetmealDto setmealDto = new SetmealDto();

            //new的setmealDto此时只有categoryName,没有其他普通属性
            //拷贝
            BeanUtils.copyProperties(item,setmealDto);

            //获得套餐分类ID
            Long categoryId = item.getCategoryId();
//            通过分类id查询分类对象  .此处获得一个对象。是查询的分类的分类对象
            Category category = categoryService.getById(categoryId);
            if (category !=null){
                //分类名称
                String categoryName = category.getName();
                //获得套餐分类categoryName
                setmealDto.setCategoryName(categoryName);
                //new的setmealDto此时只有categoryName,没有其他普通属性
            }
            //值返回给setmeaDto
            return setmealDto;
            //收集起来 并转成list集合
        }).collect(Collectors.toList());

        //recoreds计算之后 赋值给list 而List泛型为SetmealDto
        //将数据勇list集合接受写在上方
//        List<SetmealDto> list = null;

        //页面套餐数据没有,页面数数据构和此处返回的数据结构不同。导致不匹配 套餐分类没有数据
//        return R.success(dtoPage);

        //此时list包含所有属性的值,将list给dtopage
        dtoPage.setRecords(list);
        return R.success(dtoPage);
    }
}

3、删除套餐

单、多选删除。在售必须停售才能删除
在这里插入图片描述

service层

SetmealService接口

    /**
     * 删除套餐,同时需要删除套餐和莱品的关联数据
     * @param ids
     */
    public void removeWithDish(List<Long> ids);

SemealServiceImpl

    /**
     * 删除套餐,同时需要删除套餐和莱品的关联数据
     * @param ids
     */
    @Override
    @Transactional
    public void removeWithDish(List<Long> ids) {
//第一步:查询套餐状态,确定是否可用删除
    //构造查询条件对象
        //select count() from setmeal where id in (3) and status=1
        LambdaQueryWrapper<Setmeal>  queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal::getId,ids);//多查询多条件。第一个条件
        queryWrapper.eq(Setmeal::getStatus,1);//第二个条件

        int count =this.count(queryWrapper);
        if (count > 0) {
            //如果不能删除,抛出一个业务异常
            throw new CustomException("正在售卖中,不能删除");
        }
        //第二步:如果可以删除,先删除套餐表中的数据--setmeal
        this.removeByIds(ids);//批量删除

        //delete from setmeal_dish where setmealid in (l,2,3)
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
        //第三步:删除关系表中的数据
        setmealDishservice.remove(lambdaQueryWrapper);
    }

controller层调用service层

Setmealcontroller

    /**
     * 删除套餐
     * ids有多个,Long类型
     * 使用liST集合接收
     * @return
     */
    @DeleteMapping
    public R<String> delete(@RequestParam List<Long> ids){
        log.info("id:{}",ids);

        //controller层调用Service层
        setmealService.removeWithDish(ids);
        return R.success("套餐数据删除成功!");
    }

Ⅴ、手机验证码登录

1、短信发送

第三方短信服务会和各个运营商(移动、联通、电信)对接
常用短信服务:
阿里云/华为云/腾讯云/京东/梦网/乐信

第一步: 阿里云短信服务-注册账号
https://www.aliyun.com/

第二步: 阿里云短信服务-设置短信签名
短信消息–国内消息设置
短信签名是短信发送者的署名,表示发送方的身份

第三步: 阿里云短信服务-设置短信模板

在这里插入图片描述

第四步:阿里云短信服务-设置Accesskey
在这里插入图片描述

参照官方提供的文档。
具体开发步骤:
1、导入maven坐标
2、调用API
1
3、资料导入工具类

        <!--短信发送 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.16</version>
        </dependency>
        <dependency>
            <groupId>com.liyun</groupId>
            <artifactId>aliiyun-java-sdk-dysmsapi</artifactId>
            <version>2.1.0</version>
        </dependency>

2、手机验证码登录

2.1、交互过程

1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调
用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

2.2准备

实体类User(直接从课程资料中导入即可)
Mapper接口UserMapper
业务层接口Userservice
业务层实现类Userservicelmpl
控制层Usercontroller
工具类SMSUtils、ValidatecodeUtils(直接从课程资料中导入即可)

2.3代码开发

代码开发-修改LoginCheckFilter
在LoginCheckFilter过滤器中扩展逻辑,判断移动端用户登录状态

  //4-2、移动端登录  判断登录状态,如果已登录,则直接放行
// 第二步:在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
        if (request.getSession().getAttribute("user") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));//获取id
            //获取id
            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);

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

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

登录校验
UserController

    /**
     * 移动端用户登录
     * @param map
     * @param session
     * @return
     */
    @PostMapping("/login")
    //{phone:“13412345678“,code:“1234}
    //①Dto继承实体类扩展一个属性
    //②类型是kv 对 使用map
    //登录成功给页面返回登录信息 泛型勇user
    public R<User> sendMsg(@RequestBody Map map, HttpSession session){
        log.info(map.toString());
//一、获取手机号
        String phone = map.get("phone").toString();
//二、获取验证码
        String code = map.get("code").toString();
//三、从Session中获取保存的验证码
        Object codeInSession = session.getAttribute(phone);
//四、进行验证码的比对(页面提交的验证码和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);
            }
            //登录成功给页面返回登录信息 泛型勇user
            return R.success(user);
        }
        return R.error("登录失败");
}

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

1、导入用户地址薄相关功能代码

实体类AddressBook(直接从课程资料中导入即可)
Mapper接口AddressBookMapper
业务层接口AddressBookservice
业务层实现类AddressBookservicelmp
控制层AddressBookcontroller(直接从课程资料中导入即可)

2、菜品展示

2.1、 交互过程

1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
在这里插入图片描述

修改DisController的list方法并测试
移动端打开点击加1有口味选择

@GetMapping("/lsit")
public R<List<DishDto>> list(Dish dish){

    //构造查询条件  泛型Dish
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

    //添加条件,查询状态为1(起售状态) 的菜品
    queryWrapper.eq(Dish::getStatus,1);
//        添加排序条件, 根据实体类Dish类sort排序 desc降序排序
    queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    //调用dishService 把queryWrapper分装成list类型
    List<Dish> list = dishService.list(queryWrapper);


    List<DishDto> dishDtoList = list.stream().map((item) -> {
        DishDto dishDto = new DishDto();
        //Dish实体类中其他属性都是空的,需要拷贝
        BeanUtils.copyProperties(item,dishDto);

        Long categoryId  = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);

        if (category != null) {
            //获取分类名称  需要一个Dto并给Dto赋值
            String categoryName = category.getName();
            //需要一个Dto并给Dto赋值
            dishDto.setCategoryName(categoryName);
//            此处categoryName包含菜品分类(categoryName)和其他普通属性
        }

        //查询口味
        Long dishID = item.getId();//菜品iD

        //根据菜品id查询口味
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加条件
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dishID);
        //sQL:select * from dish_flavor where dish_id = ?
        //查出口味的集合
        List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
        dishDto.setFlavors(dishFlavorList);
        return dishDto;
    }).collect(Collectors.toList());
    return R.success(dishDtoList);
}

创建SetmealController的list方法并测试

    /**
     * 根据条件查询套餐数据
     * @param setmeal
     * @return
     */
    @GetMapping("/list")
    public R<List<Setmeal>> list( Setmeal setmeal){
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        List<Setmeal> list = setmealService.list(queryWrapper);
        return R.success(list);
    }

3、购物车

在这里插入图片描述

实体类Shoppingcart(直接从课程资料中导入即可)
Mapper接口ShoppingCartMapper
业务层接口Shoppingcartservice
业务层实现类ShoppingCartservicelmp
控制层Shoppingcartcontroller

添加购物车
ShoppingCartController

public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        log.info("购物车数据:{}",shoppingCart);

        //设置用户id,指定当前是哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        Long dishId = shoppingCart.getDishId();

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);

        if(dishId != null){
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);

        }else{
            //添加到购物车的是套餐
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }

        //查询当前菜品或者套餐是否在购物车中
        //SQL:select * from shopping_cart where user_id = ? and dish_id/setmeal_id = ?
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if(cartServiceOne != null){
            //如果已经存在,就在原来数量基础上加一(更新操作)
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number + 1);
            shoppingCartService.updateById(cartServiceOne);
        }else{
            //如果不存在,则添加到购物车,数量默认就是一
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartService.save(shoppingCart);
            cartServiceOne = shoppingCart;
        }

        return R.success(cartServiceOne);
    }

查看、清空购物车

 /**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
        log.info("查看购物车...");

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
        queryWrapper.orderByAsc(ShoppingCart::getCreateTime);

        List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

        return R.success(list);
    }

    /**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    public R<String> clean(){
        //SQL:delete from shopping_cart where user_id = ?

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());

        shoppingCartService.remove(queryWrapper);

        return R.success("清空购物车成功");
    }

4、用户下单

去支付跳转页面
——————————
orders:订单表
order_detail:订单明细表

1、交互过程

在这里插入图片描述

2、准备

实体类Orders、OrderDetail(直接从课程资料中导入即可)
Mapper接口 OrderMapper、 OrderDetailMapper
业务层接口OrderService、OrderDetailservice
业务层实现类Orderservicelmpl、OrderDetaiSenicelmpl
控制层Ordercontroller、OrderDetailcontroller

OrderController

/**
 * 订单
 */
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

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

OrderServiceImpl

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

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookservice addressBookService;

    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 用户下单
     * @param orders
     */
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long userId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId,userId);
        List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);

        if(shoppingCarts == null || shoppingCarts.size() == 0){
            throw new CustomException("购物车为空,不能下单");
        }

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

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

        long orderId = IdWorker.getId();//订单号

        AtomicInteger amount = new AtomicInteger(0);

        List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());


        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));//总金额
        orders.setUserId(userId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
        //向订单表插入数据,一条数据
        this.save(orders);

        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);

        //清空购物车数据
        shoppingCartService.remove(wrapper);
    }

Ⅶ、缓存优化

用户数量多,系统访问量大
频繁访问数据库,系统性能下降,用户体验差

1、环境搭建

1、maven坐标

        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>

2、配置文件
在项目的application.yml中加入redis相关配置:

spring:
  redis:
    host:127.0.0.1
    port: 6379
    password: root
    database: 0

3、配置类

在项自中加入配置类Redisconfig
便于观察redis中的key
默认是另一种序列化方式

1、缓存短信验证码

session有效期是30分钟

1、1实现思路

在这里插入图片描述

1、2、代码改造

Controller层UserController

①注入

> //    1、在服务端Usercontroller中注入RedisTemplate对象,用于操作Redis
    @Autowired
    private RedisTemplate redisTemplate;

②发送手机短信验证码方法

            //第四步:需要将生成的验证码保存到Session
//            session.setAttribute(phone,code);

            //将生成的验证码缓存到Redis中,并且设置有效期为5分钟
            //object key,Object value,Duration timeout)
            redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);

③移动端用户登录获取验证码&成功删除验证码

//三、从Session中获取保存的验证码
//        Object codeInSession = session.getAttribute(phone);

        //从Redis中获取缓存的验证码
        Object codeInSession=redisTemplate.opsForValue().get(phone);
(略)

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

2、缓存菜品数据

频繁查询数据,数据库性能下降

1、改造Dishcontroller的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis。
2、改造Dishcontroller的save和update方法,加入清理缓存的逻辑
注意:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。

代码改造

DishController层

    @Autowired
    private RedisTemplate redisTemplate;

list方法

@GetMapping("/lsit")
public R<List<DishDto>> list(Dish dish){
        List<DishDto> dishDtoList = null;
        //动态构造key查询现根据菜品(分类 川 粤)和分类下的菜品,请求数据有分类 id 状态
    //dish_135456454553_1
    String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();

    //先从redis中获取缓存数据 强转
    dishDtoList =(List<DishDto>) redisTemplate.opsForValue().get(key);

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

    //构造查询条件  泛型Dish
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

    //添加条件,查询状态为1(起售状态) 的菜品
    queryWrapper.eq(Dish::getStatus,1);
//        添加排序条件, 根据实体类Dish类sort排序 desc降序排序
    queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    //调用dishService 把queryWrapper分装成list类型
    List<Dish> list = dishService.list(queryWrapper);


     dishDtoList = list.stream().map((item) -> {
        DishDto dishDto = new DishDto();
        //Dish实体类中其他属性都是空的,需要拷贝
        BeanUtils.copyProperties(item,dishDto);

        Long categoryId  = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);

        if (category != null) {
            //获取分类名称  需要一个Dto并给Dto赋值
            String categoryName = category.getName();
            //需要一个Dto并给Dto赋值
            dishDto.setCategoryName(categoryName);
//            此处categoryName包含菜品分类(categoryName)和其他普通属性
        }

        //查询口味
        Long dishID = item.getId();//菜品iD

        //根据菜品id查询口味
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加条件
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dishID);
        //sQL:select * from dish_flavor where dish_id = ?
        //查出口味的集合
        List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
        dishDto.setFlavors(dishFlavorList);
        return dishDto;
    }).collect(Collectors.toList());

    //如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis,1h过期
    redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);

    return R.success(dishDtoList);
}

Spring Cache
缓存套餐数据

猜你喜欢

转载自blog.csdn.net/weixin_55008454/article/details/130030166