吉瑞外卖笔记

1.项目整体搭建

这里用到的是springboot3+mybatisplus

1.1数据库搭建

整体表搭建,这里我是直接用的老师给的数据库

 1.2maven项目搭建

依赖

这两个jar包第一次用,记录一下

fastjson     json处理,可将对象转化为json形式  可将对象中的属性以string的形式响应出去

因为这里用的是雪花算法,生成的id是64位,类型是long型,响应给客户端后客户端因为长度过大会精度损失使得客户端想服务器传递过来的id是错误的,所以这里不能以long类型传递,要以string传递

使用注解@JsonFormat(shape = JsonFormat.Shape.STRING)解决上面问题

设置响应日期格式@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

commons-lang  工具类可用于MD5加密

    <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.0.5</version>
    </parent>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.20</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--json,用户json转化,设置对象属性响应时的json形式 比如一个属性是long型,可以设置
            string-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <!--工具类,可以省略MD5等工具-->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>

                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>


            </plugin>
        </plugins>
    </build>

1.3 application.yml配置


server:
  port: 8080
  servlet:
    context-path: /
spring:
  application:
    #应用的名称,可选
    name: reggie_take_out
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db_reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456

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  #主键策略,雪花算法
  type-aliases-package: org.example.pojo
  mapper-locations: classpath:/mapper/*.xml

#临时文件路径  项目启动后,有的功能要上传文件,这时文件会保存在临时文件中
reggie:
  path: F:/Java图片/

1.4创建项目启动入口

@SpringBootApplication
@MapperScan("org.example.mapper")
@ServletComponentScan  //将过滤器给扫描进来
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
    
    //mybatis plus插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));  //分页插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());   //乐观锁
        return interceptor;
    }
}

1.5前端资源引入

首先创建static目录,前端资源这里直接复制就行,切记要在static目录下

如果资源不在static目录,那么记得在application.yml中设置

1.6使用mybatisx生成

这里先连接数据库,选择所有表,然后生成对应的实体类,service,service实现类,mapper,mapper.xml

2.后台登录功能开发

登入的请求

请求体

这时候我们编写对应的controller

1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果

后续客户端会根据session中的内容来判断它是否登入成功,我们也会根据session中的内容来判断是哪个用户来进行操作

    /**
     * @Description: 登入
     * @param: request  如果登入成功,就将用户的id存入session中
     * @param: employee
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/8 18:38
     */


    @PostMapping("login")
    public R login(HttpServletRequest request, @RequestBody Employee employee) {
        R r = employeeService.login(request, employee);
        return r;
    }

service实现类

    @Override
    public R login(HttpServletRequest request, Employee employee) {
       employee.setPassword(DigestUtils.md5DigestAsHex(employee.getPassword().getBytes()));//加密
        LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Employee::getUsername,employee.getUsername());
        Employee byId = employeeMapper.selectOne(wrapper);  //根据客户端传来的用户名来
        if(byId == null){
            return R.error("没有这个用户");
        }else{
           if(employee.getPassword().equals(byId.getPassword())){
                    if(byId.getStatus() == 0){
                return R.error("该用户已被禁用");
                    }else {

                        HttpSession session = request.getSession();
                        session.setAttribute("employee",byId.getId());
                        return R.success(byId);
                    }
           }else {
               return R.error("用户密码错误");
           }
        }
    }

3.后台退出功能

退出的请求

编写对应的controller

客户端根据session中的内容来判断登入是否成功,这里我们只需把session清空就可以

    /**
     * @Description:员工退出 清理session中的id,返回结果
     * @param: request
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/8 21:57
     */

    @PostMapping("logout")
    public R logout(HttpServletRequest request) {
        R r = employeeService.logout(request);
        return r;
    }

service实现类

  @Override
    public R logout(HttpServletRequest request) {
        HttpSession session = request.getSession();
        session.removeAttribute("employee");  //删除session中的数据
        return R.success("退出成功");
    }

4.员工管理模块

4.1完善员工登录功能(重点)

前面的登陆存在一个问题,如果用户不进行登陆,直接访问系统的首页,照样可以正常访问,这种设计是不合理的,我们希望看到的效果是只有完成了登陆后才可以访问系统中的页面,如果没有登陆则跳转到登陆页面

这里我们使用的是过滤器

 自定义过滤器

package org.example.filter;

import com.alibaba.fastjson.JSON;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.example.common.BaseContext;
import org.example.common.R;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.io.IOException;


@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); //路径匹配器,支持通配符

    /*
     * @Description:
     *检查是否登入
     * 逻辑如下
     * 1.获取本次请求的url
     * 2.判断本次请求是否需要处理
     * 3.如果不需要处理,那就直接放行
     * 4.判断登入状态,如果已登录,则直接放行
     * 5.如果为登入则返回未登录结果
     *  注意:如果这里没有跳转登录页面,可以清理游览器缓存再次尝试
     * @param: null
     * @return:
     * @Author: 刘某人
     * @Date 2024/8/8 22:49
     */

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //1.获取本次请求的url
        String requestURI = request.getRequestURI();
        log.info("拦截到请求:{}", requestURI);
        String[] urls = new String[]{    //里面定义的是不需要处理的请求    "/backend/**", "/front/**" 这两个是静态资源,不需要过滤,只处理请求,//"/common/**"是文件的上传下载
                "/employee/login", "/employee/logout", "/backend/**", "/front/**" ,"employee/page"
                ,"/common/**"
        };
        //  2.判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
        //  3.如果不需要处理,那就直接放行
        if (check) {
            log.info("本次请求{}不需要处理", requestURI);
            filterChain.doFilter(request, response);
            return;
        }
        // 4.判断登入状态,如果已登录,则直接放行
        Long employee = (Long)request.getSession().getAttribute("employee");//获取登入成功的session ,登入成功session有内容,未登入则没有也就是null
        if (employee != null) {       //非空则放行
           // long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
          //  log.info("当前线程id为{}",id);

            BaseContext.setCurrentId(employee); //将当前session的id存入当前线程


            log.info("用户已登入,用户id为{}", employee);
            filterChain.doFilter(request, response);
            return;
        }
        //5.如果为登入则返回未登录结果,给客户端响应一个JSON数据
        log.info("用户未登入");
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));  // JSON.toJSONString 将对象转换为JSON字符串,是fastjson这个依赖的
        return;
    }

    /**
     * @Description: 路径匹配,检查本次请求是否需要方行
     * @param: urls
     * @param: url
     * @return: boolean
     * @Author: 刘某人
     * @Date 2024/8/8 23:08
     */
    public boolean check(String[] urls, String url) {
        for (String u : urls) {
            boolean match = PATH_MATCHER.match(u, url);  //匹配
            if (match) {
                return true;
            }
        }
        return false;
    }
}

这里需要给启动类上加@ServletComponentScan ,将我们的过滤器扫描进来,参考1.4我已经引入了

4.2新增员工

请求方式

请求体

编写controller代码

    @Override
    public R addEmployee(HttpServletRequest request,Employee employee) {

      //  employee.setCreateTime(new Date());  //设置初始时间
      //  employee.setUpdateTime(new Date());  //设置修改时间
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));  //设置默认密码
//        HttpSession session = request.getSession();
  //      long emp = (long)session.getAttribute("employee");  //获取当前登录的人的id
    //    employee.setCreateUser(emp);   //当前登入的人id创建的这个数据,所以这里设置他的id
     //   employee.setUpdateUser(emp);   //修改同样道理
        int insert = employeeMapper.insert(employee);
        if (insert > 0){
            return R.success("新增员工成功");
        }else {
            return R.error("新增员工失败");
        }
    }

4.3编写全局异常处理(重点)

在4.2新增员工中有可能会有异常,这里我们员工表中的员工账户设置的是唯一,如果我们在客户端输入的是一个重复的账户,那么就会报异常

创建全局异常处理类

@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class GlobalExceptionHandler {


    /**
     * @Description: 主键插入重复异常处理
     * @param: e
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/9 16:42
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R exceptionHandler(SQLIntegrityConstraintViolationException e) {
        log.info("SQLIntegrityConstraintViolationException: {}", e.getMessage());
        if (e.getMessage().contains("Duplicate entry")) {
            String[] split = e.getMessage().split(" ");
            String msg = split[2] + "已经存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }

这里为什么要e.getMessage().contains("Duplicate entry")

 是因为报插入异常的时候有这个,一但有了这个就说明他是插入异常

4.4员工分页查询 

这里给所有类的日期上加这个注解@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

设置他的时间的响应形式

请求方式和请求体

这里需要加入mybatisplus的分页插件,我已经加入了,参考1.4项目启动入口

这里的分页有两个功能1是分页,2是看用户会不会根据姓名来查询,也就是条件查询(这里用的模糊查询)

controller

    /**
     * @Description:
     * 员工分页
     * @param: page
     * @param: pageSize
     * @param: name  有可能会根据名字分页,看客户端输入不输入
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/9 17:15
     */

    @GetMapping("/page")
    public R page(@Param("page") int page,@Param("pageSize") int pageSize,@Param("name") String name){
        log.info("page={},pageSize={},name={}",page,pageSize,name);
        R r= employeeService.page(page,pageSize,name);
        return r;
    }

 service实现类

    @Override
    public R page(int page, int pageSize, String name) {

        Page<Employee> page1 = new Page<>(page, pageSize);
          LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
          //姓名过滤
        wrapper.like(name != null,Employee::getName,name);
        //排序一下
        wrapper.orderByDesc(Employee::getUpdateTime);
        //查询 ,封装给page1
        employeeMapper.selectPage(page1,wrapper);

        return R.success(page1);
    }

分页的三个时机,①用户登录成功时,分页查询一次 ②用户使用条件查询的时候分页一次 ③跳转页面的时候分页查询一次

4.5启用/禁用员工账户

这里需要我们在每个实体类的id上加入

@JsonFormat(shape = JsonFormat.Shape.STRING) 

原因前面已经说明了,客户端接收不了64位的long类型,会导致精度损失,这个注解是让我们把要响应给客户端的long类型设置为string类型,这样就不会精度损失了

要将每一个实体类的id上加入这个注解,切记

请求方式 (这个请求也是4.6的请求)

请求体

controller

    /**
     * @Description:
     * 用户admin修改其他员工的状态,根据id修改员工信息,两个功能
     *  注意:这里员工id是雪花算法生成的long类型,客户端会精度损失导致发送过来的id是错误的
     *  解决方式:需要在对应的实体类id上加上 @JsonFormat(shape = JsonFormat.Shape.STRING) 注解
     * @param: request
     * @param: employee
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/9 18:22
     */

    @PutMapping()
    public R updateEmployee(HttpServletRequest request,@RequestBody Employee employee){
        R r=employeeService.updateEmployee(request,employee);
        return r;
    }

service实现类

    /**
     * @Description:
     *          精度损失解决方式 在实体id上加@JsonFormat(shape = JsonFormat.Shape.STRING)注解
     *          解决方式二:消息转化器
     * @param: request
     * @param: employee
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/9 19:10
     */

    @Override
    public R updateEmployee(HttpServletRequest request, Employee employee) {
        System.out.println(employee);         //这里的员工id是不对的,因为雪花算法生成19位数字,而客户端接收的时候损失精度,在传到服务端的时候这个id就错了,所以修改失败
    //    Long employee1 =(Long) request.getSession().getAttribute("employee");
    //    employee.setUpdateUser(employee1);
       // employee.setUpdateTime(new Date());
       // long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
      //  log.info("当前线程id为{}",id);
        int i = employeeMapper.updateById(employee);
        System.out.println(i);
        return R.success("员工信息修改成功");
    }

4.6编辑员工信息

进入编辑员工的时候会出现一个请求,,用来展示编辑的内容

请求方式

controller

 @GetMapping("/{id}")
    public R getEmployeeById(@PathVariable Long id){
                R r=employeeService.getEmployeeById(id);
        return r;
    }

service实现类

  @Override
    public R getEmployeeById(Long id) {
        Employee byId = employeeMapper.selectById(id);
        return R.success(byId);

    }

修改的请求和4.5的请求一样这里无需展示

5.菜品分类管理

5.1公共字段填充(重点)

我们发现,数据库中,有好多张表,他们这几张都有这几个字段

那么这4个字段就是公共字段,而我们每次插入和修改的时候,这4个字段都比较固定

所有我们可以使用mybatisplus的公共字段填充功能,这样我们以后插入和修改的时候,这4个字段就不需要我们自己来编写了

第一步

所有实体类上这四个字段都加上这几个注解,切记是所有实体类

这是在指定自动填充的策略

比如说 createtimes 这个属性,只有在插入的时候才设置,其他时候不需要设置,所以这里只给他设置了插入填充

而updateTime字段, 插入和修改的时候都需要设置,所有这里是 INSERT_UPDATE

插入和修改填充

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

 第二步设置一个处理类:在此类中为公共字段赋值

这个类需要实现MetaObjectHandler 接口

package org.example.common;


import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;

import org.springframework.stereotype.Component;

import java.util.Date;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;


/**
 * @Description: 自定义元数据对象处理器
 * 用来处理数据库中公共数据
 * @param: null
 * @return:
 * @Author: 刘某人
 * @Date 2024/8/9 21:27
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

//    @Autowired   方法一:不推荐,如果session过多,不知道是用的哪个session
//    private HttpSession session;



    /**
     * @Description:
     * 插入操作,自动填充
     * @param: metaObject
     * @return: void
     * @Author: 刘某人
     * @Date 2024/8/9 21:33
     */

    @Override
    public void insertFill(MetaObject metaObject) {

     //   long employee = (long)session.getAttribute("employee");
        Long id = BaseContext.getCurrentId();//当前线程的id,也是当前登录用户的id,从session中获取的


        log.info("公共字段自动填充[insert]");

        metaObject.setValue("createTime",new Date());
        metaObject.setValue("updateTime",new Date());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
        log.info(metaObject.toString());
    }

    /**
     * @Description:
     *更新操作,自动填充
     * @param: metaObject
     * @return: void
     * @Author: 刘某人
     * @Date 2024/8/9 21:33
     */

    @Override
    public void updateFill(MetaObject metaObject) {
   //     long employee = (long)session.getAttribute("employee");
        log.info("公共字段自动填充[update]");
        long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
        log.info("当前线程id为{}",id);
        metaObject.setValue("updateTime",new Date());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
        log.info(metaObject.toString());
    }
}

在创建一个BaseContext类 用来获取动态员工id

我们用线程来获取当前登入员工的id,只要是登入成功,这里就会把员工的id存入session中

ThreadLocal 可以将session存储起来

方法:

发现:一个线程对应一次请求,不同线程对应不同请求

BaseContext 这里把ThreadLocal 设置为静态,是为了让它一直存在,可以一直获去当前当前请求的session,这个类还把ThreadLocal 的两个方法封装了

/**
 * @Description:
 * 基于于ThreadLocal封装的工具类,用户保存和获取当前登录用户id
 * @param: null
 * @return:
 * @Author: 刘某人
 * @Date 2024/8/9 22:16
 */

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


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

 因为每一次请求都需要经过过滤器,而我们的BaseContext正好可以过滤器中获取当前请求的id(也是session)

又因为只有登入成功才有session,所以我们的BaseContext在过滤器判断用户登入成功的那个地方获取session

过滤器判断登入成功代码(部分)完整部分上面有在4.1

  // 4.判断登入状态,如果已登录,则直接放行
        Long employee = (Long)request.getSession().getAttribute("employee");//获取登入成功的session ,登入成功session有内容,未登入则没有也就是null
        if (employee != null) {       //非空则放行
           // long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
          //  log.info("当前线程id为{}",id);
            BaseContext.setCurrentId(employee); //将当前session的id存入当前线程
            log.info("用户已登入,用户id为{}", employee);
            filterChain.doFilter(request, response);
            return;
        }

 5.2新增分类

请求方式

请求体

controller

@PostMapping
    public R save(@RequestBody Category category){
        log.info("category:{}",category);
        categoryService.save(category);
        return R.success("新增分类成功");
    }

5.3菜品类的分页

请求方式

controller

    /**
     * @Description:
     * 菜品分类的分页功能
     * @param: page
     * @param: pageSize
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/11 20:52
     */

    @GetMapping("page")
    public R sapage(int page, int pageSize){
     return    categoryService.sapage(page,pageSize);
    }

service实现类

    @Override
    public R sapage(int page, int pageSize) {
        Page<Category> page1 = new Page<>(page, pageSize);
        LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByAsc(Category::getSort);  //根据sort字段升序排序
        wrapper.orderByDesc(Category::getUpdateTime);//如果sort有一样的那就根据最后修改时间排序
        categoryMapper.selectPage(page1,wrapper);
        return R.success(page1);

    }

5.4删除分类(重点)

首先需要判断当前分类中有没有菜品  (套餐需要判断,分类也需要判断,看他们各自下面有没有菜品与套餐明细)

如果有那就无法删除

没有那就可以删除

请求方式

我们需要根据客户端传过来的分类id中查询当前分类有没有菜品

这个时候需要用到多表查询 

contoller

   /**
     * @Description:
     *根据id删除分类 ,如果要删除的分类他的里面有菜品那就不能删除,需要多表关联
     * @param: id
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/9 23:28
     */

    @DeleteMapping
    public R delete(Long ids){
        log.info("删除ids为:{}",ids);
       // categoryService.removeById( ids);
        categoryService.remove(ids);
        return R.success("当前分类删除成功");
    }

service实现类 

   @Override
    public void remove(long ids) {
        //1.查询当前分类是否关联了菜品,如果关联了,则抛出业务异常
        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();//dish是菜品实体类
        wrapper.eq(Dish::getCategoryId,ids);
        long count = dishservice.count(wrapper);
        if(count>0){     //说明菜品表里有数据
            //关联了菜品,抛出业务异常
            throw new CustException("当前分类关联了菜品,不能删除");
        }
        //2.查询当前分类是否关联了套餐,如果关联了,则抛出业务异常
        LambdaQueryWrapper<Setmeal> wrapper1 = new LambdaQueryWrapper<>(); //Setmeal是套餐实体类
        wrapper1.eq(Setmeal::getCategoryId,ids);
        long count1 = setmealService.count(wrapper1);
        if(count1>0){
            //关联了套餐,抛出业务异常
            throw new CustException("当前分类关联了菜品,不能删除");
        }
        //3.正常删除
        categoryMapper.deleteById(ids);
    }

 这里删除失败是抛出一个业务异常,所以我们需要自定义这么一个异常,再在全局异常处理上处理它

自定义异常

/**
 * @Description:
 *自定义业务异常,在service层抛出
 * @param: null
 * @return:
 * @Author: 刘某人
 * @Date 2024/8/9 23:50
 */

public class CustException extends RuntimeException{

    public CustException(String message){
        super(message);
    }
}

添加异常到全程异常处理

   @ExceptionHandler(CustException.class)
    public R exceptionHandler(CustException e) {
        log.info("CustException: ", e.getMessage());
        return R.error(e.getMessage());
    }

5.5修改分类

请求方式

请求体

controller

    /**
     * @Description:
     *分类的修改功能
     * @param: category
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 0:06
     */

    @PutMapping
    public R update(@RequestBody Category category){
        log.info("category:{}",category);
        categoryService.updateById(category);
        return R.success("修改分类成功");
    }

6.菜品管理业务功能

6.1文件的上传与下载(重点)

这两个功能后续会多次用到

文件上传

请求方式

请求体

在application中设置服务端存储文件的位置

#临时文件路径  项目启动后,有的功能要上传文件,这时文件会保存在临时文件中
reggie:
  path: F:/Java图片/

controller

MultipartFile file  这里的file必须和请求体中的保持一致

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

    /**
     * @Description:
     * 通用的文件上传下载
     * @param: null
     * @return:
     * @Author: 刘某人
     * @Date 2024/8/10 17:14
     */


    /**
     * @Description: 文件上传
     * @param: file 用来接收客户来传来的文件,这里的参数名字和客户端的保持一致不然接收不到
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 17:17
     */

    @RequestMapping("/upload")
    public R upload(MultipartFile file) {
        //创建一个目录,将临时文件都放进去
        File file1 = new File(besepath);
        boolean directory = file1.isDirectory();//判断目录是否存在
        if (directory == false) {  //不存在
            file1.mkdirs();  //创建目录
        }
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());
        String filename = file.getOriginalFilename();//原始文件名称  ,,原始文件名称虽然好记,但是存在文件名重复的问题,所以使用UUID来重命名文件
//这里使用UUID来重命名文件,避免文件名称重复导致文件覆盖
        String uuidstring = UUID.randomUUID().toString(); //生成的uuid
        String substring = filename.substring(filename.lastIndexOf("."));//获取文件后缀名
        uuidstring = uuidstring + substring;  //拼接完成文件名称
        try {
            //将临时文件转存到指定位置
            file.transferTo(new File(besepath + uuidstring));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(uuidstring); //返回文件名称
    }

文件下载(响应给客户端,让客户端可以显示图片)

因为要响应给客户端,所以这里形参设置的是Httpservleresponse

用它的输出流来响应给客户端 

   
    @Value("${reggie.path}")
    private String besepath;
 /**
     * @Description: 文件下载,响应给客户端
     * @param: name 文件名称
     * @param: response  输入流需要response来获取
     * @return: void
     * @Author: 刘某人
     * @Date 2024/8/10 18:09
     */
    @GetMapping("/download")
    public void doload(String name, HttpServletResponse response) {
        try {
            //输入流读取文件内容
            FileInputStream inputStream = new FileInputStream(besepath + name);
            //因为要响应给客户端,所以这里用response来获取输出流,在游览器上显示
            ServletOutputStream outputStream = response.getOutputStream();
            response.setContentType("image/jpeg");//设置响应文件类型
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
                outputStream.flush(); //刷新一下
            }
            inputStream.close();//关闭资源
            outputStream.close();//关闭资源
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

6.2新增菜品

这是新增菜品的页面  

这个新增菜品功能 需要给我们发4次请求

第一个请求是:菜品分类

第二个请求是:文件上传;

第三个请求是:文件下载

第四个请求是:提交添加

刚进入页面的时候客户端就给我发了一次请求,也就是上面这个图片的菜品分类

请求方式

controller

    /**
     * @Description:
     *  菜品管理中添加菜品中的分类下拉框,根据type查询分类
     * @param: type
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 18:57
     */

    @GetMapping("/list")
    public R list(Category category){
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(category!=null,Category::getType,category.getType());
        queryWrapper.orderByAsc(Category::getSort);  //根据sort升序排序
     //   queryWrapper.orderByDesc(Category::getUpdateTime); //根据更新时间降序排序
        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }

请求方式

请求体

这里我们可以发现,我们的菜品表(dish),它并没有这个flavors字段(口味)

口味字段在菜品口味表中(dish_flavor)中

所以我们这里需要创建一个实体类,实体类中需要有菜品表,还需要有多个口味表

用这个实体类来接收客户端发送过来的数据

实体类(dishdto)

这个实体类继承了菜品实体类,也就拥有了它的属性

而且还有一个 集合用来装口味  (毕竟一个菜,可以多有个口味(比如爱吃辣,不要香菜等)可以理解为1张菜品表对应多个菜品口味表 1对多的关系)

/**
 * @Description:
 * 数据传输对象
 * 这里继承了菜品,所以可以传输菜品的数据
 * 这里还用集合包裹了口味,所以可以传输口味的数据
 * 所以这个类可以接受前端提交的菜品和口味的数据
 * @param: null
 * @return:
 * @Author: 刘某人
 * @Date 2024/8/10 21:38
 */

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();  //口味,多个,一个菜多个口味

    private String categoryName;     //菜品分类名称

    private Integer copies;
}

 controller

    /**
     * @Description:
     * 菜品管理中的新建菜品
     * 因为新建菜品中有一个口未选择,所以这里需要多表
     * @param: dto
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 21:40
     */

    @PostMapping()
    public R addDish(@RequestBody DishDto dto){
        log.info("dto{}",dto);
        dishService.addDish(dto);
        return R.success("新增菜品成功");
    }

service实现类

菜品表的信息都全,所以可以直接插入

根据这里,我们发现,客户端并没有给我们的菜品口味表传递它对应的菜品id过来,

所以我们这里要给我们每一个菜品口味表添加菜品id

    @Transactional  //事务防止一个表插入成功一个插入失败,这样子会导致数据不一致
    @Override
    public void addDish(DishDto dto) {
        //保存菜品的基本信息到dish表
        dishMapper.insert(dto);
        //菜品口味保存DishFlavor
        Long dishId = dto.getId(); //获取菜品id
        List<DishFlavor> flavors = dto.getFlavors(); //获取菜品口味信息
        for (DishFlavor flavor : flavors) {
            flavor.setDishId(dishId); //设置菜品id
        }
        dishFlavorService.saveBatch(flavors);
    }

6.3菜品信息分页(难点)

分析

这个功能 需要发送二个请求

请求一:分页

多个请求二:文件下载 (因为数据都展示出来了,图片资源也需要展示)

展示的是所有菜品,但菜品表中并没有菜品分类,所以这里需要多表查询

请求方式

图片左上角还有一个查询,那个请求方式和分页一样 

因为这里还有一个菜品分类需要给客户端响应过去

所以这里我们响应过去的实体类还是dishdto这个实体类(实体类代码在6.2)

controller

  @Autowired
    private DishService dishService;

    @Autowired
    private CategoryService category;
    @GetMapping("/page")
    public R getDishByCategoryId(int page,int pageSize ,String name){
        Page<Dish> page1 = new Page<>(page, pageSize);
        Page<DishDto> disdtoPage = new Page<>();

        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(name!=null,Dish::getName,name); //模糊
         wrapper.orderByDesc(Dish::getUpdateTime);  //排序
        dishService.page(page1,wrapper);

        //对象拷贝
        BeanUtils.copyProperties(page1,disdtoPage,"records");//除了records的,其他都拷贝
           List<Dish> records = page1.getRecords();  //dish表所有数据
           List<DishDto> list =new ArrayList<>();

        //多表查询
        for (Dish dish:records) {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(dish,dishDto);  //将菜品保存到dishdto中
            Long categoryId = dish.getCategoryId();   //获取菜品对应的菜品分类的id
            Category category1 = category.getById(categoryId);  //根据菜品分类id查询到菜品分类
            if(category1!=null){
                dishDto.setCategoryName(category1.getName()); //将菜品分类名称保存起来
            }
            list.add(dishDto);  //添加到 list中 
        }
        disdtoPage.setRecords(list); 
        return R.success(disdtoPage); //把dishdtopage分页响应回去
    }

6.4修改菜品(难点)

分析:我们刚点进来修改菜品,这里就展示了未修改前的所有数据

也就是说:我们还没修改,客户端就已经给我们发送了3次请求

第一次请求:菜品分类

第二次请求:图片下载

第三次请求:显示要未修改前的信息

在加上最后的修改,这里一共用到了4次请求

第三次请求

请求方式

controller

    /**
     * @Description:
     *菜品管理,修改菜单中的展示内容,需要用到多表查询
     * @param: id
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 23:39
     */

    @GetMapping("/{id}")
    public R getDishById(@PathVariable Long id){
        DishDto dto = dishService.getDishByid(id);
        return R.success(dto);
    }

service实现类

    /**
     * @Description:根据id查询对应信息,以及他的口味信息,多表查询
     * @param: id
     * @return: org.example.dto.DishDto
     * @Author: 刘某人
     * @Date 2024/8/10 23:33
     */

    @Override
    public DishDto getDishByid(Long id) {

        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);  //这里的this ,可以理解为DishService 

        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

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

        return dishDto;

    }

提交修改

请求方式

请求体

controller

   @PutMapping()
    public R updateDish(@RequestBody DishDto dto){
        dishService.updateDish(dto);
        return R.success("修改成功");
    }

 service实现类

梳理一下逻辑

首先,菜品表可以直接修改

菜品口味表:修改前

修改后

这里发现和之前的完全不一样,所有这里采用的策略是

先把菜品对应的菜品口味全部删除

再把从客户端获取的菜品口味插入

这样也是实现了修改的功能

    @Override
    public void updateDish(DishDto dto) {
        //更新dish表
        this.updateById(dto);  //这里的this就是DishService
        //清理当前菜品对应口味数据
        LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(DishFlavor::getDishId, dto.getId());//根据菜品id查询口味信息
        dishFlavorMapper.delete(wrapper);//删除口味信息
        //添加提交过来的口味数据
        List<DishFlavor> flavors = dto.getFlavors();
        for (DishFlavor flavor : flavors) {
            flavor.setDishId(dto.getId()); //设置菜品id
        }
        dishFlavorService.saveBatch(flavors);//保存口味信息


    }

6.5批量删除功能

批量删除和删除的请求方式是一样的,所以单个的就不需要写

请求方式

controller

    /**
     * @Description:
     * 菜品管理的批量删除功能 ,删除菜品并且删除它在服务端保存的图片
     * @param: ids
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 19:24
     */

    @DeleteMapping
    public R delete( @RequestParam List<Long> ids){
        dishService.delete(ids);
        return R.success("删除成功");
    }

这里我的批量删除是把数据库的信息删除,把对应的图片从磁盘中删除

service实现类


    @Value("${reggie.path}")
    private String sfile;
  @Override
    public void delete(List<Long> ids) {
        System.out.println(ids + "-------------------------------------------------------------");
        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Dish::getId, ids);
        List<Dish> listfile = dishMapper.selectList(wrapper);   //根据id查询到所有菜品
        File file = new File(sfile);  //这里的sfile就是我们之前定义的服务端存放图片的文件夹
        for (Dish dish : listfile
        ) {
            File file1 = new File(file + "/" + dish.getImage());  //从菜品属性中获取的图片的名称
            boolean delete = file1.delete();            //删除图片
            if (true) {
                System.out.println(file1.getPath() + "------------");
                System.out.println(dish.getImage() + "删除成功");
            } else {
                System.out.println("删除失败");
            }
        }
        this.removeByIds(ids);  //this相当于DishService   ,删除菜品信息
    }

 6.6批量停售启售

批量停售启售和停售启售的请求方式是一样的,所以单个的就不需要写

请求方式

 controller

    /**
     * @Description:
     * 批量修改菜品状态
     * @param: status
     * @param: ids
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/10 21:34
     */
    @PostMapping("status/{status}")
    public R updateStatus(@PathVariable int status,@RequestParam List<Long> ids){
        dishService.updateStatus(status,ids);
        return R.success("修改成功");
    }

service实现类

    @Override
    public void updateStatus(int status, List<Long> ids) {
        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Dish::getId, ids);        //根据id查询   
        // List<Dish> list = dishService.list(wrapper);
        List<Dish> list = dishMapper.selectList(wrapper);  //查找到所有要修改的菜品
        for (Dish dish : list
        ) {
            dish.setStatus(status);   //给每一个菜品设置它的状态
        }
        //   dishService.updateBatchById(list);
        this.updateBatchById(list);   //提交修改
    }

7.套餐管理业务功能

7.1添加套餐

这里一共有5个请求:

请求一:套餐分类(和之前的餐品分类是同一个请求,这里不用写)

请求二:添加菜品  

请求三:展示添加的菜品  (和请求二一样 ,请求二将菜品返回,请求三显示)

请求四:上传图片 (已有)

请求五:下载图片(已有)

请求六:整体提交保存

请求方式  (这里根据菜品分类来查询分类下所有在线的菜品)

controller

    /**
     * @Description:
     * 套餐管理中添加套餐 中有一个添加菜品的下拉框
     * @param: categoryId
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/12 11:47
     */

    @GetMapping("list")
    public R listDish( Dish dish){
       return dishService.listDish(dish);
    }

service实现类

    @Override
    public R listDish(Dish dish) {
        LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Dish::getCategoryId, dish.getCategoryId());
        wrapper.eq(Dish::getStatus, 1);  //查询状态为1的菜品
        List<Dish> list = this.list(wrapper);
        return R.success(list);
    }

请求方式

请求体

这里我们的套餐表并没有setmealDishes这个字段

所以这里我们建立一个SetmealDto实体类用来接收

 SetmealDto

@Data
public class SetmealDto extends Setmeal {
    private List<SetmealDish> setmealDishes; // 套餐菜品列表
    private String categoryName; // 套餐分类名称
}

controller

    /**
     * @Description:
     *添加套餐功能
     * @param: setmealDto
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/12 16:27
     */


    @PostMapping
    public R save(@RequestBody SetmealDto setmealDto){
    R r=    setmealService.saveinsert(setmealDto);
        return  r;
    }

service实现类 

1.首先,套餐是可以直接插入的

因为从请求体种可以看出 它并没给套餐的菜品表 对应的 套餐id

所以这里要给每一个套餐的菜品设置它的id,然后在插入

  @Transactional
    @Override
    public R saveinsert(SetmealDto setmealDto) {
        setmealMapper.insert(setmealDto);
        List<SetmealDish> dishes = setmealDto.getSetmealDishes();
        for (SetmealDish dish : dishes
        ) {
            dish.setSetmealId(setmealDto.getId().toString());

        }
        service.saveBatch(dishes);
        return R.success("添加成功");
    }

7.2套餐的分页

请求方式

controller

    /**
     * @Description:
     * 套餐管理模块分类页面
     * @param: page
     * @param: pageSize
     * @param: name
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/12 15:23
     */


    @GetMapping("page")
    public R setmealpage(int page,int pageSize,String name){
        return setmealService.setmealpage(page,pageSize,name);
    }

service实现类

 @Override
    public R setmealpage(int page, int pageSize, String name) {
        Page<SetmealDto> setmealDtoPagepage = new Page<>();
        Page<Setmeal> page1 = new Page<>();
        LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(name != null, Setmeal::getName, name);
        wrapper.orderByDesc(Setmeal::getUpdateTime);
        setmealMapper.selectPage(page1, wrapper);
        BeanUtils.copyProperties(page1, setmealDtoPagepage, "records");
        ArrayList<SetmealDto> list = new ArrayList<>();
        for (Setmeal record : page1.getRecords()) {
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(record, setmealDto);
            Long categoryId = record.getCategoryId();
            Category byId = mapper.selectById(categoryId); //CategoryMapper
            if (byId != null) {
                setmealDto.setCategoryName(byId.getName());
            }
            list.add(setmealDto);
        }
        setmealDtoPagepage.setRecords(list);
        return R.success(setmealDtoPagepage);
    }

7.3删除套餐

这个功能同样可以批量删除

请求方式

controller

    /**
     * @Description:
     * 批量删除套餐,也可以单独删除
     * @param: ids
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/12 15:46
     */

    @DeleteMapping
    public R delete(@RequestParam List<Long> ids){
        return setmealService.delete(ids);
    }

service实现类

 @Transactional
    @Override
    public R delete(List<Long> ids) {
        List<Setmeal> setmeals = this.listByIds(ids);

        LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Setmeal::getId, ids);
        wrapper.eq(Setmeal::getStatus, 1);
        int count = (int)this.count(wrapper);  //查询状态为1的套餐数量
        if(count>0){
            throw new CustException("套餐正在售卖中,不能删除");
        }

       this.removeByIds(ids);

        //图片删除
        for (Setmeal setmeal : setmeals) {
            boolean b = deleteImage(setmeal.getImage());
            if(b){
                System.out.println("删除成功");
            }
        }
        //删除套餐中菜品
        LambdaQueryWrapper<SetmealDish> wrapper1 = new LambdaQueryWrapper<>();
        wrapper1.in(SetmealDish::getSetmealId, ids);
        service.remove(wrapper1); //删除套餐中菜品





        return R.success("删除成功");
    }

这里掉用了一个删除图片的方法


    //删除服务器中的图片
    public boolean deleteImage(String name) {
        File file = new File(sfile);
        File file1 = new File(file + "/" + name);  //从菜品属性中获取的图片的名称
        boolean delete = file1.delete();            //删除图片
        if (true) {
            System.out.println(file1.getPath() + "------------");
            System.out.println(name + "删除成功");
            return true;
        } else {
            System.out.println("删除失败");
            return false;
        }

    }

7.4修改套餐

请求了3次

一次请求时显示套餐分类(已有)

一次请求时下载图片(已有哦)

一次请求时显示套餐内容

请求方式

 

controller

    /**
     * @Description:
     * 套餐管理模块的套餐修改,进入套餐修改显示原始数据
     * @param: id
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/12 16:51
     */

    @GetMapping("{id}")
    public R list( @PathVariable Long id){
        return setmealService.listup(id);
    }

service

    @Override
    public R listup(Long id) {
        SetmealDto dto = new SetmealDto();
        Setmeal byId = this.getById(id);
        BeanUtils.copyProperties(byId, dto);
        LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SetmealDish::getSetmealId, id); //根据套餐id查询菜品
        List<SetmealDish> list = service.list(wrapper);  //这里的service是  SetmealDishService
        dto.setSetmealDishes(list);
        return R.success(dto);

    }

整体提交修改

请求方式

请求体

controller

    @PutMapping
    public R update(@RequestBody SetmealDto setmealDto){
        return setmealService.updatetc(setmealDto);
    }

service实现类

    @Override
    public R updatetc(SetmealDto setmealDto) {
        setmealMapper.updateById(setmealDto);  //修改套餐
        //删除原始套餐中的菜品
        LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SetmealDish::getSetmealId, setmealDto.getId()); //根据套餐id查询菜品
        service.remove(wrapper); //删除套餐中菜品
        List<SetmealDish> dishes = setmealDto.getSetmealDishes();  //获取套餐中的菜品
        for (SetmealDish dish : dishes
        ) {
            dish.setSetmealId(setmealDto.getId().toString());
        }
        service.saveBatch(dishes); //修改套餐重点菜品
        return R.success("修改成功");
    }

7.5批量修改状态

这里可批量,也可以单个

请求方式

请求体

controller

   @PostMapping("status/{status}")
    public R status(@PathVariable Integer status,@RequestParam List<Long> ids){
                return setmealService.status(status,ids);
    }

service

    @Override
    public R status(Integer status, List<Long> ids) {

        LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Setmeal::getId, ids);
        List<Setmeal> list = this.list(wrapper);
        for(Setmeal s:list){
            s.setStatus(status);
        }

        this.updateBatchById(list);
        return R.success("修改成功");
    }

8订单管理业务开发

8.1 订单的分页

请求方式

controller

    //后台订单查询
    @GetMapping("page")
    public R page(Integer page, Integer pageSize, String number, String beginTime, String endTime) {
        Page<Orders> page1 = new Page<>(page, pageSize);
        LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(number != null, Orders::getNumber, number); //订单号
        wrapper.between(beginTime != null && endTime != null, Orders::getOrderTime, beginTime, endTime); //时间
        ordersService.page(page1, wrapper); //分页
        return R.success(page1);
    }

8.2后台订单状态更改 

请求方式

请求体

controller

  @PutMapping
    //后台订单更改状态
    public R status(@RequestBody Orders orders) {
        ordersService.updateById(orders);
        return R.success("修改成功");
    }

9.移动端开发

这里我用的时java的验证码功能不是阿里云的手机验证码

阿里云的得花钱

这里不展示阿里云得那种方式登入

9.1登入模块

获取随机验证码类

package org.example.util;

import java.util.Random;

/**
 * 随机生成验证码工具类
 */
public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

手机获取得验证码请求

请求体

移动端登入请求

请求体

因为我们在前面写了过滤器,这里又是移动端的登入,我们并没有在后台登录,而过滤器是过滤没有后台登录得请求,所以我们需要把这两个请求放在过滤器的不需要处理请求字符串数组中,

代码如下

 String[] urls = new String[]{    //里面定义的是不需要处理的请求    "/backend/**", "/front/**" 这两个是静态资源,不需要过滤,只处理请求,//"/common/**"是文件的上传下载
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",
                "/user/login"

        }; 

然后就是移动端登入成功后,也是对一些业务的处理,这些业务处理都是需要在登入的情况下才可以处理,所以我们在登入成功的时候还得把id放到session中来判断,接着我们还需要在过滤器中设置如果移动端登入了就放行移动端的所有请求

具体代码如下  (这里的代码只是对之前的增加,之前过滤器的代码不要动)

放入session的key为user

     //4.2 移动端的用户是否登入
        if (request.getSession().getAttribute("user") != null) {       //非空则放行  ,判断
            Long userid = (Long)request.getSession().getAttribute("user");  //获取移动端登入成功的id
                BaseContext.setCurrentId(userid); //将当前session的id存入当前线程
            log.info("用户已登入,用户id为{}", userid);
            filterChain.doFilter(request, response);  //放行
            return;
        }

 获取验证码的业务处理

controller

    /**
     * @Description:
     * 验证码
     * @param: user  这里不可以是String类型,String类型的话,session报存不了
     * @param: request
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/13 21:29
     */

    @PostMapping("sendMsg")
    public R sendMsg(@RequestBody User user, HttpServletRequest request){
        String phone = user.getPhone();
        if (phone!=null){
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("验证码为{}",code);   //到时候从后台来看验证码,不然没办法登入
           request.getSession().setAttribute(phone,code);  //1个手机号对应1个验证码 ,放到session中,后面登入来判断它
            return R.success("手机验证码短信发送成功");
        }
        return R.error("短信发送失败");
    }

登录业务处理

controller

 @PostMapping("/login")
    public R<User> login(@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);
            }
            session.setAttribute("user",user.getId());
            return R.success(user);
        }
        return R.error("登录失败");
    }

退出登入业务处理

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

9.2地址模块

根据登入者id查询地址

请求方式

    /**
     * @Description: 显示当前id得所有地址
     * @param:
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/13 21:58
     */
    @GetMapping("list")
    public R list() {
        Long id = BaseContext.getCurrentId();
        LambdaQueryWrapper<AddressBook> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(AddressBook::getUserId, id); //根据用户id查询地址
        wrapper.orderByDesc(AddressBook::getUpdateTime); //根据更新时间排序
        return R.success(addressBookService.list(wrapper));
    }

新增地址

controller

请求方式 POST

    /**
     * @Description: 新建地址
     * @param: addressBook
     * @return: org.example.common.R<org.example.pojo.AddressBook>
     * @Author: 刘某人
     * @Date 2024/8/13 21:58
     */

    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        Long id = BaseContext.getCurrentId();
        addressBook.setUserId(id);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

设置默认地址

一个id只能有一个默认地址

请求方式

请求体

controller

    /**
     * @Description:
     * 设置默认地址
     * 第一步 将所有地址的isDefault设置为0
     * 第二步 将当前地址的isDefault设置为1
     * @param: addressBook
     * @return: org.example.common.R<org.example.pojo.AddressBook>
     * @Author: 刘某人
     * @Date 2024/8/13 22:05
     */

    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
        Long id = BaseContext.getCurrentId();
        LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<AddressBook>();
        wrapper.eq(AddressBook::getUserId, id); //根据用户id查询地址
        wrapper.set(AddressBook::getIsDefault, 0); //将所有地址的isDefault设置为0
        addressBookService.update(wrapper);
        addressBook.setIsDefault(1); //将当前地址的isDefault设置为1
        addressBookService.updateById(addressBook); //更新当前地址
        return R.success(addressBook);
    }

修改地址

第一步:点击进行显示原始地址

请求方式

    /**
     * @Description: 修改地址中显示初始地址
     * @param: id
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/13 22:04
     */

    @GetMapping("/{id}")
    public R get(@PathVariable Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        if (addressBook != null) {
            return R.success(addressBook);
        } else
            return R.error("没有找到该地址");
    }

第二部 提交修改

请求方式PUT

controller

 @PutMapping
    public R update(@RequestBody AddressBook addressBook){
        addressBook.setUserId(BaseContext.getCurrentId());
         addressBookService.updateById(addressBook);
         return R.success("修改成功");
    }

9.3购物车模块

展示

这里有三个请求

这个请求式获取套餐和菜品的类型(之前写过)

这里修改一下那个代码

    @GetMapping("/list")
    public R list(Category category){
        if(category.getType()==null){
             return    R.success( categoryService.list());
        }else {
            LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(category!=null,Category::getType,category.getType());
            queryWrapper.orderByAsc(Category::getSort);  //根据sort升序排序
            //   queryWrapper.orderByDesc(Category::getUpdateTime); //根据更新时间降序排序
            List<Category> list = categoryService.list(queryWrapper);
            return R.success(list);
        }
    }

第二个请求 (用户购物车里有东西的时候,这里就展示出来了)

    /**
     * @Description:
     * 查询用户账户下购物车的商品有哪些
     * @param:
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 18:25
     */

    @GetMapping("list")
    public R list() {
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
        wrapper.orderByAsc(ShoppingCart::getCreateTime);
        return R.success(shoppingCartService.list(wrapper));
    }

第三个请求(之前写过,这里不展示)

添加购物车

请求方式

请求体

controller

    /**
     * @Description: 给当前用户购物车内添加商品
     * @param: shoppingCart
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 16:53
     */
    @PostMapping("add")
    public R add(@RequestBody ShoppingCart shoppingCart) {
        shoppingCart.setUserId(BaseContext.getCurrentId());  //设置为当前主键id
        shoppingCart.setCreateTime(new Date());   //设置当前时间
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId, shoppingCart.getUserId());  //查询购物车中有没有对应的当前登入者的id

        if (shoppingCart.getDishId() != null) {           //添加得是菜品
            wrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());  //根据菜品id和当前登入者的id,来判断购物车李有没有这个菜品
        } else {            //添加得是套餐
            wrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());  //和上面同理
        }

        ShoppingCart one = shoppingCartService.getOne(wrapper);  //查询购物车中有没有这个菜品或套餐
        System.out.println(one + "----------------------------------------------");
        if (one != null) {  //购物车中有这个菜品或套餐,数量加一
            one.setNumber(one.getNumber() + 1);
            shoppingCartService.updateById(one);  //相同的菜品或套餐。。如果插入得是一样得那就在数量上+1
            return R.success(one);
        } else {       //购物车中没有
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);  //插入购物车
            return R.success(shoppingCart);
        }

    }

移出购物车

请求方式

请求体

controller

    /**
     * @Description:
     *  购物车删除
     *  这里购物车商品为1的时候需要先修改为0再删除, 不然页面商品数量还是显示的1,切记
     * @param: shoppingCart
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 18:23
     */

    @PostMapping("sub")
    public R sub(@RequestBody ShoppingCart shoppingCart) {
        shoppingCart.setUserId(BaseContext.getCurrentId()); //设置为当前登入用户的id
        System.out.println(BaseContext.getCurrentId() + "-------------------------------------");
        //根据用户id和菜品id来查询
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId, shoppingCart.getUserId());
        if (shoppingCart.getDishId() != null) {            //看要删除的是菜品还是套餐
            wrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
        } else {
            wrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
        }

        ShoppingCart one = shoppingCartService.getOne(wrapper);  //查询购物车中有没有这个菜品或套餐
        Integer i = one.getNumber();

        if (one != null) {

           if(i > 0) {
                one.setNumber(i - 1);
                shoppingCartService.updateById(one);  //数量不为1时,数量-1
            }
            i = one.getNumber();
            if (i == 0 || i == null) {
                shoppingCartService.removeById(one.getId());
            }
            return R.success(one);    //这里前端要显示数量,,数量为0,直接删除的话数量还是1,这里需要先修改为0再删除
        } else {
            return R.error("购物车中没有这个菜品或套餐,请刷新网页");
        }

    }

清空购物车

请求方式

controller

    /**
     * @Description: 清楚当前登入用户购物车内所有商品
     * @param:
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 16:51
     */

    @DeleteMapping("clean")
    public R clean() {
        shoppingCartService.remove(new LambdaQueryWrapper<ShoppingCart>().eq(ShoppingCart::getUserId, BaseContext.getCurrentId()));
        return R.success("清空购物车成功");
    }

9.4订单模块

提交订单

请求方式 POST

请求体

controller

    /**
     * @Description: 用户下单
     * @param: orders
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 20:19
     */

    @PostMapping("submit")
    public R submint(@RequestBody Orders orders, HttpSession session) {

        R r = ordersService.submit(orders, session);
        return r;
    }

service实现

思路:一个用户一次只能下一单,一单产生一个订单,一单中有很多菜,一道菜对应一个订单明细

所以提交订单需要分别对订单表和订单明细表插入

 @Transactional
    @Override
    public R submit(Orders orders, HttpSession session) {
        //获得当前用户id
        Long object = (Long) session.getAttribute("user");
        //查询当前用户购物车数据
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId, object);
        List<ShoppingCart> shoppingCarts = shoppingmapper.selectList(wrapper);
        if (shoppingCarts == null || shoppingCarts.size() < 0) {
            throw new CustException("没有这个订单");
        }
        //查询用户
        User user = userMapper.selectById(object);
        //查询地址
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = AddressBookMapper.selectById(addressBookId);
        if (addressBook == null) {
            throw new CustException("地址信息有误");
        }
        //放到订单表 1条
        long id = IdWorker.getId();//订单号生成
        orders.setId(id);
        orders.setNumber(String.valueOf(id));
        orders.setStatus(2);
        orders.setUserId(object);
        orders.setOrderTime(new Date());
        orders.setCheckoutTime(new Date());
        orders.setPayMethod(1);
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() + " " + addressBook.getCityName() +
                " " + addressBook.getDistrictName() + " " + addressBook.getDetail()));  //省市区加起来
        //计算金额
        long amount = 0;
        for (ShoppingCart shoppingCart : shoppingCarts
        ) {
            Integer number = shoppingCart.getNumber();
            BigDecimal amount1 = shoppingCart.getAmount();
            amount += number * amount1.longValue();
        }
        orders.setAmount(BigDecimal.valueOf(amount));
        //插入到订单表中
        ordersMapper.insert(orders);
        //插入到明细表中  多条

        for (ShoppingCart shoppingCart : shoppingCarts) {
            OrderDetail ordersDetail = new OrderDetail();
            ordersDetail.setOrderId(id);
            ordersDetail.setNumber(shoppingCart.getNumber());
            ordersDetail.setDishFlavor(shoppingCart.getDishFlavor());
            ordersDetail.setDishId(shoppingCart.getDishId());
            ordersDetail.setName(shoppingCart.getName());
            ordersDetail.setSetmealId(shoppingCart.getSetmealId());
            ordersDetail.setImage(shoppingCart.getImage());
            ordersDetail.setAmount(shoppingCart.getAmount());
            orderDetailMapper.insert(ordersDetail);
        }

        //清空购物车
        shoppingmapper.delete(wrapper);
        return R.success("ok");

    }

查看订单

效果

这里展示了订单表和订单明细

所有需要用orderdto来封装一下,用于返回

实体orderdto类

@Data
public class OrderDto extends Orders {
    private List<OrderDetail> orderDetails;  //订单明细
}

请求方式

controller

    //移动端订单查询
    @GetMapping("userPage")
    public R userPage(Integer page, Integer pageSize) {
        Page<Orders> page1 = new Page<>(page, pageSize);
        LambdaQueryWrapper<Orders> wrapper1 = new LambdaQueryWrapper<>();
        wrapper1.orderByDesc(Orders::getOrderTime); //时间
        ordersService.page(page1,wrapper1); //分页
        Page<OrderDto> dtoPage = new Page<>();  //dto分页
        BeanUtils.copyProperties(page1, dtoPage, "records"); //拷贝
        List<Orders> orders = page1.getRecords();
        List<OrderDto> orderDtos = new ArrayList<>(); //订单详情
        for (int i = 0; i < orders.size(); i++) {
            OrderDto orderDto = new OrderDto();
            BeanUtils.copyProperties(orders.get(i), orderDto);
            Long orderId = orders.get(i).getId();
            LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(OrderDetail::getOrderId, orderId); //订单id

            List<OrderDetail> list = orderDetailService.list(wrapper); //订单详情
            orderDto.setOrderDetails(list); //将订单详情放入dto中
            orderDtos.add(orderDto); //将dto放入list中
        }
        dtoPage.setRecords(orderDtos); //将dto放入分页中
        return R.success(dtoPage); //返回dto分页
    }

再来一单功能

请求方式POST

请求体oders接受

controller

首先需要获取当前账户的订单明细内容

因为每次下单成功会那么下单的菜品会保存到订单明细表中,再来一单再来的式上一单的菜品,所以需要从订单明细表中获取上一个订单购买的菜品,并将这些菜品放到购物车当中(因为我们已经下单成功了(下单成功会清空购物车),所以这里需要放入购物车),

这样我们点再来一单的时候购物车会直接显示我们上一单的菜品

    /**
     * @Description:
     * 再来一单功能
     * @param: orders
     * @return: org.example.common.R
     * @Author: 刘某人
     * @Date 2024/8/14 22:54
     */

    @PostMapping("again")
    public R again(@RequestBody Orders orders){
        Long id = orders.getId();
        Orders orders1 = ordersService.getById(id); //根据id查询订单
        LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OrderDetail::getOrderId, id); //订单id
        List<OrderDetail> list = orderDetailService.list(wrapper); //订单详情

        for (int i = 0; i < list.size(); i++) {
            ShoppingCart shoppingCart = new ShoppingCart();
            shoppingCart.setUserId(orders1.getUserId());
            shoppingCart.setImage(list.get(i).getImage());
            shoppingCart.setName(list.get(i).getName());
            shoppingCart.setDishId(list.get(i).getDishId());
            shoppingCart.setSetmealId(list.get(i).getSetmealId());
            shoppingCart.setNumber(list.get(i).getNumber());
            shoppingCart.setAmount(list.get(i).getAmount());
            shoppingCartService.save(shoppingCart); //将订单详情放入购物车
        }
        return R.success("重新下单成功");
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值