瑞吉外卖,入门到部署全过程

本内容来自码云开源内容,同时部分优化,有需要的可以去看看原本内容

README.md · Ndelible/瑞吉外卖 - 码云 - 开源中国 (gitee.com)

项目介绍 

本项目(瑞吉外卖)是一款专门为餐饮企业定制的软件产品,其中包括系统管理后台和移动端应用两部分。系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等操作。

 环境搭建
  • JDK 8.0 版本
  • Maven-3.3.9
  • MySql 8.0.23
  • Redis 4.0
  • Tomcat-7.0.57
 使用工具
  • Intellij IDEA 2020.1.3
  • vmware-pro15 虚拟机
  • CentOS-7 系统
  • FinalShell 终端
 技术选型

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

项目使用 

�使用说明
  1. 本项目自带前端代码,无需前端代码的编写。
  2. 本项目是通过虚拟机创建服务器进行项目部署,实现前后端分离管理。
  3. 使用前需要安装虚拟机及Linux系统搭建服务器。
  4. 项目运行前,需要更改相对应的端口号及主机地址,也可通过配置文件修改。
⚠ 注意事项
  1. 前后端分离,前提是需要两台服务器进行管理前后端。
  2. 缓存信息优化,则需要第三台服务器接收缓存信息。

项目开发 

开发环境搭建
  1. 创建项目对应的数据库。
  2. 导入表结构(资料/数据模型/db_reggie.sql)
  1. 创建maven工程
  1. 导入maven配置
  • 添加spring-boot-starter-parent
  • 指定java版本为1.8
  • 导入依赖
  • 添加spring-boot-maven-plugin插件

完整pom.xml文件如下:

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>
        <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>
</project>

  • 导入Spring Boot配置文件application.yml
  • 将资料中logback.xml放到resource目录下

server:
  port: 8080
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:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

  • 编写启动类

    @Slf4j
    @SpringBootApplication
    public class ReggieApplication {
        public static void main(String[] args){
            SpringApplication.run(ReggieApplication.class,args);
            log.info("项目启动成功!");
        }
    }

  • 导入前端资源
  • 创建配置类WebMvcConfig,设置静态资源映射,否则无法访问页面

  1. @Configuration
    public class WebMvcConfig extends WebMvcConfigurationSupport {

        /**
         * 设置静态资源映射,将静态资源映射到/backend和/front下,可以直接访问
         * @param registry
         */
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            //log.info("开始进行静态资源映射...");
            registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
            registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
        }
  • 项目整体框架
  • 创建自定义工具异常类CustomException​,用于抛出异常,以及常量。因为我是后面开始写的代码所以我这里就开始加入异常类

  •  * @author PangPAi
     * @date 2024/7/12
     *  自定义异常类
     *  常量类
     */

    public class CustomException extends RuntimeException {

        public CustomException(String message){
            super(message);
        }
        public static class AddressBook {
            public static final String ADDRESS_BOOK_ERROR = "没有找到该对象";
        }
        public static class Category {
            public static final String SUCCESS_CATEGORY_MESSAGE = "新增分类成功";
            public static final String DELETE_CATEGORY_MESSAGE = "分类信息删除成功";
            public static final String DELETE_CATEGORY_ID = "删除分类,id为:{}";
            public static final String UPDATE_CATEGORY_MESSAGE = "修改分类信息成功";
            public static final String UPDATE_CATEGORY = "修改分类信息:{}";
            public static final String FIND_CATEGORY = "查询所有分类信息";
            public static final String FIND_CATEGORY_LIST = "查询结果:{}";

        }
        public static class Dish {
            public static final String SUCCESS_DISH_MESSAGE = "新增菜品成功";
            public static final String UPDATE_DISH_MESSAGE = "修改菜品成功";
            public static final String DELETE_DISH_MESSAGE = "删除成功";
            public static final String status_success = "售卖状态修改成功";
            public static final String status_error = "设置失败!状态异常";
        }
        public static class Employee {
            public static final String LOGIN_ERROR = "登录失败";
            public static final String LOGIN_ERROR_USER_STATUS = "账号已禁用";
            public static final String LOGOUT_SUCCESS = "退出成功";
            public static final String SAVE_EMPLOYEE = "修改密码成功";
            public static final String SAVE_SUCCESS = "新增员工成功";
            public static final String UPDATE_SUCCESS = "员工信息修改成功";
            public static final String UPDATE_Thread_ID = "线程id为:{}";
            public static final String getById_ID = "根据id查询员工信息...";
            public static final String getById_ERROR = "没有查询到对应员工信息";

        }
        public static class Order {
            public static final String SUBMIT_ORDER = "订单数据:{}";
            public static final String SUBMIT_ORDER_SUCCESS = "下单成功";
            public static final String UPDATE_ORDER_STATUS = "派送成功";
            public static final String LOGOUT_SUCCESS = "派送失败";
        }
        public static class Setmeal {
            public static final String SAVE_SetMal_DTO = "套餐信息:{}";
            public static final String SAVE_SUCCESS = "新增套餐成功";
            public static final String DELETE_SUCCESS = "套餐数据删除成功";
            public static final String startOrStop_success = "起售停售";
        }
        public static class ShoppingCart {
            public static final String ADD_shoppingCart = "购物车数据:{}";
            public static final String LOOK_shoppingCart = "查看购物车...";
            public static final String CLEAR_shoppingCart = "清空购物车成功";
        }

        public static class Impl{
            public static final String Category_remove_dishService = "当前分类下关联了菜品,不能删除";
            public static final String Category_remove_setmealService = "当前分类下关联了套餐,不能删除";
            public static final String Dish_deleteByIds_status = "套餐正在售卖中,不能删除";
            public static final String Order_submit_shoppingCarts = "购物车为空,不能下单";
            public static final String Order_submit_addressBook = "用户地址信息有误,不能下单";
            public static final String Setmeal_removeWithDish_status = "套餐正在售卖中,不能删除";

        }
        public static final String Application = "项目启动成功!"; //这里是启动项的日志输出
    }

这里还要注意一点,自定义异常类需要手动引入,比如才EmployeeController​类中,我们可以使用

​import static com.pangpai.ruiji.common.CustomException.Employee.*;​

管理端

后台登录功能开发
  • 查看登录需求信息
  • 对数据模型(employee表)分析
  • 创建实体类Employee,并对employee表进行映射
  • 创建关于Employee的Controller、Service、Mapper文件
  • 导入返回结果类R
  •     @ApiModelProperty("编码")
        private Integer code; //编码:1成功,0和其它数字为失败

        @ApiModelProperty("错误信息")
        private String msg; //错误信息

        @ApiModelProperty("数据")
        private T data; //数据

        @ApiModelProperty("动态数据")
        //private Map map = new HashMap(); //动态数据
    #这里需要注意的是,我个人的问题,就是在部署的时候出现了问题,最后把这里改了就好了哦。具体原理可以去查查
        private Map<String, String> map = new HashMap<>();//动态数据
  • 处理思路如下:
后台退出功能开发
  • 清理Session中的用户信息
  • 返回结果
  • 方法一(推荐使用)

    @Override
    public R<String> logout(HttpSession session){
        //1、清除session中用户信息
        session.invalidate();
        //2.返回登录成功信息
        return R.success("登出成功了");
    }

  • 方法二

    @Override
    public R<String> logout(HttpSession session){
        //1、清除session中用户信息
        session.removeAttribute("employee");
        //2.返回登录成功信息
        return R.success("登出成功了");
    }

完善登录功能
  • 处理思路如下:
  • 代码实现步骤
    1. 创建自定义过滤器LoginCheckFilter实现Filter接口,类上加上@WebFilter注解
    2. 在启动类上加入注解@ServletComponentScan
    3. 完善过滤器的处理逻辑
  • 功能测试
新增员工
  • 对数据模型(employee表)分析
  • 代码实现步骤
    1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
    2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
    3. Service调用Mapper操作数据库,保存数据
  • 在EmployeeController中创建新增员工的方法
  •     /**
         * 新增员工
         * @param employee
         * @return
         */
        @PostMapping
        public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
            log.info(SAVE_EMPLOYEE,employee.toString());

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

            employee.setCreateTime(LocalDateTime.now());
            employee.setUpdateTime(LocalDateTime.now());
            //获得当前登录用户的id
            //Long empId = (Long) request.getSession().getAttribute("employee");
            //employee.setCreateUser(empId);
            //employee.setUpdateUser(empId);

            employeeService.save(employee);
            return R.success(SAVE_SUCCESS);
  • 全局异常类处理

  •  * 全局异常处理
     */
    @ControllerAdvice(annotations = {RestController.class, Controller.class})
    @ResponseBody
    @Slf4j
    public class GlobalExceptionHandler {

        /**
         * 异常处理方法
         * @return
         */
        @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
        public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
            log.error(ex.getMessage());

            if(ex.getMessage().contains("Duplicate entry")){
                String[] split = ex.getMessage().split(" ");
                String msg = split[2] + "已存在";
                return R.error(msg);
            }
            return R.error("未知错误");
        }

        /**
         * 异常处理方法
         * @return
         */
        @ExceptionHandler(CustomException.class)
        public R<String> exceptionHandler(CustomException ex){
            log.error(ex.getMessage());
            return R.error(ex.getMessage());
        }

    }
分页查询
  • 思路逻辑
    1. 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端。
    2. 服务端Controller接收页面提交的数据并调用Service查询数据。
    3. Service调用Mapper操作数据库,查询分页数据。
    4. Controller将查询到的分页数据响应给页面。
    5. 页面接收到分页数据并通过ElementUI的Table组件展示到页面上。
  • Controller 代码如下:
  •     /**
         * 员工信息分页查询
         * @param page
         * @param pageSize
         * @param name
         * @return
         */
        @GetMapping("/page")
        public R<Page> page(int page,int pageSize,String name){
            log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
            //构造分页构造器
            Page 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);
        }
启用/禁用员工账号
  • 逻辑思路
    1. 页面发送ajax请求,将参数(id、status)提交到服务端
    2. 服务端Controller接收页面提交的数据并调用Service更新数据
    3. Service调用Mapper操作数据库
  • Controller 代码如下:
  •     /**
         * 根据id修改员工状态
         * @param employee
         * @return
         */
        @PutMapping
        public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
            log.info(employee.toString());
            long id = Thread.currentThread().getId();
            log.info(UPDATE_Thread_ID,id);
            //Long empId = (Long)request.getSession().getAttribute("employee");
            //employee.setUpdateTime(LocalDateTime.now());
            //employee.setUpdateUser(empId);
            employeeService.updateById(employee);
            return R.success(UPDATE_SUCCESS);
        }
  • 代码修复
  • js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
  • long型数据统一转为String字符串。
    1. 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)。
    2. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。
  •     /**
         * 扩展mvc框架的消息转换器
         * @param converters
         */
        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            //log.info("扩展消息转换器...");
            //创建消息转换器对象
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            //设置对象转换器,底层使用Jackson将Java对象转为json
            messageConverter.setObjectMapper(new JacksonObjectMapper());
            //将上面的消息转换器对象追加到mvc框架的转换器集合中
            converters.add(0,messageConverter);
        }
查询员工信息
  • 执行流程
    1. 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
    2. 在add.html页面获取url中的参数[员工id]
    3. 发送ajax请求,请求服务端,同时提交员工id参数
    4. 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
    5. 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
    6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
    7. 服务端接收员工信息,并进行处理,完成后给页面响应
    8. 页面接收到服务端响应信息后进行相应处理
  • Controller 代码如下:
  •    /**
         * 根据id查询员工信息
         * @param id
         * @return
         */
        @GetMapping("/{id}")
        public R<Employee> getById(@PathVariable Long id){
            log.info(getById_ID);
            Employee employee = employeeService.getById(id);
            if(employee != null){
                return R.success(employee);
            }
            return R.error(getById_ERROR);
        }
公共字段自动填充

实现步骤:

1. 在实体类的属性上加入@TableField注解,指定自动填充的策略。

    //mybaits-plus自动填充
    @TableField(fill = FieldFill.INSERT)//插入时自动填充
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)//插入,更新时自动填充
    private LocalDateTime updateTime;

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

    @TableField(fill = FieldFill.INSERT_UPDATE)//插入,更新时自动填充
    private Long updateUser;

2. 编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。

  • 功能完善
    1. 编写BaseContext工具类,基于ThreadLocal封装的工具类

    •  * @author PangPAi
       * @date 2024/7/12
       * 基于ThreadLocal封装工具类,用于存储和获取上下文信息,当前用户登录ID等。
       */
      public class BaseContext {
          private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

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

          /**
           * 获取值
           * @return
           */
          public static Long getCurrentId(){
              return threadLocal.get();
          }
      }
    1. 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
    1. 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
    •     /**
           * 更新操作,自动填充
           * @param metaObject
           */
          @Override
          public void updateFill(MetaObject metaObject) {
              log.info("公共字段自动填充[update]...");
              log.info(metaObject.toString());

              long id = Thread.currentThread().getId();
              log.info("线程id为:{}",id);
              metaObject.setValue("updateTime",LocalDateTime.now());
              metaObject.setValue("updateUser",BaseContext.getCurrentId());
          }
新增分类或菜品
  • 在CategoryController中创建新增分类的方法
  •     @PostMapping
        public R<String> save(@RequestBody Category category) {
            log.info("category:{}", category);
            categoryService.save(category);
            return R.success(SUCCESS_CATEGORY_MESSAGE);
        }
  • 在CategoryServiceImpl中创建新增分类的方法

  1. public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService{

        /**
         * 根据id删除分类,删除之前需要进行判断
         * @param id
         */
        @Override
        public void remove(Long id) {
        }
    }
删除分类或菜品
  • 在Controller中创建delete方法
  •     @DeleteMapping
        public R<String> delete(Long id) {
            log.info(DELETE_CATEGORY_ID, id);
            categoryService.removeById(id);
            //categoryService.remove(id);
            return R.success(SUCCESS_CATEGORY_MESSAGE);
        }
  • 在Service中创建delete方法
  1. interface CategoryService extends IService<Category> {
         void remove(Long id);
    }
  • 功能完善
    1. 创建体类Dish和Setmeal
    2. Mapper接口DishMapper和SetmealMapper
    3. Service接口DishService和SetmealService
    4. Service实现类DishServiceImpl和SetmealServiceImpl
  • CategoryServiceImpl 代码如下:

  1. public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService{

  2. private DishService dishService;

  3. private SetmealService setmealService;

  • * 根据id删除分类,删除之前需要进行判断
    * @param id
    */
    @Override
    public void remove(Long id) {
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据分类id进行查询
    dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
    int count1 = dishService.count(dishLambdaQueryWrapper);

  • if(count1 > 0){
    //已经关联菜品,抛出一个业务异常
    throw new CustomException(Category_remove_dishService);
    }
    //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据分类id进行查询
    setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
    int count2 = setmealService.count();
    if(count2 > 0){
    //已经关联套餐,抛出一个业务异常
    throw new CustomException(Category_remove_setmealService);
    }
    //正常删除分类
    super.removeById(id);
    }
    }
修改分类或菜品
  • Controller 代码如下:

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

文件上传下载
  • 文件CommonController​类上传代码实现
    1. 定义图片路径
    1. Controller 代码如下:
  • 文件下载代码实现
  1. 代码如下:

  2. @Slf4j
    @RequestMapping("/common")
    ublic class CommonController {
        @Value("${reggie.path}")
        private String basePath;

        /**
         * 文件上传
         * @param file
         * @return
         */
        @ApiOperation(value = "文件上传", notes = "上传文件",httpMethod = "POST")
        @PostMapping("/upload")
        public R<String> upload(MultipartFile file){
            //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
            log.info(file.toString());
            //原始文件名
            String originalFilename = file.getOriginalFilename();//abc.jpg
            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
            //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
            String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg
            //创建一个目录对象
            File dir = new File(basePath);
            //判断当前目录是否存在
            if(!dir.exists()){
                //目录不存在,需要创建
                dir.mkdirs();
            }
            try {
                //将临时文件转存到指定位置
                file.transferTo(new File(basePath + fileName));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return R.success(fileName);
        }

        /**
         * 文件下载
         * @param name
         * @param response
         */
        @ApiOperation(value = "文件下载", notes = "下载文件",httpMethod = "GET")

        @GetMapping("/download")
        public void download(String name, HttpServletResponse response){
            try {
                //输入流,通过输入流读取文件内容
                FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

                //输出流,通过输出流将文件写回浏览器
                ServletOutputStream outputStream = response.getOutputStream();
                //设置响应头信息,告诉浏览器这是个图片
                response.setContentType("image/jpeg");
                int len = 0;
                byte[] bytes = new byte[1024];
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes,0,len);
                    outputStream.flush();
                }
                //关闭资源
                outputStream.close();
                fileInputStream.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
套餐和菜品分类

套餐分类

  • CategoryController 代码如下:
  •     /**
         * 根据条件查询数据
         * @param category
         * @return
         */
        @ApiOperation(value = "根据条件查询分类数据",notes = "根据条件查询分类数据",httpMethod = "GET")
        @GetMapping("/list")
        public R<List<Category>> list(Category category) {
            log.info(FIND_CATEGORY);
            LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>(); //条件构造器
            queryWrapper.eq(category.getType() != null, Category::getType, category.getType()); //添加查询条件
            queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime); //排序条件
            List<Category> list = categoryService.list(queryWrapper);
            log.info(FIND_CATEGORY_LIST, list);
            return R.success(list);

        }

菜品分类

  • DishController 代码如下:
保存数据
  • 往数据表中保存数据
  •     /**
         * 新增套餐
         * @param setmealDto
         * @return
         */
        @ApiOperation(value = "新增套餐",notes = "新增套餐",httpMethod = "POST")
        @PostMapping
        public R<String> save(@RequestBody SetmealDto setmealDto){
            log.info(SAVE_SetMal_DTO,setmealDto);
            setmealService.saveWithDish(setmealDto);
            return R.success(SAVE_SUCCESS);
        }
  • 在SetmealService接口中扩展saveWithDish方法
  •     /**
         * 新增套餐,同时需要保存套餐和菜品的关联关系
         * @param setmealDto
         */
        public void saveWithDish(SetmealDto setmealDto);
  • 在SetmealServiceImpl类中实现saveWithDish方法

  1. @Slf4j
    public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {

        @Autowired
        private SetmealDishService setmealDishService;

        /**
         * 新增套餐,同时需要保存套餐和菜品的关联关系
         * @param setmealDto
         */
        @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操作
            setmealDishService.saveBatch(setmealDishes);
        }
批量删除
  • 在SetmealController中创建delete方法
  •    /**
         * 删除套餐
         * @param ids
         * @return
         */
        @ApiOperation(value = "删除套餐",notes = "删除套餐",httpMethod = "DELETE")
        @DeleteMapping
        public R<String> delete(@RequestParam List<Long> ids){
            log.info("ids:{}",ids);
            setmealService.removeWithDish(ids);
            return R.success(DELETE_SUCCESS);
        }
  • 在SetmealService接口中扩展removeWithDish方法
  •     /**
         * 删除套餐,同时需要删除套餐和菜品的关联数据
         * @param ids
         */
        public void removeWithDish(List<Long> ids);
  • 在SetmealServiceImpl中实现removeWithDish方法
  •     /**
         * 删除套餐,同时需要删除套餐和菜品的关联数据
         * @param ids
         */
        @Transactional
        public void removeWithDish(List<Long> ids) {
            //select count(*) from setmeal where id in (1,2,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_removeWithDish_status);
            }

            //如果可以删除,先删除套餐表中的数据---setmeal
            this.removeByIds(ids);

            //delete from setmeal_dish where setmeal_id in (1,2,3)
            LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
            //删除关系表中的数据----setmeal_dish
            setmealDishService.remove(lambdaQueryWrapper);
        }
批量启售/停售
  • 在DishController
  •     /**
         * 修改菜品状态
         * @param status
         * @param ids
         * @return
         */
        @PostMapping("/status/{status}")
        //这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
        public R<String> status(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
            //log.info("status:{}",status);
            //log.info("ids:{}",ids);
            LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
            queryWrapper.in(ids != null, Dish::getId, ids);
            //根据数据进行批量查询
            List<Dish> list = dishService.list(queryWrapper);

            if (list != null) {
                for (Dish dish : list) {
                    if (dish != null) {
                        dish.setStatus(status);
                        dishService.updateById(dish);
                    }
                }
                return R.success(status_success);
            }
            return R.error(status_error);
        }

移动端

邮箱短信发送
  • 导入maven坐标,这里我把所有的,一次性给了,并且修改过一些内容

     <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
    </properties>
    <dependencies>
        <!-- Spring Boot的核心启动器,提供基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 用于测试Spring Boot应用的依赖项,如JUnit、Spring Test等 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 阿里云短信服务SDK -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.16</version>
            <!-- 阿里云短信服务SDK,用于发送短信 -->
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.1.0</version>
            <!-- 阿里云短信服务API -->
        </dependency>

        <!-- 邮件发送功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <!-- 构建RESTful Web服务所需的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <!-- MyBatis增强工具,简化CRUD操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!-- Java工具类库,自动处理boilerplate代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <!-- JSON处理库,用于Java对象与JSON格式转换 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <!-- Apache常用工具类库 -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!-- MySQL数据库JDBC驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>8.0.33</scope>
        </dependency>

        <!-- Druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

        <!-- Redis的Java客户端库 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.8.0</version>
        </dependency>

        <!-- Spring Data Redis集成 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Spring Cache集成 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
<!--         Spring Security集成-->
<!--        <dependency>-->
<!--            <groupId>org.apache.shardingsphere</groupId>-->
<!--            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>-->
<!--            <version>4.0.0-RC1</version>-->
<!--        </dependency>-->


<!--        <!– 日志记录功能 –>-->
<!--        <dependency>-->
<!--            <groupId>io.springfox</groupId>-->
<!--            <artifactId>springfox-boot-starter</artifactId>-->
<!--            <version>3.0.0</version>-->
<!--        </dependency>-->

        <!-- Swaggger2 API文档生成工具 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

<!--        <!– Knife4j文档生成工具 –>-->
<!--        <dependency>-->
<!--            <groupId>com.github.xiaoymin</groupId>-->
<!--            <artifactId>knife4j-spring-boot-starter</artifactId>-->
<!--            <version>3.0.2</version>-->
<!--        </dependency>-->

        <!-- Google核心库,提供额外的集合类型和工具 -->
        <!-- 避免版本冲突 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
            <!-- 避免版本冲突 -->
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.83</version>
        </dependency>


    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.pangpai.ruiji.RuijiApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

  • 2、调用API(将资料中工具类导入即可)
手机验证码登录
  • 准备工作:
    1. 导入实体类 User
    2. Mapper接口 UserMapper
    3. 业务层接口 UserService
    4. 业务层实现类 UserServiceImpl
    5. 控制层 UserController
    6. 导入工具类SMSUtils、ValidateCodeUtils
  • 修改过滤器

  •  * 检查用户是否已经完成登录
     */
    @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();// /backend/index.html

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

            //定义不需要处理的请求路径
            String[] urls = new String[]{
                    "/employee/login",
                    "/employee/logout",
                    "/backend/**",
                    "/front/**",
                    "/common/**",
                    "/user/sendMsg",
                    "/user/login",
                    "/swagger-ui.html",
                    "/doc.html",
                    "/webjars/**",
                    "/swagger-resources",
                    "/v2/**",
                    "/test/exception"

            };

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

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

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

                Long empId = (Long) request.getSession().getAttribute("employee");
                BaseContext.setCurrentId(empId);

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

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

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

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

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

        }

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

  1. @Slf4j
    @RequestMapping("/user")
    @Api(tags = "用户模块", description = "用户模块相关接口,短信邮箱服务")
    public class UserController {
        @Resource
        UserService userService;

        @Resource
        JavaMailSender javaMailSender;

        @Autowired
        private RedisTemplate redisTemplate;

        @Value("${spring.mail.username}")
        String from;


        @ApiOperation(value = "发送验证码", notes = "发送验证码",httpMethod = "POST")
        @PostMapping("/sendMsg")
        private R<String> sendMsg(@RequestBody User user, HttpSession session) {
            //这里用qq邮箱去发送验证码
            //获取到前端提交过来的qq号
            String phone = user.getPhone();
            //这里工具类判是否为空
            if (StringUtils.isNotEmpty(phone)) {
                //这里用到生成验证码的工具类
                String code = ValidateCodeUtils.generateValidateCode(4).toString();//生成四位的验证码
                log.info("code={}", code);
                SimpleMailMessage simpleMailMessage = new SimpleMailMessage();  // 构建一个邮件的对象
                simpleMailMessage.setFrom(from);   //设置邮件发件者
                simpleMailMessage.setTo(phone);  //设置邮件接受者
                simpleMailMessage.setSubject("登录验证码");   //设置有纪念的主题
                String text = "[PangPAi] 您的瑞吉外卖登录验证码为" + code + ",请勿泄露";  //设置邮件的征文
                simpleMailMessage.setText(text);
                //将生成的验证码保存到Session
                //将我们生成的手机号和验证码放到session里面,我们后面用户填入验证码之后,我们验证的时候就从这里去取然后进行比对
                //这里我们需要一个异常捕获
                //ession.setAttribute(phone, code);
                redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);//将验证码保存到redis中,设置过期时间为5分钟
                try {
                    javaMailSender.send(simpleMailMessage);//发送邮件
                    return R.success("验证码短信发送成功");
                } catch (MailException e) {
                    e.printStackTrace();
                }
            }
            return R.error("验证码发送失败");
        }


        //移动应用登录端
        @ApiOperation(value = "移动端登录", notes = "移动端登录",httpMethod = "POST")
        @PostMapping("/login")
        //这里使用map来接收前端传过来的值
        private R<User> login(@RequestBody Map map, HttpSession session) {
            log.info(map.toString());
            //使用map来接收参数,接收键值参数、
            //编写处理逻辑
            //获取到手机号
            //获取到验证码
            //从Session中获取到保存的验证码
            //将session中获取到的验证码和前端提交过来的验证码进行比较,这样就可以实现一个验证的方式
            //比对页面提交的验证码和session中
            //判断当前的手机号在数据库查询是否有记录,如果没有记录,说明是一个新的用户,然后自动将这个手机号进行注册
            String phone = map.get("phone").toString();
            String code = map.get("code").toString();
            //获取session中phone字段对应的验证码
            //Object codeInSession = session.getAttribute(phone);

            //从redis中获取验证码
            Object codeInSession = redisTemplate.opsForValue().get(phone);

            //下面进行比对
            if (codeInSession != null && codeInSession.equals(code)) {
                LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
                //在表中根据号码来查询是否存在该邮箱用户
                userLambdaQueryWrapper.eq(User::getPhone, phone);
                User user = userService.getOne(userLambdaQueryWrapper);
                if (user == null) {
                    //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
                    user = new User();
                    user.setPhone(phone);
                    user.setStatus(1);
                    userService.save(user);
                }
                //这里我们将user存储进去,后面各项操作,我们会用,其中拦截器那边会判断用户是否登录,所以我们将这个存储进去,
                session.setAttribute("user", user.getId());

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

                return R.success(user);
            }
            return R.error("验证失败");
        }

        /**
         * 退出登录
         *
         * @param session
         * @return
         */
        @ApiOperation(value = "退出登录", notes = "退出登录",httpMethod = "POST")
        @PostMapping("/loginout")
        public R<String> loginout(HttpSession session) {
            //清除session中的id。
            session.removeAttribute("user");
            return R.success("退出登录成功");
        }
    }
导入用户地址簿
  • 实现流程
    1. 实体类 AddressBook
    2. Mapper接口 AddressBookMapper
    3. 业务层接口 AddressBookService
    4. 业务层实现类 AddressBookServiceImpl
    5. 控制层 AddressBookController
菜品展示
  • 修改DishController的list方法
  •     /**
         * 根据条件查询对应的菜品数据
         * @param dish
         * @return
         */
        @GetMapping("/list")
        public R<List<DishDto>> list(Dish dish){
            List<DishDto> dishDtoList = null;
            //动态构造key值
            String key = "dish_" + dish.getCategoryId() +"_" + dish.getStatus();//缓存的key值
            //从redis中获取缓存数据
            dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
            if(dishDtoList != null){
                //如果存在,直接返回,无需查询
                return R.success(dishDtoList);
            }
            //构造查询条件
            LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
            //添加条件,查询状态为1(起售状态)的菜品
            queryWrapper.eq(Dish::getStatus,1);

            //添加排序条件
            queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
            List<Dish> list = dishService.list(queryWrapper);
            dishDtoList = list.stream().map((item) -> {
                DishDto dishDto = new DishDto();
                BeanUtils.copyProperties(item,dishDto);
                Long categoryId = item.getCategoryId();//分类id
                //根据id查询分类对象
                Category category = categoryService.getById(categoryId);

                if(category != null){
                    String categoryName = category.getName();
                    dishDto.setCategoryName(categoryName);
                }

                //当前菜品的id
                Long dishId = item.getId();
                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缓存
            redisTemplate.opsForValue().set(key, dishDtoList, 60, TimeUnit.MINUTES);

            return R.success(dishDtoList);
        }
  • 在SetmealController中创建list方法

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

            List<Setmeal> list = setmealService.list(queryWrapper);
            return R.success(list);
        }
购物车
  • 准备流程:
    1. 导入实体类 ShoppingCart
    2. Mapper接口 ShoppingCartMapper
    3. 业务层接口 ShoppingCartService
    4. 业务层实现类 ShoppingCartServiceImpl
    5. 控制层 ShoppingCartController
  • 添加购物车
    1. 在ShoppingCartController中创建add方法
  •  /**
         * 添加购物车
         *
         * @param shoppingCart
         * @return
         */
        @ApiOperation(value = "添加购物车",notes = "添加购物车",httpMethod = "POST")
        @PostMapping("/add")
        public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
            log.info(ADD_shoppingCart, 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);
        }
  • 查看购物车
    1. 在ShoppingCartController中创建list方法
  •  /**
         * 查看购物车
         * @return
         */
        @ApiOperation(value = "查看购物车",notes = "查看购物车",httpMethod = "GET")
        @GetMapping("/list")
        public R<List<ShoppingCart>> list() {
            log.info(LOOK_shoppingCart);

            // 获取当前用户的ID
            Long currentId = BaseContext.getCurrentId();
            // 创建查询条件,查询当前用户的购物车记录
            LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());//指定当前用户的ID
            queryWrapper.orderByAsc(ShoppingCart::getCreateTime);//按照创建时间排序

            // 获取购物车列表
            List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

            // 遍历列表,检查并删除数量小于1的记录
            for (ShoppingCart cartItem : list) {
                if (cartItem.getNumber() <= 0) {
                    // 如果数量小于或等于0,则删除该记录
                    shoppingCartService.removeById(cartItem.getId());
                }
            }
            // 重新获取更新后的购物车列表
            list = shoppingCartService.list(queryWrapper);
            return R.success(list);
        }
  • 清空购物车
    1. 在ShoppingCartController中创建clean方法
  •  /**
         * 清空购物车
         *
         * @return
         */
        @ApiOperation(value = "清空购物车",notes = "清空购物车",httpMethod = "DELETE")
        @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(CLEAR_shoppingCart);
        }
用户下单
  • 实现流程:
    1. 导入实体类 Orders、OrderDetail
    2. Mapper接口 OrderMapper、OrderDetailMapper
    3. 业务层接口 OrderService、OrderDetailService
    4. 业务层实现类 OrderServiceImpl、OrderDetailServiceImpl
    5. 控制层 OrderController、OrderDetailController
  • 在OrderController中创建submit方法
  •     @ApiOperation(value = "用户下单", notes = "用户下单接口", httpMethod = "POST")
        @PostMapping("/submit")
        public R<String> submit(@RequestBody Orders orders) {
            log.info(SUBMIT_ORDER, orders);
            orderService.submit(orders);
            return R.success(SUBMIT_ORDER_SUCCESS);
        }

在OrderServiceImpl中实现submit方法

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

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

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook == null){
            throw new CustomException(Order_submit_addressBook);
        }

        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、用户数量多,系统访问量大

    2、频繁服务数据库,系统性能下降,用户体验差。

环境搭建
  • 流程如下:
    1. 使用git来管理代码
  • Gitee上,之后在本地上创建新的分支,完成后并推送到码云社区,来专门开发优化缓存的代码。
    1. maven坐标,之前也给过了

  1.   <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    1. 配置文件:application.yml

  2.   port: 8080
    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 # 请填写你的数据库的密码
      redis:
       # 使用本地 redis 服务就用本地的 Ip 地址;使用Linux 上的 edis 服务就用 Linux 上的 Ip 地址
        host: 172.168.2.94 # 请填写你自己的 Ip 地址
        port: 6379
        password: 123456 # 填写你自己设置的 redis 的密码;没设置密码请注释该行
        database: 0
      cache:
        redis:
          time-to-live: 1800000 # 设置缓存数据的过期时间

    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

    reggie:
      path: D:\IDEA\ptg\ # 请填写你自己所存储的图片的位置
    1. 配置类:RedisConfig.java

  3. public class RedisConfig extends CachingConfigurerSupport {

        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

            //默认的Key序列化器为:JdkSerializationRedisSerializer
            redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
            //redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化

            redisTemplate.setConnectionFactory(connectionFactory);
            return redisTemplate;
        }
    }
缓存短信验证码
  • 改造思路,这里的东西我前期UserController中全给过了
    1. 在服务端 UserController 中注入 RedisTemplate 对象,用于操作 Redis
  •   @Autowired
        private RedisTemplate redisTemplate;
    1. 在服务端 UserController 的 sendMsg 方法中,将随机生成的验证码缓存到 Redis 中,并设置有效期为 5 分钟
  •    redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);//将验证码保存到redis中,设置过期时间为5分钟
    1. 在服务端 UserController 的 login 方法中,从 Redis 中获取缓存的验证码,如果登录成功则删除 Redis 中的验证码
  •            session.setAttribute("user", user.getId());

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

                return R.success(user);
缓存菜品数据
  • 改造思路
    1. 改造 DishController的list方法,这里我前期给过了

    /**
     * 根据条件查询对应的菜品数据
     * @param dish
     * @return
     */
    @ApiOperation(value = "根据条件查询对应的菜品数据", notes = "根据条件查询对应的菜品数据",httpMethod = "GET")
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
        List<DishDto> dishDtoList = null;
        //动态构造key值
        String key = "dish_" + dish.getCategoryId() +"_" + dish.getStatus();//缓存的key值
        //从redis中获取缓存数据
        dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
        if(dishDtoList != null){
            //如果存在,直接返回,无需查询
            return R.success(dishDtoList);
        }
        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
        //添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);

        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);
        dishDtoList = list.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);
            Long categoryId = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);

            if(category != null){
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }

            //当前菜品的id
            Long dishId = item.getId();
            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缓存
        redisTemplate.opsForValue().set(key, dishDtoList, 60, TimeUnit.MINUTES);

        return R.success(dishDtoList);
    }

  1. 改造 DishController的save方法

   /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @ApiOperation(value = "新增菜品", notes = "新增菜品",httpMethod = "POST")
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.saveWithFlavor(dishDto);
        return R.success(SUCCESS_DISH_MESSAGE);
    }

  1. 改造 DishController的update方法

    /**
     * 修改菜品
     * @param dishDto
     * @return
     */
    @ApiOperation(value = "修改菜品", notes = "修改菜品",httpMethod = "PUT")
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.updateWithFlavor(dishDto);
        return R.success(UPDATE_DISH_MESSAGE);
    }

SpringCache
  • 介绍

Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。

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

  • 针对不同的缓存技术需要实现不同的CacheManager:

  • 引导类上加@EnableCaching

@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
@EnableSwagger2//开启swagger2
public class RuijiApplication {

    public static void main(String[] args) {
        SpringApplication.run(RuijiApplication.class, args);
        log.info(Application);
    }
}

  • 集成Redis

1.pom.xml

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

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

2.application.yml

spring:
  redis:
    host: 192.168.200.200
    port: 6379
    password: root@123456
    database: 0
  cache:
    redis:
      time-to-live: 1800000   #设置缓存过期时间,可选

缓存套餐数据
  • pom.xml中引入依赖

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

  • application.yml中设置缓存过期时间

spring:  
  cache:
    redis:
      time-to-live: 1800000 #设置缓存数据的过期时间

  • 启动类上加入@EnableCaching注解

@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
@EnableSwagger2//开启swagger2
public class RuijiApplication {

    public static void main(String[] args) {
        SpringApplication.run(RuijiApplication.class, args);
        log.info(Application);
    }
}

  • 序列化对象

/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 * @param <T>
 */
@Data
@ApiModel("返回结果")
public class R<T> implements Serializable{

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

    @ApiModelProperty("错误信息")
    private String msg; //错误信息

    @ApiModelProperty("数据")
    private T data; //数据

    @ApiModelProperty("动态数据")
//    private Map map = new HashMap(); //动态数据
    private Map<String, String> 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, (String) value);
//        this.map.put(key, value);
        return this;
    }

}

清理套餐数据

这里忘记了,不知道怎么写,可以去温习一下内容

读写分离
Mysql主从复制

这个我单独写了一份:mysql主从复制-CSDN博客

项目实现读写分离
  • 数据库环境准备(主从复制)
  • reggie,运行SQL
  • 代码改造
  • Sharding-JDBC实现读写分离的步骤:
    1. 导入Maven坐标
  •  <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
        <version>4.0.0-RC1</version>
     </dependency>
    1. 在配置文件中配置读写分离的规则

  1. shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://180.76.54.3:3306/reggie?characterEncoding=utf-8
        username: root
        password: root
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.81.128:3306/reggie?characterEncoding=utf-8
        username: root
        password: root
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin #轮询
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false
    main:
    allow-bean-definition-overriding: true
    1. 在配置文件中配置允许bean定义覆盖配置顶

  2.   main:
        allow-bean-definition-overriding: true
项目部署
  • 部署架构

  • 部署环境说明
  • 部署前端项目
    1. 在服务器 A 中安装 Nginx,将整理打包好的项目 dist 目录上传到 Nginx 的 html 目录下
    1. 修改 Nginx 配置文件 nginx.conf

  1.   listen        80;
      server_name   localhost;

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

      # 反向代理配置
      location ^~ /api/{
              rewrite ^/api/(.*)$ /$1 break;
              proxy_pass http://192.168.138.101:8080;
      }

      error_page 500 502 503 504 /50x.html;
      location = /50x.html{
          root html;
      }
    }
  • 部署后端项目
    1. 在服务器 B 中安装 jdk、git、maven、MySQL,使用 git clone 命令将 git 远程仓库的代码克隆下来。
    2. 将资料中提供的 reggieStart.sh 文件上传到服务器 B
    • chmod 777 reggieStart.sh 命令设置执行权限
      同时根据实际情况来更改 .sh文件:vim reggieStart.sh。
      对于需要加载图片的位置需要自己手动导入 Linux,并更改相关配置文件的路径
    • img目录用于存放图片
    • path改为你自己服务器中图片所放的位置,并且需要保存,提交并推送到git仓库中
    1. 执行 reggieStart.sh 脚本文件,自动部署项目:./reggieStart.sh。
    2. 这里不用担心第2步中后半段,因为自动部署文件中有拉取远程仓库的功能,会自动更新本地仓库
  • 最终效果:

最后,如果想要部署到云服务器上,也是可以的,但是量力而行。

​​

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小胖胖哇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值