瑞吉外卖项目 Springboot+mybatisplus

瑞吉外卖项目

这个项目是基于springboot+mybatis-plus开发的,很适合新手入门的第一个小项目

主要是一些crud的操作,目的是让你了解开发的过程

在这之前我先是学了javaSE、javaWeb、Mysql、maven、git、SSM、springboot和mybatisplus

也了解了一点前端的h5、css、js、vue和elementui

我是全程跟着黑马学习,感兴趣的朋友也可以去看看黑马讲这个项目的视频,很清晰;

黑马程序员Java项目实战《瑞吉外卖》

项目后台和小程序部分功能已经做完
已全部上传,感兴趣的朋友们可以看看,也可以来问我要数据库文件
第一次做项目,做的不是很好,但是功能都能实现,可以跑通,仅供大家参考
github:https://github.com/yourTdd/REGGIE
gitee:https://gitee.com/liao-tao020415/reggie

后台业务开发

项目构建

  • 在IDEA中创建一个springboot项目,以下是我的pom文件

  • yaml文件中主要是端口和数据库的设置,就不放出来了,感兴趣的朋友可以问我要.sql文件

    <?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/>
        </parent>
    
        <groupId>com.lt</groupId>
        <artifactId>reggie</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.junit.platform</groupId>
                <artifactId>junit-platform-launcher</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <scope>compile</scope>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.20</version>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.76</version>
            </dependency>
    
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>2.6</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.23</version>
            </dependency>
    
    
            <!--阿里云短信服务-->
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.5.16</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
                <version>1.1.0</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.4.5</version>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

数据库设计

该项目一共有11张表,对应着11个实体类

在这里插入图片描述

登陆/退出功能

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

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

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

        // 4、密码比对
        if (!emp.getPassword().equals(password)){
            return R.error("登录失败!");
        }

        // 5、查看员工状态,是否禁用
        if (emp.getStatus() == 0){
            return R.error("账号已禁用!");
        }

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

        return R.success(emp);
    }
后台系统退出功能
@PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        request.getSession().removeAttribute("employee");
        return R.success("退出成功!");
    }
后台系统首页构成和效果展示方式

在这里插入图片描述

右侧会随左侧menu选中不同而进行页面的切换

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

完善登录功能

问题:如果用户不登陆,直接访问系统首页面也可以正常访问

  • 只有登录成功之后才可以访问,没有登录则跳转到登录页面

  • 创建自定义过滤器

    /**
     * 检查用户是否已经完成登录
     */
    @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
    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();
            // 2、不处理的请求
            String[] urls = new String[]{
                    "/employee/login",
                    "/employee/logout",
                    "/backend/**",
                    "/front/**"
            };
    
            // 3、是否需要处理
            boolean check = check(urls, requestURI);
            // 4、如果不需要处理,直接放行
            if (check) {
                filterChain.doFilter(request, response);
                return;
            }
            // 5、判断登录状态,如果已登录,直接放行
            if (request.getSession().getAttribute("employee") != null) {
                filterChain.doFilter(request, response);
                return;
            }
            // 6、如果未登录则返回未登录结果,通过输出流的方式向客户端页面响应数据
            response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
            return;
    
        }
    
        /**
         * 路径匹配,检查本次请求是否需要放行
         *
         * @param urls
         * @param requestURI
         * @return
         */
        public boolean check(String[] urls, String requestURI) {
            for (String url : urls) {
                boolean match = PATH_MATCHER.match(url, requestURI);
                if (match) {
                    return true;
                }
            }
            return false;
        }
    }
    

员工管理业务

基本结构
  • 员工模型

在这里插入图片描述

  • 实体类Emloyee

    @Data
    public class Employee implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
        private String username;
    
        private String name;
    
        private String password;
    
        private String phone;
    
        private String sex;
    
        private String idNumber;
    
        private Integer status;
    
        private LocalDateTime createTime;
    
        private LocalDateTime updateTime;
    
        private Long createUser;
    
        private Long updateUser;
    
    }
    
  • Mapper接口

    @Mapper
    public interface EmployeeMapper extends BaseMapper<Employee> {
    }
    
  • 业务层接口及实现类

    public interface EmployeeService extends IService<Employee> {
    }
    
    @Service
    public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
    }
    
  • EmloyeeController

    @Slf4j
    @RestController
    @RequestMapping("/employee")
    public class EmployeeController{
    
        @Autowired
        private EmployeeService employeeService;
    }
    
新增员工

在这里插入图片描述

对username添加了唯一约束

在这里插入图片描述

在controller中添加新增方法

/**
 * 新增员工
 * @param employee
 * @return
 */
@PostMapping
public R<String> add(@RequestBody Employee employee,HttpServletRequest request){
    log.info("新增员工,员工信息:"+employee);

    // 设置初始密码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("新增员工成功");
}
全局异常处理

为解决username唯一约束,不能新增重复的员工

新建一个统一异常捕获

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

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException e){
        log.error(e.getMessage());
        if (e.getMessage().contains("Duplicate entry")){
            String[] split = e.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}
员工信息分页查询
  • 配置mybaatis-plus的分页插件

    /**
     * 配置MP的分页插件
     */
    @Configuration
    public class MPConfig {
    
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor(){
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return interceptor;
        }
    }
    
  • 编写分页查询代码

        /**
         * 员工信息分页查询
         * @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> qw = new LambdaQueryWrapper();
            // 添加过滤条件
            qw.like(StringUtils.isNotEmpty(name),Employee::getName,name);
            // 添加排序条件
            qw.orderByDesc(Employee::getUpdateTime);
            // 执行查询
            employeeService.page(pageInfo,qw);
            return R.success(pageInfo);
        }
    }
    
启用/禁用员工账号

员工状态为禁用的不能登录系统,只有管理员可以对其他普通用户进行启用、禁用操作

普通用户登陆系统后,启用/禁用按钮不显示

</el-button>
            <el-button
              type="text"
              size="small"
              class="delBut non"
              @click="statusHandle(scope.row)"
              v-if="user === 'admin'"
            >
              {{ scope.row.status == '1' ? '禁用' : '启用' }}
            </el-button>

修改员工信息,可以修改状态,也适用于编辑员工信息

/**
 * 根据id修改员工信息
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Employee employee, HttpServletRequest request){

    Long empId = (Long) request.getSession().getAttribute("employee");
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);
    employeeService.updateById(employee);
    return R.success("员工信息修改成功!");
}
丢失精度问题
  • 分页查询时id
    在这里插入图片描述

  • 禁用请求时id在这里插入图片描述

==>  Preparing: UPDATE employee SET status=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 0(Integer), 2022-07-16T09:04:56.277(LocalDateTime), 1(Long), 1548105591044567000(Long)
<==    Updates: 0

JS只能精确16位,JS对Long型数字进行了处理,导致请求的id与数据库不一致

  • 在服务端给页面响应json数据时进行处理,将long型转换位String类型

    添加对象转换器JacksonObjectMapper, 添加一系列的序列化器,其中也包含了Long->String

    public class JacksonObjectMapper extends ObjectMapper {
    
        public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
        public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
        public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    
        public JacksonObjectMapper() {
            super();
            //收到未知属性时不报异常
            this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
    
            //反序列化时,属性不存在的兼容处理
            this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
            SimpleModule simpleModule = new SimpleModule()
                    .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                    .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                    .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                    .addSerializer(BigInteger.class, ToStringSerializer.instance)
                    // 将Long型转换为String
                    .addSerializer(Long.class, ToStringSerializer.instance)
                    .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                    .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                    .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
    
            //注册功能模块 例如,可以添加自定义序列化器和反序列化器
            this.registerModule(simpleModule);
        }
    }
    
  • 在WebMvcConfig中扩展SpringMVC的消息转换器,使用对象转换器进行object->json转换

    /**
         * 扩展mvc框架的消息转换器
         * @param converters
         */
        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            // 创建消息转换器对象
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            // 设置对象转换器
            messageConverter.setObjectMapper(new JacksonObjectMapper());
            // 将消息转换器对象追加到mvc框架的转换器集合中
            converters.add(0,messageConverter);// 放到 0 位置优先使用
        }
    
  • 禁用成功

在这里插入图片描述

在这里插入图片描述

编辑员工信息
  • 根据id查询员工信息将其反显到编辑页面

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        if (employee!=null){
            return R.success(employee);
        }
        return R.error("没有查询到该员工信息");
    }
    
  • 再通过修改方法进行修改,与上面修改员工状态一致

    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Employee employee, HttpServletRequest request){
    
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(empId);
        employeeService.updateById(employee);
        return R.success("员工信息修改成功!");
    }
    

分类管理业务

公共字段自动填充
  • 在实体类emloyee的公共字段上增加@TableField注解

        @TableField(fill = FieldFill.INSERT) // 插入时填充
        private LocalDateTime createTime;
    
        @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时填充
        private LocalDateTime updateTime;
    
        @TableField(fill = FieldFill.INSERT)
        private Long createUser;
    
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Long updateUser;
    
  • 创建自定义元数据对象处理器实现MetaObjectHandler接口

    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
    
        /**
         * 插入操作,自动填充
         * @param metaObject
         */
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("公共字段自动填充[insert]");
            metaObject.setValue("createTime", LocalDateTime.now());
            metaObject.setValue("updateTime", LocalDateTime.now());
            metaObject.setValue("createUser", new Long(1));
            metaObject.setValue("updateUser", new Long(1));
        }
    
        /**
         * 更新操作,自动填充
         * @param metaObject
         */
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("公共字段自动填充[update]");
    
            Long id = Thread.currentThread().getId();
            log.info("线程id为: {}",id);
    
            metaObject.setValue("updateTime", LocalDateTime.now());
            metaObject.setValue("updateUser", new Long(1));
        }
    }
    
功能完善
  • 上述公共字段自动填充时,不能获取到当前登录用户的id,创建人和修改人都模拟了一个id

    又因为,Filter、Controller中的update方法、处理器中的updateFill方法都处于一个线程

在这里插入图片描述

  • ThreadLocal是Thread的局部变量

    ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果

    只有在线程内才能获取到对应的值,线程外则不能访问

  • 编写工具类封装ThreadLocal,在filter中调用其设置当前用户id,在handler中来获取(这两者处于同一线程)

    • 编写工具类BaseContext

      /**
       * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
       */
      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();
          }
      }
      
    • 在LoginCheckFilter中设置id

      Long empId = (Long) request.getSession().getAttribute("employee");
      BaseContext.setCurrentId(empId);
      
    • 在MyMetaObjectHandler中获取id

      之前此处只填入了new Long(1),将其替换为BaseContext.getCurrentId()

      metaObject.setValue("createUser", BaseContext.getCurrentId());
      metaObject.setValue("updateUser", BaseContext.getCurrentId());
      
新增分类
  • 分类模型

在这里插入图片描述

  • name字段唯一约束

在这里插入图片描述

  • 实体类

    @Data
    public class Category implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
    
        //类型 1 菜品分类 2 套餐分类
        private Integer type;
    
    
        //分类名称
        private String name;
    
    
        //顺序
        private Integer sort;
    
    
        //创建时间
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
    
    
        //更新时间
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private LocalDateTime updateTime;
    
    
        //创建人
        @TableField(fill = FieldFill.INSERT)
        private Long createUser;
    
    
        //修改人
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Long updateUser;
    }
    
  • CategoryMapper接口

    @Mapper
    public interface CategoryMapper extends BaseMapper<Category> {
    }
    
  • 业务层接口及实现类

    public interface CategoryService extends IService<Category> {
    }
    
    @Service
    public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService{
    }
    
  • CategoryController

    /**
     * 分类管理
     */
    @RestController
    @RequestMapping("/category")
    @Slf4j
    public class CategoryController {
    
        @Autowired
        private CategoryService categoryService;
    
        @PostMapping
        public R<String> add(@RequestBody Category category){
    
            log.info("category:{}",category);
            categoryService.save(category);
            return R.success("新增分类成功!");
        }
    }
    
分类信息分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
    // 分页构造器
    Page<Category> pageInfo = new Page<>(page,pageSize);
    // 条件构造器
    LambdaQueryWrapper<Category> qw = new LambdaQueryWrapper();
    // 添加排序条件
    qw.orderByAsc(Category::getSort);
    // 进行分页查询
    categoryService.page(pageInfo,qw);
    return R.success(pageInfo);
}
删除分类

分类关联了菜品和套餐,不能随意删除,需要判断

  • 在CategoryService中编写remove方法,并在其实现类中作出判断

    @Autowired
    private DishService dishService;
    
    @Autowired
    private SetmealService setmealService;
    
    /**
     * 根据id删除分类,删除之前需要判断
     * @param id
     */
    @Override
    public void remove(Long id) {
    
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 添加查询条件
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
    
        // 查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
        if (count1 > 0){
            // 已经关联菜品,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }
    
        // 查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count();
        if (count2 > 0){
            // 已经关联套餐,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        // 正常删除分类
        super.removeById(id);
    }
    
  • 创建自定义异常,并用全局异常处理器捕获异常

    /**
     * 自定义业务异常类
     */
    public class CustomException extends RuntimeException{
    
        public CustomException(String message){
            super(message);
        }
    }
    
    • 在GlobalExceptionHandler中添加处理方法

      /**
       * 异常处理方法
       * @return
       */
      @ExceptionHandler(CustomException.class)
      public R<String> exceptionHandler(CustomException e){
          log.error(e.getMessage());
          return R.error(e.getMessage());
      }
      
修改分类
/**
 * 根据id修改分类
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category){
    log.info("修改分类信息:{}",category);
    categoryService.updateById(category);
    return R.success("修改分类信息成功成功");
}

菜品管理业务

文件上传下载
  • 添加upload页面,负责文件上传
<el-upload class="avatar-uploader"
        action="/common/upload"
        :show-file-list="false"
        :on-success="handleAvatarSuccess"
        :before-upload="beforeUpload"
        ref="upload">
    <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
    <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>

在这里插入图片描述

  • 编写文件上传的控制器

    @RestController
    @RequestMapping("/common")
    public class CommonController {
    
        @Value("${reggie.path}")
        private String basePath;
    
        /**
         * 文件上传
         * @param file
         * @return
         */
        @PostMapping("/upload")
        public R<String> upload(MultipartFile file) {
            // file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除
            // 原始文件名
            String originalFilename = file.getOriginalFilename();
            // 切最后一个点 得到后缀
            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
    
            // 使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
            String fileName = UUID.randomUUID().toString() + suffix;
    
            // 创建一个目录对象
            File dir = new File(basePath);
            // 判断当前目录是否存在
            if (!dir.exists()){
                // 目录不存在,需要创建
                dir.mkdirs();
            }
    
            try {
                // 将临时文件转存到指定位置
                file.transferTo(new File(basePath + fileName));
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return R.success(fileName);
        }
    }
    
  • 文件下载

    /**
     * 文件下载
     * @param name
     * @param response
     */
    @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();
        }
    }
    
新增菜品
数据模型
  • 菜品

在这里插入图片描述

  • 菜品口味
    在这里插入图片描述

  • 创建实体类Dish和DishFlavor

    @Data
    public class Dish implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
    
        //菜品名称
        private String name;
    
    
        //菜品分类id
        private Long categoryId;
    
    
        //菜品价格
        private BigDecimal price;
    
    
        //商品码
        private String code;
    
    
        //图片
        private String image;
    
    
        //描述信息
        private String description;
    
    
        //0 停售 1 起售
        private Integer status;
    
    
        //顺序
        private Integer sort;
    
    
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
    
    
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private LocalDateTime updateTime;
    
    
        @TableField(fill = FieldFill.INSERT)
        private Long createUser;
    
    
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Long updateUser;
    
    
        //是否删除
        private Integer isDeleted;
    
    }
    
    @Data
    public class DishFlavor implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
    
        //菜品id
        private Long dishId;
    
    
        //口味名称
        private String name;
    
    
        //口味数据list
        private String value;
    
    
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
    
    
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private LocalDateTime updateTime;
    
    
        @TableField(fill = FieldFill.INSERT)
        private Long createUser;
    
    
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Long updateUser;
    
    
        //是否删除
        private Integer isDeleted;
    
    }
    
    新增菜品界面
    • 发送查询菜品分类请求在这里插入图片描述在这里插入图片描述

    • 保存菜品的同时保存对应的口味数据

    /**
     * 根据条件查询分类数据
     * @param category
     * @return
     */
    @GetMapping("/list")
    public R<List<Category>> type(Category category){
        LambdaQueryWrapper<Category> qw = new LambdaQueryWrapper();
        qw.eq(category.getType()!=null,Category::getType,category.getType());
        qw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        List<Category> list = categoryService.list(qw);
        return R.success(list);
    }
    
  • 创建DishDTO封装菜品口味及其他数据

    @Data
    public class DishDto extends Dish {
    
        private List<DishFlavor> flavors = new ArrayList<>();
    
        private String categoryName;
    
        private Integer copies;
    }
    
  • 在controller中编写新增代码

    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.save(dishDto);
        return R.success("添加菜品成功!");
    }
    
分页查询

分页查询页面中需要显示菜品分类名称,而dish表中只有分类id的字段

则需要用到dishDTO来封装分类名称

page方法中传入的name一起作用于按照名字查询

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

    Page<Dish> pageInfo = new Page<>(page, pageSize);

    Page<DishDto> dishDtoPage = new Page<>();

    LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper();
    lqw.like(name!=null,Dish::getName,name);
    lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    dishService.page(pageInfo,lqw);

    // 对象拷贝
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

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

    List<DishDto> list = records.stream().map((i) -> {
        DishDto dishDto = new DishDto();

        BeanUtils.copyProperties(i, dishDto);

        Long categoryId = i.getCategoryId();// 分类id
        // 根据id查询分类对象
        Category category = categoryService.getById(categoryId);
        String categoryName = category.getName();
        dishDto.setCategoryName(categoryName);
        return dishDto;
    }).collect(Collectors.toList());

    dishDtoPage.setRecords(list);

    return R.success(dishDtoPage);
}
删除/批量删除
/**
 * 删除和批量删除
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(@RequestParam(value = "ids")List<Long> ids){
    dishService.removeByIds(ids);
    return R.success("删除菜品成功!");
}
停售/启售
/**
 * 批量 启售/停售
 * @param stauts
 * @param ids
 * @return
 */
@PostMapping("/status/{status}")
public R<String> status(@PathVariable("status") String stauts,@RequestParam List<Long> ids){
    LambdaUpdateWrapper<Dish> uw = new LambdaUpdateWrapper();
    uw.in(Dish::getId,ids).set(Dish::getStatus,stauts.equals("0")?"0":"1");
    dishService.update(uw);
    return R.success("操作成功!");
}
修改菜品
  • 先要查询菜品分类,在新增菜品中已经写过可以用同一个

    @GetMapping("/list")
    public R<List<Category>> type(Category category){
        LambdaQueryWrapper<Category> qw = new LambdaQueryWrapper();
        qw.eq(category.getType()!=null,Category::getType,category.getType());
        qw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        List<Category> list = categoryService.list(qw);
        return R.success(list);
    }
    
  • 根据id查询菜品信息和对应口味信息返显到页面

    查询菜品信息和对应口味信息要借助DishDTO,其中封装了二者

    在Service中编写方法

    /**
     * 根据id来查询菜品的信息和对应的口味信息
     * @param id
     * @return
     */
    @Override
    public DishDto getByIdWithFlavor(Long id) {
        // 菜品基本信息
        Dish dish = this.getById(id);
    
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);
    
        // 对应的口味,从dish_flavor中查
        LambdaQueryWrapper<DishFlavor> qw = new LambdaQueryWrapper<>();
        qw.eq(DishFlavor::getDishId,dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(qw);
        dishDto.setFlavors(flavors);
    
        return dishDto;
    }
    

    最后在controller中响应

    /**
     * 根据id来查询菜品信息和口味信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<DishDto> getById(@PathVariable Long id){
    
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }
    
  • 提交

    • Service

      /**
       * 更新菜品信息,同时更新对应的口味信息
       * @param dishDto
       */
      @Transactional
      public void updateWithFlavor(DishDto dishDto) {
          // 更新dish表基本信息
          this.updateById(dishDto);
      
          // 清理当前菜品对应的口味数据  dish_flavor表的delete操作
          LambdaQueryWrapper<DishFlavor> qw = new LambdaQueryWrapper<>();
          qw.eq(DishFlavor::getDishId,dishDto.getId());
          dishFlavorService.remove(qw);
      
          // 添加当前提交过来的口味数据  dish_flavor表的insert操作
          List<DishFlavor> flavors = dishDto.getFlavors();
      
          flavors = flavors.stream().map((i)->{
              i.setDishId(dishDto.getId());
              return i;
          }).collect(Collectors.toList());
          dishFlavorService.saveBatch(flavors);
      
      }
      
    • Controller

      /**
       * 修改菜品
       * @param dishDto
       * @return
       */
      @PutMapping
      public R<String> update(@RequestBody DishDto dishDto){
          dishService.updateWithFlavor(dishDto);
          return R.success("保存成功!");
      }
      

套餐管理业务

套餐信息分页查询
  • 页面向后台发送ajax请求

在这里插入图片描述

  • 后台controller接收请求给出响应(既可以接收分页,也可以接收条件查询name)

    /**
     * 分页查询和条件查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        Page<SetmealDto> setmealDtoPage = setmealService.page(page, pageSize, name);
        return R.success(setmealDtoPage);
    }
    
  • 业务逻辑处理在Service中

    因为套餐信息和对应套餐分类名称categoryName不在一张表中

    创建SetmealDto封装信息

    @Data
    public class SetmealDto extends Setmeal {
    
        private List<SetmealDish> setmealDishes;
    
        private String categoryName;
    }
    
    /**
     * 分页查询套餐信息及其对应套餐分类名称
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @Override
    public Page<SetmealDto> page(int page,int pageSize,String name) {
    
        Page<Setmeal> pageinfo = new Page<>();
        Page<SetmealDto> setmealDtoPage = new Page<>();
    
        LambdaQueryWrapper<Setmeal> qw = new LambdaQueryWrapper<>();
        qw.eq(name!=null,Setmeal::getName,name);
        qw.orderByDesc(Setmeal::getUpdateTime);
        this.page(pageinfo,qw);
    
        BeanUtils.copyProperties(pageinfo,setmealDtoPage,"records");
    
        List<Setmeal> records = pageinfo.getRecords();
    
        List<SetmealDto> list = records.stream().map((i) -> {
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(i, setmealDto);
            Long categoryId = i.getCategoryId();
    
            Category category = categoryService.getById(categoryId);
            if (category != null) {
                setmealDto.setCategoryName(category.getName());
            }
            return setmealDto;
        }).collect(Collectors.toList());
    
        setmealDtoPage.setRecords(list);
        return setmealDtoPage;
    }
    
新增套餐
  • 页面向后台发送ajax请求

在这里插入图片描述

  • 后台controller接收请求并给出响应

    • /category/list

      /**
       * 根据条件查询分类数据
       * @param category
       * @return
       */
      @GetMapping("/list")
      public R<List<Category>> type(Category category){
          LambdaQueryWrapper<Category> qw = new LambdaQueryWrapper();
          qw.eq(category.getType()!=null,Category::getType,category.getType());
          qw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
          List<Category> list = categoryService.list(qw);
          return R.success(list);
      }
      
    • /dish/list

      /**
       * 根据分类id查询菜品
       * @param dish
       * @return
       */
      @GetMapping("/list")
      public R<List<Dish>> list(Dish dish){
          List<Dish> dishes = dishService.get(dish);
          return R.success(dishes);
      }
      
  • 根据id查询菜品Service

    /**
     * 根据分类id查询菜品
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public R<List<Dish>> list(Dish dish){
        List<Dish> dishes = dishService.get(dish);
        return R.success(dishes);
    }
    
  • 保存

    • 后台发送请求

在这里插入图片描述

  • controller给出响应

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
        boolean flag = setmealService.saveWithDish(setmealDto);
        return flag?R.success("新增成功!"):R.error("新增失败!");
    }
    
  • Service实现业务

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     * @return
     */
    @Override
    @Transactional
    public boolean saveWithDish(SetmealDto setmealDto) {
        // 保存套餐的基本信息
        boolean flag1 = this.save(setmealDto);
    
        // 保存套餐和菜品的关联信息
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        List<SetmealDish> dishes = setmealDishes.stream().map((i) -> {
            i.setSetmealId(setmealDto.getId());
            return i;
        }).collect(Collectors.toList());
    
        boolean flag2 = setmealDishService.saveBatch(dishes);
        return flag1&&flag2;
    }
    
修改套餐
  • 页面给后台发送ajax请求获取该套餐信息

在这里插入图片描述

  • controller接收请求响应信息返回

    /**
     * 查询详情
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<SetmealDto> getById(@PathVariable Long id){
        SetmealDto setmealDto = setmealService.getWithDishById(id);
        return R.success(setmealDto);
    }
    
  • service处理业务

    /**
     * 查询详情
     * @param id
     * @return
     */
    @Override
    public SetmealDto getWithDishById(Long id) {
        SetmealDto setmealDto = new SetmealDto();
        Setmeal setmeal = this.getById(id);
        BeanUtils.copyProperties(setmeal,setmealDto);
    
        LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper<>();
        qw.eq(id!=null,SetmealDish::getSetmealId,id);
        List<SetmealDish> list = setmealDishService.list(qw);
    
        setmealDto.setSetmealDishes(list);
    
        return setmealDto;
    }
    
  • 页面发送保存请求

在这里插入图片描述

  • 在service中处理业务,在controller接收并响应

    • service

      /**
       * 修改套餐信息,并保存到关联信息
       * @return
       */
      @Override
      @Transactional
      public boolean updateWithDish(SetmealDto setmealDto) {
          // 修改套餐信息
          boolean flag1 = this.updateById(setmealDto);
          // 修改关联信息
          LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper<>();
          qw.eq(SetmealDish::getSetmealId,setmealDto.getId());
          setmealDishService.remove(qw);
      
          // 把套餐里的菜品和套餐的id关联
          List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
          setmealDishes.stream().map((i)->{
              i.setSetmealId(setmealDto.getId());
              return i;
          }).collect(Collectors.toList());
      
          boolean flag2 = setmealDishService.saveBatch(setmealDishes);
      
          return flag1&&flag2;
      }
      
    • controller

      /**
       * 保存修改
       * @param setmealDto
       * @return
       */
      @PutMapping
      public R<String> update(@RequestBody SetmealDto setmealDto){
          boolean flag = setmealService.updateWithDish(setmealDto);
          return flag?R.success("修改成功!"):R.error("修改失败");
      }
      
删除套餐
  • 页面给后台发送ajax请求

在这里插入图片描述

  • controller接收请求并给出响应

    /**
     * 删除套餐的同时删除对应的关联信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(@RequestParam List ids){
        boolean flag = setmealService.deleteWithDish(ids);
        return flag?R.success("删除成功!"):R.error("删除失败");
    }
    
  • Service处理业务

    /**
     * 删除套餐的同时删除对应的关联信息
     * @param ids
     * @return
     */
    @Override
    @Transactional
    public boolean deleteWithDish(List<Long> ids) {
        boolean flag1 = this.removeByIds(ids);
        LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper<>();
        qw.in(SetmealDish::getSetmealId,ids);
        boolean flag2 = setmealDishService.remove(qw);
        return flag1&&flag2;
    }
    
启售/停售
  • 页面向后台发送ajax请求

    • 停售
      在这里插入图片描述

    • 启售
      在这里插入图片描述

  • controller接收请求并给出响应

    /**
     * 更改状态
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> status(@PathVariable String status, @RequestParam List ids){
        boolean b = setmealService.updateStatus(status, ids);
        return b?R.success("更新状态成功!"):R.error("更新失败!");
    }
    
  • Service处理业务

    /**
     * 更新状态
     * @param status
     * @param ids
     */
    @Override
    public boolean updateStatus(String status, List ids) {
        LambdaUpdateWrapper<Setmeal> uw = new LambdaUpdateWrapper();
        uw.in(Setmeal::getId,ids).set(Setmeal::getStatus,status.equals("0")?"0":"1");
        boolean flag = this.update(uw);
        return flag;
    }
    

最终效果

  • 员工管理页面
    效果展示

  • 分类管理页面
    在这里插入图片描述

  • 菜品管理页面在这里插入图片描述

  • 套餐管理页面在这里插入图片描述

  • 订单明细页面在这里插入图片描述

  • 小程序页面在这里插入图片描述

在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

涛堆堆

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

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

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

打赏作者

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

抵扣说明:

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

余额充值