苍穹外卖-day03

苍穹外卖-day03

课程内容

  • 公共字段自动填充
  • 新增菜品
  • 菜品分页查询
  • 删除菜品
  • 修改菜品

**功能实现:**菜品管理

菜品管理效果图:

1. 公共字段自动填充

1.1 问题分析

在上一章节我们已经完成了后台系统的员工管理功能菜品分类功能的开发,在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:

序号字段名含义数据类型
1create_time创建时间datetime
2create_user创建人idbigint
3update_time修改时间datetime
4update_user修改人idbigint

而针对于这些字段,我们的赋值方式为:

1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。

2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下:

新增员工方法:

	/*** 新增员工** @param employeeDTO*/public void save(EmployeeDTO employeeDTO) {//.......................////设置当前记录的创建时间和修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//设置当前记录创建人id和修改人idemployee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改employee.setUpdateUser(BaseContext.getCurrentId());///employeeMapper.insert(employee);}

编辑员工方法:

	/*** 编辑员工信息** @param employeeDTO*/public void update(EmployeeDTO employeeDTO) {//........................................///employee.setUpdateTime(LocalDateTime.now());employee.setUpdateUser(BaseContext.getCurrentId());///employeeMapper.update(employee);}

新增菜品分类方法:

	/*** 新增分类* @param categoryDTO*/public void save(CategoryDTO categoryDTO) {//....................................////设置创建时间、修改时间、创建人、修改人category.setCreateTime(LocalDateTime.now());category.setUpdateTime(LocalDateTime.now());category.setCreateUser(BaseContext.getCurrentId());category.setUpdateUser(BaseContext.getCurrentId());///categoryMapper.insert(category);}

修改菜品分类方法:

	/*** 修改分类* @param categoryDTO*/public void update(CategoryDTO categoryDTO) {//....................................////设置修改时间、修改人category.setUpdateTime(LocalDateTime.now());category.setUpdateUser(BaseContext.getCurrentId());//categoryMapper.update(category);}

如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

答案是可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。

1.2 实现思路

在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

序号字段名含义数据类型操作类型
1create_time创建时间datetimeinsert
2create_user创建人idbigintinsert
3update_time修改时间datetimeinsert、update
4update_user修改人idbigintinsert、update

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

若要实现上述步骤,需掌握以下知识(之前课程内容都学过)

**技术点:**枚举、注解、AOP、反射

1.3 代码开发

按照上一小节分析的实现步骤依次实现,共三步。

1.3.1 步骤一

自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。

package com.sky.annotation;import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定义注解,用于标识某个方法需要进行功能字段自动填充处理*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//数据库操作类型:UPDATE INSERTOperationType value();
}

其中OperationType已在sky-common模块中定义

package com.sky.enumeration;/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT
}
1.3.2 步骤二

自定义切面 AutoFillAspect

在sky-server模块,创建com.sky.aspect包。

package com.sky.aspect;/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){/重要//可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解log.info("开始进行公共字段自动填充...");}
}

完善自定义切面 AutoFillAspect 的 autoFill 方法

package com.sky.aspect;import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){log.info("开始进行公共字段自动填充...");//获取到当前被拦截的方法上的数据库操作类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象OperationType operationType = autoFill.value();//获得数据库操作类型//获取到当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if(args == null || args.length == 0){return;}Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射来赋值if(operationType == OperationType.INSERT){//为4个公共字段赋值try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setCreateTime.invoke(entity,now);setCreateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}else if(operationType == OperationType.UPDATE){//为2个公共字段赋值try {Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}}
}
1.3.3 步骤三

在Mapper接口的方法上加入 AutoFill 注解

CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作

package com.sky.mapper;@Mapper
public interface CategoryMapper {/*** 插入数据* @param category*/@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +" VALUES" +" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")@AutoFill(value = OperationType.INSERT)void insert(Category category);/*** 根据id修改分类* @param category*/@AutoFill(value = OperationType.UPDATE)void update(Category category);}

同时,将业务层为公共字段赋值的代码注释掉。

1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。

2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。

1.4 功能测试

新增菜品分类为例,进行测试

启动项目和Nginx

查看控制台

通过观察控制台输出的SQL来确定公共字段填充是否完成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看表

category表中数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中create_time,update_time,create_user,update_user字段都已完成自动填充。

由于使用admin(id=1)用户登录进行菜品添加操作,故create_user,update_user都为1.

1.5 代码提交

点击提交:

提交过程中,出现提示:

继续push:

推送成功:

2. 新增菜品

2.1 需求分析与设计

2.1.1 产品原型

后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。

新增菜品原型:

当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。

业务规则:

  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片
2.1.2 接口设计

根据上述原型图先粗粒度设计接口,共包含3个接口。

接口设计:

  • 根据类型查询分类(已完成)
  • 文件上传
  • 新增菜品

接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。

1. 根据类型查询分类


2. 文件上传


3. 新增菜品



2.1.3 表设计

通过原型图进行分析:

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

表名说明
dish菜品表
dish_flavor菜品口味表

1). 菜品表:dish

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)菜品名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)菜品价格
imagevarchar(255)图片路径
descriptionvarchar(255)菜品描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

2). 菜品口味表:dish_flavor

字段名数据类型说明备注
idbigint主键自增
dish_idbigint菜品id逻辑外键
namevarchar(32)口味名称
valuevarchar(255)口味值

2.2 代码开发

2.2.1 文件上传实现

因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。

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

实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:

  1. 直接将图片保存到服务的硬盘(springmvc中的文件上传)
    1. 优点:开发便捷,成本低
    2. 缺点:扩容困难
  2. 使用分布式文件系统进行存储
    1. 优点:容易实现扩容
    2. 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
  3. 使用第三方的存储服务(例如OSS)
    1. 优点:开发简单,拥有强大功能,免维护
    2. 缺点:付费

在本项目选用阿里云的OSS服务进行文件存储。(前面课程已学习过阿里云OSS,不再赘述)

实现步骤:

1). 定义OSS相关配置

在sky-server模块

application-dev.yml

sky:alioss:endpoint: oss-cn-hangzhou.aliyuncs.comaccess-key-id: LTAI5tPeFLzsPPT8gG3LPW64access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7bucket-name: sky-take-out

application.yml

spring:profiles:active: dev    #设置环境
sky:alioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: ${sky.alioss.access-key-secret}bucket-name: ${sky.alioss.bucket-name}

2). 读取OSS配置

在sky-common模块中,已定义

package com.sky.properties;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;}

3). 生成OSS工具类对象

在sky-server模块

package com.sky.config;import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置类,用于创建AliOssUtil对象*/
@Configuration
@Slf4j
public class OssConfiguration {@Bean@ConditionalOnMissingBeanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}

其中,AliOssUtil.java已在sky-common模块中定义

package com.sky.utils;import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;/*** 文件上传** @param bytes* @param objectName* @return*/public String upload(byte[] bytes, String objectName) {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);try {// 创建PutObject请求。ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}//文件访问路径规则 https://BucketName.Endpoint/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}
}

4). 定义文件上传接口

在sky-server模块中定义接口

package com.sky.controller.admin;import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;/*** 通用接口*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传* @param file* @return*/@PostMapping("/upload")@ApiOperation("文件上传")public Result<String> upload(MultipartFile file){log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//截取原始文件名的后缀   dfdfdf.pngString extension = originalFilename.substring(originalFilename.lastIndexOf("."));//构造新文件名称String objectName = UUID.randomUUID().toString() + extension;//文件的请求路径String filePath = aliOssUtil.upload(file.getBytes(), objectName);return Result.success(filePath);} catch (IOException e) {log.error("文件上传失败:{}", e);}return Result.error(MessageConstant.UPLOAD_FAILED);}
}
2.2.2 新增菜品实现

1). 设计DTO类

在sky-pojo模块中

package com.sky.dto;import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;@Data
public class DishDTO implements Serializable {private Long id;//菜品名称private String name;//菜品分类idprivate Long categoryId;//菜品价格private BigDecimal price;//图片private String image;//描述信息private String description;//0 停售 1 起售private Integer status;//口味private List<DishFlavor> flavors = new ArrayList<>();
}

2). Controller层

进入到sky-server模块

package com.sky.controller.admin;import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;/*** 菜品管理*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;/*** 新增菜品** @param dishDTO* @return*/@PostMapping@ApiOperation("新增菜品")public Result save(@RequestBody DishDTO dishDTO) {log.info("新增菜品:{}", dishDTO);dishService.saveWithFlavor(dishDTO);//后绪步骤开发return Result.success();}
}

3). Service层接口

package com.sky.service;import com.sky.dto.DishDTO;
import com.sky.entity.Dish;public interface DishService {/*** 新增菜品和对应的口味** @param dishDTO*/public void saveWithFlavor(DishDTO dishDTO);}

4). Service层实现类

package com.sky.service.impl;@Service
@Slf4j
public class DishServiceImpl implements DishService {@Autowiredprivate DishMapper dishMapper;@Autowiredprivate DishFlavorMapper dishFlavorMapper;/*** 新增菜品和对应的口味** @param dishDTO*/@Transactionalpublic void saveWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);//向菜品表插入1条数据dishMapper.insert(dish);//后绪步骤实现//获取insert语句生成的主键值Long dishId = dish.getId();List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);});//向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);//后绪步骤实现}}}

5). Mapper层

DishMapper.java中添加

	/*** 插入菜品数据** @param dish*/@AutoFill(value = OperationType.INSERT)void insert(Dish dish);

在/resources/mapper中创建DishMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper"></mapper>

DishFlavorMapper.java

package com.sky.mapper;import com.sky.entity.DishFlavor;
import java.util.List;@Mapper
public interface DishFlavorMapper {/*** 批量插入口味数据* @param flavors*/void insertBatch(List<DishFlavor> flavors);}

在/resources/mapper中创建DishFlavorMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper"><insert id="insertBatch">insert into dish_flavor (dish_id, name, value) VALUES<foreach collection="flavors" item="df" separator=",">(#{df.dishId},#{df.name},#{df.value})</foreach></insert>
</mapper>

2.3 功能测试

进入到菜品管理—>新建菜品

由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加的数据。

dish表:

dish_flavor表:

测试成功。

2.4代码提交

后续步骤和上述功能代码提交一致,不再赘述。

3. 菜品分页查询

3.1 需求分析和设计

3.1.1 产品原型

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

菜品分页原型:

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、最后操作时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

业务规则:

  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
3.1.2 接口设计

根据上述原型图,设计出相应的接口。


3.2 代码开发

3.2.1 设计DTO类

根据菜品分页查询接口定义设计对应的DTO:

在sky-pojo模块中,已定义

package com.sky.dto;import lombok.Data;
import java.io.Serializable;@Data
public class DishPageQueryDTO implements Serializable {private int page;private int pageSize;private String name;private Integer categoryId; //分类idprivate Integer status; //状态 0表示禁用 1表示启用}
3.2.2 设计VO类

根据菜品分页查询接口定义设计对应的VO:

在sky-pojo模块中,已定义

package com.sky.vo;import com.sky.entity.DishFlavor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {private Long id;//菜品名称private String name;//菜品分类idprivate Long categoryId;//菜品价格private BigDecimal price;//图片private String image;//描述信息private String description;//0 停售 1 起售private Integer status;//更新时间private LocalDateTime updateTime;//分类名称private String categoryName;//菜品关联的口味private List<DishFlavor> flavors = new ArrayList<>();
}
3.2.3 Controller层

根据接口定义创建DishController的page分页查询方法:

	/*** 菜品分页查询** @param dishPageQueryDTO* @return*/@GetMapping("/page")@ApiOperation("菜品分页查询")public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {log.info("菜品分页查询:{}", dishPageQueryDTO);PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义return Result.success(pageResult);}
3.2.4 Service层接口

在 DishService 中扩展分页查询方法:

	/*** 菜品分页查询** @param dishPageQueryDTO* @return*/PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
3.2.5 Service层实现类

在 DishServiceImpl 中实现分页查询方法:

	/*** 菜品分页查询** @param dishPageQueryDTO* @return*/public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现return new PageResult(page.getTotal(), page.getResult());}
3.2.6 Mapper层

在 DishMapper 接口中声明 pageQuery 方法:

	/*** 菜品分页查询** @param dishPageQueryDTO* @return*/Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

在 DishMapper.xml 中编写SQL:

<select id="pageQuery" resultType="com.sky.vo.DishVO">select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id<where><if test="name != null">and d.name like concat('%',#{name},'%')</if><if test="categoryId != null">and d.category_id = #{categoryId}</if><if test="status != null">and d.status = #{status}</if></where>order by d.create_time desc
</select>

3.3 功能测试

3.3.1 接口文档测试

**启动服务:**访问http://localhost:8080/doc.html,进入菜品分页查询接口

**注意:**使用admin用户登录重新获取token,防止token失效。

点击发送:

3.3.2 前后端联调测试

启动nginx,访问 http://localhost

点击菜品管理

数据成功查出。

4. 删除菜品

4.1 需求分析和设计

4.1.1 产品原型

在菜品列表页面,每个菜品后面对应的操作分别为修改删除停售,可通过删除功能完成对菜品及相关的数据进行删除。

删除菜品原型:

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉
4.1.2 接口设计

根据上述原型图,设计出相应的接口。


20221121211814429

**注意:**删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。

4.1.3 表设计

在进行删除菜品操作时,会涉及到以下三张表。

注意事项:

  • 在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
  • setmeal_dish表为菜品和套餐关联的中间表。
  • 若删除的菜品数据关联着某个套餐,此时,删除失败。
  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。

4.2 代码开发

4.1.2 Controller层

根据删除菜品的接口定义在DishController中创建方法:

	/*** 菜品批量删除** @param ids* @return*/@DeleteMapping@ApiOperation("菜品批量删除")public Result delete(@RequestParam List<Long> ids) {log.info("菜品批量删除:{}", ids);dishService.deleteBatch(ids);//后绪步骤实现return Result.success();}
4.2.2 Service层接口

在DishService接口中声明deleteBatch方法:

	/*** 菜品批量删除** @param ids*/void deleteBatch(List<Long> ids);
4.2.3 Service层实现类

在DishServiceImpl中实现deleteBatch方法:

    @Autowiredprivate SetmealDishMapper setmealDishMapper;/*** 菜品批量删除** @param ids*/@Transactional//事务public void deleteBatch(List<Long> ids) {//判断当前菜品是否能够删除---是否存在起售中的菜品??for (Long id : ids) {Dish dish = dishMapper.getById(id);//后绪步骤实现if (dish.getStatus() == StatusConstant.ENABLE) {//当前菜品处于起售中,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}//判断当前菜品是否能够删除---是否被套餐关联了??List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if (setmealIds != null && setmealIds.size() > 0) {//当前菜品被套餐关联了,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}//删除菜品表中的菜品数据for (Long id : ids) {dishMapper.deleteById(id);//后绪步骤实现//删除菜品关联的口味数据dishFlavorMapper.deleteByDishId(id);//后绪步骤实现}}
4.2.4 Mapper层

在DishMapper中声明getById方法,并配置SQL:

	/*** 根据主键查询菜品** @param id* @return*/@Select("select * from dish where id = #{id}")Dish getById(Long id);

创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:

package com.sky.mapper;import com.sky.entity.SetmealDish;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;@Mapper
public interface SetmealDishMapper {/*** 根据菜品id查询对应的套餐id** @param dishIds* @return*///select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
}

SetmealDishMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper"><select id="getSetmealIdsByDishIds" resultType="java.lang.Long">select setmeal_id from setmeal_dish where dish_id in<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">#{dishId}</foreach></select>
</mapper>

在DishMapper.java中声明deleteById方法并配置SQL:

	/*** 根据主键删除菜品数据** @param id*/@Delete("delete from dish where id = #{id}")void deleteById(Long id);

在DishFlavorMapper中声明deleteByDishId方法并配置SQL:

    /*** 根据菜品id删除对应的口味数据* @param dishId*/@Delete("delete from dish_flavor where dish_id = #{dishId}")void deleteByDishId(Long dishId);

4.3 功能测试

既可以通过Swagger接口文档进行测试,也可以通过前后端联调测试,接下来,我们直接使用前后端联调测试

进入到菜品列表查询页面

对测试菜品进行删除操作

同时,进到dish表和dish_flavor两个表查看测试菜品的相关数据都已被成功删除。

再次,删除状态为启售的菜品

点击批量删除

删除失败,因为起售中的菜品不能删除。

4.4 代码提交

后续步骤和上述功能代码提交一致,不再赘述。

5. 修改菜品

5.1 需求分析和设计

5.1.1 产品原型

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击保存按钮完成修改操作。

修改菜品原型:

5.1.2 接口设计

通过对上述原型图进行分析,该页面共涉及4个接口。

接口:

  • 根据id查询菜品
  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

我们只需要实现根据id查询菜品修改菜品两个接口,接下来,我们来重点分析这两个接口。

1). 根据id查询菜品


2). 修改菜品


注:因为是修改功能,请求方式可设置为PUT。

5.2 代码开发

5.2.1 根据id查询菜品实现

1). Controller层

根据id查询菜品的接口定义在DishController中创建方法:

    /*** 根据id查询菜品** @param id* @return*/@GetMapping("/{id}")@ApiOperation("根据id查询菜品")public Result<DishVO> getById(@PathVariable Long id) {log.info("根据id查询菜品:{}", id);DishVO dishVO = dishService.getByIdWithFlavor(id);//后绪步骤实现return Result.success(dishVO);}

2). Service层接口

在DishService接口中声明getByIdWithFlavor方法:

	/*** 根据id查询菜品和对应的口味数据** @param id* @return*/DishVO getByIdWithFlavor(Long id);

3). Service层实现类

在DishServiceImpl中实现getByIdWithFlavor方法:

	/*** 根据id查询菜品和对应的口味数据** @param id* @return*/public DishVO getByIdWithFlavor(Long id) {//根据id查询菜品数据Dish dish = dishMapper.getById(id);//根据菜品id查询口味数据List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//后绪步骤实现//将查询到的数据封装到VODishVO dishVO = new DishVO();BeanUtils.copyProperties(dish, dishVO);dishVO.setFlavors(dishFlavors);return dishVO;}

4). Mapper层

在DishFlavorMapper中声明getByDishId方法,并配置SQL:

    /*** 根据菜品id查询对应的口味数据* @param dishId* @return*/@Select("select * from dish_flavor where dish_id = #{dishId}")List<DishFlavor> getByDishId(Long dishId);
5.2.1 修改菜品实现

1). Controller层

根据修改菜品的接口定义在DishController中创建方法:

	/*** 修改菜品** @param dishDTO* @return*/@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishDTO) {log.info("修改菜品:{}", dishDTO);dishService.updateWithFlavor(dishDTO);return Result.success();}

2). Service层接口

在DishService接口中声明updateWithFlavor方法:

	/*** 根据id修改菜品基本信息和对应的口味信息** @param dishDTO*/void updateWithFlavor(DishDTO dishDTO);

3). Service层实现类

在DishServiceImpl中实现updateWithFlavor方法:

	/*** 根据id修改菜品基本信息和对应的口味信息** @param dishDTO*/public void updateWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);//修改菜品表基本信息dishMapper.update(dish);//删除原有的口味数据dishFlavorMapper.deleteByDishId(dishDTO.getId());//重新插入口味数据List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishDTO.getId());});//向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);}}

4). Mapper层

在DishMapper中,声明update方法:

	/*** 根据id动态修改菜品数据** @param dish*/@AutoFill(value = OperationType.UPDATE)void update(Dish dish);

并在DishMapper.xml文件中编写SQL:

<update id="update">update dish<set><if test="name != null">name = #{name},</if><if test="categoryId != null">category_id = #{categoryId},</if><if test="price != null">price = #{price},</if><if test="image != null">image = #{image},</if><if test="description != null">description = #{description},</if><if test="status != null">status = #{status},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="updateUser != null">update_user = #{updateUser},</if></set>where id = #{id}
</update>

5.3 功能测试

本次测试直接通过前后端联调测试 ,可使用Debug方式启动项目,观察运行中步骤。

进入菜品列表查询页面,对第一个菜品的价格进行修改

点击修改,回显成功

菜品价格修改后,点击保存

修改成功

5.4 代码提交

后续步骤和上述功能代码提交一致,不再赘述。

();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}

**4). Mapper层****在DishMapper中,声明update方法:**```java/*** 根据id动态修改菜品数据** @param dish*/@AutoFill(value = OperationType.UPDATE)void update(Dish dish);

并在DishMapper.xml文件中编写SQL:

<update id="update">update dish<set><if test="name != null">name = #{name},</if><if test="categoryId != null">category_id = #{categoryId},</if><if test="price != null">price = #{price},</if><if test="image != null">image = #{image},</if><if test="description != null">description = #{description},</if><if test="status != null">status = #{status},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="updateUser != null">update_user = #{updateUser},</if></set>where id = #{id}
</update>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/173318.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

成都无人机测绘公司 无人机测绘服务 无人机航测作业

无人机测绘是传统航空摄影测量方式的重要补充方式&#xff0c;它具有灵活、高效、适用范围广、生产周期短等优势&#xff0c;在小区域和飞行困难地区获取高分辨率图像具有明显的优势。目前&#xff0c;无人机测绘主要应用于土地监管、灾害应急处理、城市规划管理等方面。那么&a…

uniapp开发app,在ios真机上出现的css样式问题

比如下面的问题&#xff0c;在iphone 13上出现&#xff0c;在iphone xR上正常。 问题一&#xff1a;border:1rpx造成边框显示不全 在iphone13上border边框有一部分不显示&#xff1a; 在iphone xR上显示正常&#xff1a; 解决办法是&#xff1a; 将border边框设置中的1rpx改…

html2pdf

页面布局时将需要保存在同一页pdf的dom元素用div包裹&#xff0c;并为该div添加class类名&#xff0c;例如.convertPDF&#xff0c;如果有多页创建多个.convertPDF这个div&#xff0c;再循环保存pdf即可 用到了html2canvas和JsPdf这两个插件&#xff0c;自行站内搜索安装

drawio特性

drawio的特性 drawio是领先的基于Web技术的草图和图表功能功能的应用。 保证数据的安全 集成了各种不同的平台&#xff0c;和提供了在线的免费编辑器&#xff0c;可以使用app.diagrams.net来方案&#xff0c;drawio本身不会存储用户的数据。 随着互联网时代的发展&#xff0…

CVE-2021-44228 Apache log4j 远程命令执行漏洞

一、漏洞原理 log4j(log for java)是由Java编写的可靠、灵活的日志框架&#xff0c;是Apache旗下的一个开源项目&#xff0c;使用Log4j&#xff0c;我们更加方便的记录了日志信息&#xff0c;它不但能控制日志输出的目的地&#xff0c;也能控制日志输出的内容格式&#xff1b;…

走进国产机器人领军品牌华数机器人,共探数字化变革魔力

近日&#xff0c;纷享销客举办的“一院两司服务对接会暨走进纷享销客【数字化标杆】游学示范基地活动”在佛山顺利举行&#xff0c;本期活动走进华中数控旗下品牌、国家级专精特新“小巨人”企业华数机器人&#xff0c;特邀佛山华数机器人有限公司常务副总经理杨林、纷享销客广…

Go语言入门心法(十五):Go微服务实战

Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 Go语言入门心法(四): 异常体系 Go语言入门心法(五): 函数 Go语言入门心法(六): HTTP面向客户端|服务端编程 Go语言入门心法(七): 并发与通道 Go语言入门心法(八): mysql驱动安装报错o…

Nginx 的配置文件(负载均衡,反向代理)

Nginx可以配置代理多台服务器&#xff0c;当一台服务器宕机之后&#xff0c;仍能保持系统可用。 cmd查找端口是否使用&#xff1a;netstat -ano Nginx出现403 forbidden #解决办法&#xff1a;修改web目录的读写权限&#xff0c;或者是把nginx的启动用户改成目录的所属用户&…

IOC课程整理-13 Spring校验

1. Spring 校验使用场景 2. Validator 接口设计 3. Errors 接口设计 4. Errors 文案来源 5. 自定义 Validator 6. Validator 的救赎 7. 面试题精选 Spring 校验接口是哪个 org.springframework.validation.Validator Spring 有哪些校验核心组件&#xff1f;

ThinkPad电脑HDMI接口失灵如何解决?

ThinkPad电脑HDMI接口失灵如何解决&#xff1f; 如果平时正常使用的外接显示器&#xff0c;某天突然无法使用了&#xff0c;重新插拔依然无信号的话&#xff0c;可以打开系统的设备管理器&#xff08;快捷键winx&#xff09;&#xff0c;首先看一下监视器的识别情况&#xff0c…

基于RK3568高性价比全国产EMS储能解决方案(二)设计方案

目录 版 本 修 订 记 录 1. 产品介绍 1.1. 什么是XM3568-EP 1.2. 产品特点 1.3. 外壳尺寸 1.4. 外壳外观 1.5. 规格参数 2. 设备使用介绍 2.1. 下载需要使用到的驱动和调试工具 2.2. 启动网关 2.3. DEBUG串口的使用方法 2.4. LED指示灯说明 3. Linux系…

HPV感染的风险:闫会宁主任分析酒店环境中的常见因素

人类乳头瘤病毒(HPV)是一种普遍存在的病毒&#xff0c;其存在和传播方式多种多样。近年来&#xff0c;人们对于HPV的认识不断深入&#xff0c;知道其在酒店环境中的传播风险。本文将探讨哪些情况下在酒店可能感染HPV。 一、HPV的传播方式 HPV主要通过直接接触传播&#xff0c…

RocketMQ快速入门

RocketMQ的发展历史 Apache RocketMQ 是一个开源的分布式消息中间件系统&#xff0c;最初由阿里巴巴集团开发并于2016年贡献给 Apache 软件基金会。以下是 RocketMQ 的发展历史的主要里程碑&#xff1a; 2012年&#xff1a;阿里巴巴内部项目 RocketMQ 最初是阿里巴巴内部的一个…

SpringCloud 微服务全栈体系(四)

第六章 Nacos 配置管理 Nacos 除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 一、统一配置管理 当微服务部署的实例越来越多&#xff0c;达到数十、数百时&#xff0c;逐个修改微服务配置就会让人抓狂&#xff0c;而且很容易出错。我们需要一种统一配置管理方案…

Day 11 python学习笔记

模块 内置模块 random random&#xff1a;随机数模块 我们可以在解释器中看到其蕴含的方法 接下来我解释一些常用的方法&#xff1a; random.random( ) random.random( ) 返回0-1的随机数 [0,1) >>> random.random() 0.364183511476754 random.randint(n,m) r…

KNN-水仙花的分类

题目&#xff1a; 思路&#xff1a; 1、处理数据集&#xff0c;这里用的是题目已知的数据集&#xff0c;所以说需要提前将写好的数据放到excel表格里&#xff0c;再进行读取。 2、将数据集划分为训练集和测试集 3、定义K-NN模型。 4、训练模型 5、预测模型 6、计算分类精…

centos7虚拟机部署苍穹私有云环境记录

物理机建议16G内存以上&#xff0c;不然安装gpass过程中带不动虚拟机 步骤1&#xff1a;迅雷下载centos7.9镜像文件&#xff0c;并创建虚拟机&#xff0c;手动安装 http://ftp.sjtu.edu.cn/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso 后面安装gpass时会有校验…

Gerrit | 重磅! 2.x 版本升级到 3.x 版本----转

Gerrit | 重磅! 2.x 版本升级到 3.x 版本 为什么要做版本升级&#xff1f; 2.x known bugs 重大问题不一一列举&#xff0c;这里仅仅是举几个例子&#xff1a; 安全或权限问题&#xff1a;普通用户能看到敏感数据&#xff0c;例如看到其他用户的 hashed api 密码&#xff0c…

conda虚拟环境笔记收录

1、安装conda 增加执行权限&#xff1a; chmod x Anaconda3-2023.03-1-Linux-x86_64.sh 开始执行&#xff1a;./Anaconda3-2023.03-1-Linux-x86_64.sh2、查看版本 conda --version3、查看当前虚拟环境 虚拟环境和全局环境有前缀可见 如果不进行设置&#xff0c;重新启动就变成…

轻量级仿 Spring Boot=嵌入式 Tomcat+Spring MVC

啥&#xff1f;Spring Boot 不用&#xff1f;——对。就只是使用 Spring MVC Embedded Tomcat&#xff0c;而不用 Boot。为啥&#xff1f;——因为 Boot 太重了&#xff1a;&#xff09; 那是反智吗&#xff1f;Spring Boot 好好的就只是因为太重就不用&#xff1f;——稍安勿…