【项目实战 Day3】springboot + vue 苍穹外卖系统(菜品模块 完结)
目录
【开发中遇到的小知识】
1、idea代码填充快捷键
使用.var快捷键可以在创建对象时自动生成变量名
eg:
LocalDateTime.now().var + 回车可以自动生成
LocalDateTime now = LocalDateTime.now();2、四种类型的请求参数
1. Query String 参数 (查询参数)
位置:直接附在 URL 的
?后面。格式:
key1=value1&key2=value2特点:公开可见,有长度限制,会被浏览器记录。
用途:主要用于 GET 请求,传递过滤、分页、搜索等非敏感条件。
示例:
GET /api/products?**category=food&page=2&sort=price**
2. Body 参数 (请求体参数)
位置:在 HTTP 请求的正文部分。
格式:多种多样,常见有:
JSON (最常用):
{"name": "手机", "price": 2999}Form-Data:常用于文件上传。
特点:不可见,可传输大量复杂数据。
用途:主要用于 POST, PUT, PATCH 请求,传递需要创建或修改的核心数据。
示例:
POST /api/users
Body (JSON):{"username": "john", "password": "123456"}
3. Header 参数 (请求头参数)
位置:在 HTTP 请求的头部信息中。
格式:
Key: Value键值对。特点:不可见,用于传递协议和上下文信息。
用途:传递元数据,如身份认证、内容格式、客户端信息等。
常见参数:
Authorization: Bearer <token>(身份令牌)
Content-Type: application/json(告知服务器Body的格式)4. Path 参数 (路径参数)
位置:作为 URL 路径的一部分。
格式:通常用
/value形式直接嵌入路径。特点:用于标识唯一资源,是URL的一部分。
用途:指定要操作的具体资源ID。
示例:
GET /api/users/**123**(获取ID为123的用户)DELETE /api/products/**456**(删除ID为456的商品)
一、公共字段自动填充
1、AOP面向切面编程
模块中有许多字段重复率较高,如:create_time等,当系统表出现变化时,则需要重新设置这些字段,不利于系统维护
因此我们使用AOP面向切面编程:一种编程范式,主要目的是将那些【散布在应用程序多个地方、与核心业务逻辑无关的代码】分离出来,从而让开发者能更专注于核心业务。
AOP的思路是:
- 把这些功能(日志、事务等)从业务方法中抽取出来,封装成一个个独立的模块,称为 切面(Aspect)。
- 然后,它通过一种方式告诉框架:“请你在每个业务方法执行之前,先帮我运行一下我日志切面里的记录开始日志的方法”。
- 这个过程就像是“切开”了业务的流程,然后“插入”了新的功能,因此得名“切面”。
- 使用了AOP之后,代码会变得非常简洁
2、通过注解方式实现AOP
(1)自定义注解AutoFill
自定义注解:用于标识某个方法需要进行功能字段自动填充处理
/** * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理 */ @Target(ElementType.METHOD) // 声明该注解只能加在方法上 @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { // 数据库操作类型:UPDATE、INSERT OperationType value(); }其中OperationType为提前准备好的常量
/** * 数据库操作类型 */ public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT }(2)自定义切面类AutoFillAspect
自定义切面类:统一拦截加入AutoFill注解的方法,通过反射为公共字段赋值
【1】定义切入点
/** * 切入点:即对哪些类的哪些方法进行拦截 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){}execution(* com.sky.mapper.*.*(..)) —— 匹配com.sky.mapper包下所有类的所有方法
- 【第一个*】返回值类型不限
- 【com.sky.mapper.*】mapper包下的任何类
- 【.*(..)】类的任何方法,参数不限
@annotation(com.sky.annotation.AutoFill) —— 同时要求方法上必须标注有@AutoFill注解
这是组合条件,只有同时满足【在mapper包】且【方法上有@AutoFill注解】的方法才会被拦截【2】前置通知
/** * 前置通知,在通知中进行公共字段的赋值 */ @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.info("开始进行公共字段填充!");
- @Before —— 表示凡是匹配autoFillPointCut()规则的方法,在执行前都会先执行本方法
- 通常在这里会有获取方法注解、操作类型判断、反射设置字段值等后续逻辑
1)获取当前被拦截的方法上的数据库操作类型
// 获取当前被拦截的方法上的数据库操作类型 // 1.获取当前被拦截方法的签名信息 // 由于Spring AOP使用动态代理,joinPoint.getSignature()默认返回Signature接口类型 // 但我们需要获取方法上的注解,所以需要强制转换为更具体的MethodSignature类型 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 2.通过MethodSignature对象获取当前被拦截的Method对象 // 然后从该Method对象上获取特定的注解(@AutoFill) // 查询方法上是否标注了@AutoFill注解 AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 3.如果@AutoFill注解存在,调用其value()方法获取注解中定义的枚举值 // 这个枚举值(OperationType)通常表示数据库操作类型,如INSERT, UPDATE等 // 最终将获取到的操作类型赋值给operationType变量,供后续逻辑使用 OperationType operationType = autoFill.value();
- 获取当前被拦截方法的签名信息
- 通过被拦截对象签名获取被拦截对象的方法
- 查询该方法是否存在@AutoFill注解,若存在,获取其注解中定义的数据库操作类型
2)获取到当前被拦截的方法的参数 -- 实体对象
// 二、获取到当前被拦截的方法的参数--实体对象 Object[] args = joinPoint.getArgs(); if(args == null || args.length == 0){ //如果没有参数就不继续执行 return; } Object entity = args[0]; // 按规定,实体类一般放在参数的首位3)准备统一赋值的数据
// 准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId();4)根据当前不同的操作类型,为对应的属性通过反射来赋值
// 根据当前不同的操作类型,为对应的属性通过反射来赋值 if(operationType == OperationType.INSERT){ //如果当前操作为INSERT,则需要为createtime、createuser、updatetime、updateuser赋值 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) { throw new RuntimeException(e); } }else if(operationType == OperationType.UPDATE){ //如果当前操作为UPDATE,则需要为updatetime、updateuser赋值 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) { throw new RuntimeException(e); } }什么是反射?
- 正常调用:就像你去餐厅点菜,你知道菜单上有什么(类的方法),直接告诉服务员“我要一个A套餐”(调用方法orderSetA())。
- 反射调用:就像你不知道菜单有什么,你告诉服务员:“请把你们菜单上第三个分类下的第五道菜给我来一份”。你不需要在写代码时就知道那道菜具体叫什么,你只需要在运行时根据一些动态的规则(比如“第三个分类下的第五个”)来找到并点它。
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); setCreateTime.invoke(entity, now);这就是一个典型的反射应用:
entity.getClass():在运行时获取实体对象的Class信息。
getDeclaredMethod(...):动态地查找一个名叫setCreateTime(AutoFillConstant.SET_CREATE_TIME的值)、且接受一个LocalDateTime参数的方法。
method.invoke(...):动态地调用找到的这个方法,并传入具体的实体对象(entity)和参数值(now)。反射的好处:这段自动填充字段的代码是通用的。无论
entity是Employee、Category还是任何其他实体类,只要它有setCreateTime(LocalDateTime time)这个方法,这段代码就能工作。不需要为每个实体类都写一遍重复的赋值逻辑,这就是反射强大灵活性的体现。(3)在Mapper方法上加@AutoFill注解
字段自动填充主要应用在insert、update两种方法上
- insert方法需要填充4个字段内容:createtime、createuser、updatetime、updateuser
- update方法需要填充2个字段内容:updatetime、updateuser
目前我们开发了EmployeeMapper和CategoryMappper两个模块,因此我们需要在这两个Mapper的insert和update方法上加上@AutoFill注解
例如:
既然已经通过AOP实现createtime、createuser、updatetime、updateuser自动填充,那我们可以把Service层的相关填充代码注释掉
二、菜品管理模块
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在(根据类型查询分类)
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片(文件上传)
1、阿里云OSS对象存储初始化
【1】首先登陆阿里云,搜索OSS,充值完毕后进入控制台,点击【Bucket列表】
【2】创建一个新的Bucket,注意只更改名称,然后保存
【3】读写权限改为公共读
【4】在右上角找到AccessKey点击进入,并创建一个新AccessKey(注意保存好!!!)
【5】根据官方SDK文件,在项目pom.xml文件加载依赖项
【6】在application.yml和application-dev.yml文件中配置阿里云oss,其中application.yml引入,application-dev.yml进行具体配置
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}
endpoint节点在:历史访问路径 - 概览 - 外网访问节点处获取
【7】创建一个专门用于存储阿里云OSS相关配置的Java对象,并自动从应用的配置文件中读取对应的配置值
@Component //指定此类绑定配置文件中的前缀为'sky.alioss'的属性 @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }【8】引入文件上传官方代码,在阿里云oss的文档中获取,而本项目已经提前配置好文件上传相关代码
@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/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName); log.info("文件上传到:{}", stringBuilder.toString()); return stringBuilder.toString(); }【9】创建阿里云属性配置类对属性进行初始化
前边我们在application文件中提前配置好了阿里云oss所需的四个属性endpoint、accessKeyId、accessKeySecret、bucketName
而上述代码中的四个属性未初始化
所以现在需要通过配置类的方式,对四个属性进行初始化(赋值)
/** * 配置类,用于创建AliossUtil对象 */ @Configuration @Slf4j public class OssConfiguration { @Bean // 只有在当前Spring容器中不存在AliOssUtil类型的Bean时,才会创建这个Bean // 这确保了如果其他地方已经配置了AliOssUtil,这里就不会重复创建 @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }【10】四个属性值是如何通过apllication.yml、配置类等方法进行属性绑定的?
- Spring Boot启动,加载application.yml
- 发现AliOssProperties有@ConfigurationProperties,进行属性绑定
- 将AliOssProperties注册为Spring Bean
- 发现OssConfiguration需要创建AliOssUtil Bean,自动注入AliOssProperties到aliOssUtil方法
- 调用方法创建AliOssUtil实例,传入配置值
- AliOssUtil实例被注册到Spring容器中,可供其他组件使用
2、文件上传 - POST接口
【1】controller层
@RestController @Api(tags = "通用接口") @RequestMapping("/admin/common") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; @ApiOperation(value = "文件上传") @PostMapping("/upload") //因为要返回的是文件名路径,因此函数类型为Result<String> public Result<String> upload(MultipartFile file){ //注意MultipartFile后的参数名和前端传参参数名一致 log.info("文件上传:{}",file); try { //原始文件名 String originalFilename = file.getOriginalFilename(); //获取原始文件名的后缀 // 1. originalFilename.lastIndexOf(".") // - 查找字符串中最后一个点号('.')的位置(索引) // 2. originalFilename.substring(位置) // - 从指定的索引位置开始截取字符串,直到字符串末尾 String 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.info("文件上传失败:{}",e); } return Result.error(MessageConstant.UPLOAD_FAILED); } }上传成功!!
3、新增菜品 - POST接口
【1】controller层
/** * 新增菜品 * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO){ log.info("新增菜品:{}",dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); }【2】service层
- 先将DTO->实体类,向菜品表dish插入菜品数据
- 因为insert操作不返回主键id,所以需要在mybatis中设置相关属性让其返回该菜品id,接收菜品id用于填充口味表dish_flavor(插入操作后,才自动生成id,且id不会主动返回,因此需要设置相关属性令其主动返回菜品id)
- 因为口味是一个list集合,所以需要通过遍历将列表中每一个口味都绑定上对应的菜品id,最后在口味表进行批量插入
/** * 新增菜品及对应口味 * @param dishDTO */ @Transactional //因为涉及多表,因此需要保证事务一致性,要不然全成功,要不然全失败 public void saveWithFlavor(DishDTO dishDTO){ //因为DTO数据中包含菜品信息+口味,而我们只需要向菜品表中插入菜品信息 //因此只需要创建一个实体对象,并把相关属性复制进来即可 Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); //向菜品表插入1条数据 dishMapper.insert(dish); //获取insert语句返回的主键值 Long dishId = dish.getId(); //向口味表中插入n条数据 List<DishFlavor> flavorList = dishDTO.getFlavors(); if(flavorList != null && flavorList.size() > 0){ //为flavorList列表里的每一个口味对象,设置它们的菜品ID(dishId) flavorList.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavorList); } }【3】mapper层
1)DishMapper
/** * 插入菜品 * @param dish */ @AutoFill(value = OperationType.INSERT) void insert(Dish dish);2)DishFlavorMapper
/** * 批量插入菜品口味 * @param flavorList */ void insertBatch(List<DishFlavor> flavorList);【4】mybatis文件
1)DishMapper
- useGeneratedKeys="true" keyProperty="id"含义:执行完插入语句后,会将主键值存入id这个属性
- 为什么要加这两个属性?
- 因为口味表dish_flavor中需要菜品id,而菜品插入操作并不会返回主键id,我们却需要让其返回,因此需要加这两个属性,让其返回主键id
<!-- useGeneratedKeys="true" keyProperty="id"含义:执行完插入语句后,会将主键值存入id这个属性--> <!-- 为什么要加这两个属性?因为口味表dish_flavor中需要菜品id,而菜品插入操作并不会返回主键id,我们却需要让其返回,因此需要加这两个属性,让其返回主键id--> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into sky_take_out.dish(name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) values (#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser}) </insert>2)DishFlavorMapper
collection="flavorList": 指定要遍历的集合参数的名字。这里表示遍历一个名为flavorList的集合。
item="df": 为集合中的每一个元素定义一个临时变量名。这里每个元素都可以通过df来引用。
separator=",": 指定每次循环生成的内容之间的分隔符。这里用逗号,分隔,因为在 SQL 的VALUES子句中,多条记录就是用逗号分隔的。<insert id="insertBatch"> insert into sky_take_out.dish_flavor(dish_id, name, value) values <foreach collection="flavorList" item="df" separator=","> (#{df.dishId},#{df.name},#{df.value}) </foreach> </insert>
4、菜品分页查询 - GET接口
【1】controller层
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}",dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }【2】service层
为什么返回的是DishVO,而不是DishPageDTO或Dish?
因为需要返回联合查询的结果
你的
DishVO中有一个关键字段:private String categoryName;数据库中的
dish表只存储了category_id,而前端列表需要展示的是分类的名称,而不是一个冰冷的ID数字。因此,在分页查询时,你的SQL语句很可能是一个多表关联查询(例如
dishLEFT JOINcategory),这样才能一次性查出菜品及其对应的分类名称。
Dish实体类只映射了dish表的结构,它没有categoryName这个字段,无法接收联查的结果。
DishVO在这里的作用就是接收这个联查的结果集,它包含了来自多个表的数据。/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize()); //前端->后端 DTO,后端->数据库(需要处理时)entity,后端->前端 VO Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(),page.getResult()); }【3】mapper层
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);【4】mybatis层
分页查询目的:查询匹配【菜品名name、分类id、菜品状态】中任意若干项的菜品,要求对应菜品展示其【所有菜品信息+对应分类名称】
1)菜品展示其【所有菜品信息+对应分类名称】
select d.*, c.name as categoryName from dish d left outer join category c on d.category_id = c.id上述sql语句含义:查询所有菜品信息+对应分类名称
1、select d.*, c.name as categoryName
d.*:查询dish表的所有字段(d是dish表的别名)。c.name as categoryName:查询category表的name字段,并为其起一个别名categoryName。
2、from dish d
- 从
dish表进行查询,并给这个表起了一个简短的别名d。
3、left outer join category c on d.category_id = c.id
left outer join:左外连接。这意味着会返回左表 (dish) 的所有记录,即使右表 (category) 中没有匹配的记录。category c:连接category表,并为其起别名c。on d.category_id = c.id:连接的条件是dish表的category_id字段等于category表的id字段。2)查询匹配【菜品名name、分类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>完整mybatis代码
<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.* ,c.name as categoryName from sky_take_out.dish d left outer join sky_take_out.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>
5、批量删除菜品 - DELETE接口
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
【1】controller层
/** * 批量删除菜品 * @param ids * @return */ @DeleteMapping @ApiOperation("批量删除菜品") public Result detele(@RequestParam List<Long> ids){ log.info("批量删除:{}",ids); dishService.deleteBatch(ids); return Result.success(); }【2】service层
- 起售的菜品不能删除:需要历获取所有菜品的状态,若为起售,则抛出异常
- 被套餐关联的菜品不能删除:多表联查获取关联套餐的菜品数,若关联数不为空,则说明不能删除
/** * 批量删除菜品 * @param ids */ public void deleteBatch(List<Long> ids){ //1.起售中的菜品不能删除 for (Long id : ids) { //查询前端提交的每一个菜品的状态 Dish dish = dishMapper.getById(id); if(dish.getStatus() == StatusConstant.ENABLE){ throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } //2.被套餐关联的菜品不能删除 List<Long> setmealIdsByDishIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIdsByDishIds != null && setmealIdsByDishIds.size() > 0){ //说明有关联的套餐 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //3.批量删除菜品数据 dishMapper.deleteByIds(ids); //4.批量删除菜品关联的口味数据 dishFlavorMapper.deleteByDishIds(ids); }【3】mapper层
1)DishMapper
/** * 根据ids批量删除菜品 * @param ids */ void deleteByIds(List<Long> ids);2)DishFlavorMapper
/** * 根据ids批量删除对应口味 * @param dishIds */ void deleteByDishIds(List<Long> dishIds);3)SetmealDishMapper
/** * 根据菜品id查询对应套餐id * @param dishIds * @return */ // select setmeal_id from setmeal_dish where dish_id in (1,2,3)用动态sql List<Long> getSetmealIdsByDishIds(List<Long> dishIds); //因为一个菜品可能关联多个套餐,因此用list集合接收【4】mybatis层
1)DishMapper
<delete id="deleteByIds"> delete from sky_take_out.dish where id in <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </delete>2)DishFlavorMapper
<delete id="deleteByDishIds"> delete from sky_take_out.dish_flavor where dish_id in <foreach collection="dishIds" item="dishId" open="(" close=")" separator=","> #{dishId} </foreach> </delete>3)SetmealDishMapper
<!-- 从前端提供的菜品id集合中获取--> <select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from sky_take_out.setmeal_dish where dish_id in <foreach collection="dishIds" item="dishId" open="(" close=")" separator=","> #{dishId} </foreach> </select>
6、根据id查询菜品 - GET接口
为了使修改页面,数据可以回显,我们需要通过菜品id查询菜品信息+对应口味信息
【1】controller层
/** * 根据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层
为什么根据id查询菜品信息不使用sql多表联查?
因为口味表和菜品表是多对多关系,即一个菜品对应多条口味,一个口味也会对应多个菜品,如果使用多表联查,会出现菜品id重复的问题
select d.*, f.name as flavor_name, f.value as flavor_value from dish d left outer join dish_flavor f on d.id = f.dish_id
【3】mapper层
/** * 根据菜品id查询对应口味 * @param dishId * @return */ @Select("select * from sky_take_out.dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId(Long dishId);菜品信息成功回显!
7、修改菜品 - PUT接口
【1】controller层
/** * 修改菜品 * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}",dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }【2】service层
- 因为口味修改包含:口味删除与修改,而用户操作时可能会同时涉及这两个操作,因此我们直接统一先删除所有口味,再重新插入口味
为什么这里不需要xml文件获取菜品id?
- 因为修改菜品接口传入的【菜品id】为必须项,而插入菜品接口传入的【菜品id】为非必须项 所以修改菜品接口的菜品id可以直接从DTO中获取
- 而插入接口的菜品id则需要插入后生成id后返回
/** * 修改菜品 * @param dishDTO */ public void updateWithFlavor(DishDTO 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 -> { //为什么这里不需要xml文件获取菜品id? //因为修改菜品接口传入的【菜品id】为必须项,而插入菜品接口传入的【菜品id】为非必须项 //所以修改菜品接口的菜品id可以直接从DTO中获取 //而插入接口的菜品id则需要插入后生成id后返回 dishFlavor.setDishId(dishDTO.getId()); }); } dishFlavorMapper.insertBatch(flavors); }【3】mapper层
/** * 修改菜品 * @param dish */ @AutoFill(value = OperationType.UPDATE) void update(Dish dish);【4】mybatis文件
<update id="update"> update sky_take_out.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>
8、菜品起售停售 - POST接口
【1】controller层
/** * 菜品起售停售 * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("起售停售菜品") public Result startOrStop(@PathVariable Integer status, Long id){ log.info("起售停售菜品:{},{}",status,id); dishService.startOrStop(status,id); return Result.success(); }【2】service层
修改菜品状态其实就是把状态和id值填进去,让其动态更新
注:xml文件update是动态更新,像name、price这种没有修改的,其原来的值就不会变,只修改更新的status状态/** * 菜品起售停售 * @param status * @param id */ public void startOrStop(Integer status, Long id) { //修改菜品状态其实就是把状态和id值填进去,让其动态更新 //注:xml文件update是动态更新,像name、price这种没有修改的,其原来的值就不会变 //只修改更新的status状态 Dish dish = Dish.builder() .status(status) .id(id) .build(); dishMapper.update(dish); }
菜品模块开发结束~撒花!

更多推荐













































所有评论(0)