新增菜品
2.1 需求分析与设计
2.1.1 产品原型
后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。
新增菜品原型:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
业务规则:
-
菜品名称必须是唯一的
-
菜品必须属于某个分类下,不能单独存在
-
新增菜品时可以根据情况选择菜品的口味
-
每个菜品必须对应一张图片
2.1.2 接口设计
根据上述原型图先粗粒度设计接口,共包含3个接口。
接口设计:
-
根据类型查询分类(已完成)
-
文件上传
-
新增菜品
接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
1. 根据类型查询分类
2. 文件上传
3. 新增菜品
2.1.3 表设计
通过原型图进行分析:
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
表名 | 说明 |
---|---|
dish | 菜品表 |
dish_flavor | 菜品口味表 |
1). 菜品表:dish
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 菜品名称 | 唯一 |
category_id | bigint | 分类id | 逻辑外键 |
price | decimal(10,2) | 菜品价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 菜品描述 | |
status | int | 售卖状态 | 1起售 0停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
2). 菜品口味表:dish_flavor
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 口味名称 | |
value | varchar(255) | 口味值 |
2.2 代码开发
2.2.1 文件上传实现
因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。
文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
-
直接将图片保存到服务的硬盘(springmvc中的文件上传)
-
优点:开发便捷,成本低
-
缺点:扩容困难
-
-
使用分布式文件系统进行存储
-
优点:容易实现扩容
-
缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
-
-
使用第三方的存储服务(例如OSS)
-
优点:开发简单,拥有强大功能,免维护
-
缺点:付费
-
在本项目选用阿里云的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());} }
代码解析(细节-难):
这里不使用自动注入AliOssProperties的原因是因为如果使用自动注入,那么如果容器中没有AliOssProperties对象,那么就会报错,因为根据spring的依赖注入顺序,配置类的创建顺序是先创建配置类,再创建其他类,所以如果使用自动注入,那么如果容器中没有AliOssProperties对象,那么就会报错,因为根据spring的依赖注入顺序,配置类的bean一般先创建。
其中,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语句生成的主键值//使用mybatis返回来的id为菜品id赋值,这一步不要忘记了Long dishId = dish.getId();//保存完之后原先的id值被清空了,这里也是个小细节,后面再取id就取不出来了List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);}); //向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);//后绪步骤实现}} }
代码解析(细节-难):
向菜品表插入1条数据
使用mybatis返回来的id为菜品id赋值,这一步不要忘记了
保存完之后原先的id值被清空了,这里也是个小细节,后面再取id就取不出来了
向口味表插入n条数据
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"> <insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})</insert> </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>
小结
1.
无依赖时:配置类可能与其他 Bean 并行初始化,顺序不确定。
有依赖时:
如果其他 Bean 依赖配置类中的
@Bean
,Spring 会先创建配置类中的 Bean。如果配置类依赖其他 Bean(例如通过方法参数注入),则优先创建被依赖的 Bean。
2.mybatis主键返回的注解的细节
法一 <insert id="insert" useGeneratedKeys="true" keyProperty="id"> xxx </insert>
法二 @Options(useGeneratedKeys = true, keyProperty = "id")主键返回注解
tip:
使用mybatis返回来的id为对象id赋值,这一步切记不要忘记了,否则返回为null
Long dishId = dish.getId();
取完dishId之后类里的id值被清空了这里也是个小细节,后面再取id就取不出来了