(苍穹外卖 DAY5)新增菜品功能、阿里云OSS云存储服务、上传文件接口
在学习苍穹外卖过程中,弹幕常有 “为什么我打不开?为什么我没有输出?”的疑问,针对这些我也在学习过程中同样遇到的问题,万分感激在弹幕中找到了答案,并作出这系列汇总。本文内容是基于弹幕对苍穹外卖项目的实施与补充,仅供学习与分享之用,如有侵权请联系删除~
写在前面
在学习苍穹外卖过程中,弹幕常有 “为什么我打不开?为什么我没有输出?”的疑问,针对这些我也在学习过程中同样遇到的问题,万分感激在弹幕中找到了答案,并作出这系列汇总。本文内容是基于弹幕对苍穹外卖项目的实施与补充,仅供学习与分享之用,如有侵权请联系删除~
2024-11-14
目录
DishService& DishServiceImpl :
DishService的saveWithFlavor方法代码
DishServiceImpl 的saveWithFlavor方法代码
新增菜品功能
新增菜品功能需求分析和设计
分析 => 查看产品原型
思考:新增菜品,应该对数据进行何种限制?和分类功能的联合?用了几个接口?

- 保存新菜品 ==> 添加菜品数据接口
- 选择菜品分类 ==> 查询菜品分类接口
- 上传菜品图片 ==> 上传图片文件接口(why?-- 前端与后端间需要交换数据(图片))

设计 ==> 接口文档
新增菜品时需要选择分类 ==> 分类查询接口(已完成,直接调用即可)
新增菜品接口

思考:
用何种请求方式?传参数据类型? 返回值?==> post请求、请求体json传参、返回code
前端页面可看传参数据类型👇:
文件上传接口

思考:
用何种请求方式? ==> post请求
如何传参? ==> 前端传的是文件数据 -- 参数值Content-Type:multipart/from-data -- 在请求体用body把文件传过来
返回值? ==> data(文件上传OSS路径----为了前端请求图片回显)
Content-Type:multipart/from-data:
当我们发送文件上传请求时,浏览器或前端应用会告诉服务器,请求包含文件数据。
这个内容类型会将文件和其他字段(如菜品名称)分开处理,每个部分都会被编码成独立的部分,用特殊的边界符(boundary)分开。文件内容会被编码成二进制数据,并作为一部分放在请求体内传送给服务器。
这样,前端能够在同一个请求体内,发送文件和其他数据(例如,图片文件或文本数据),而且每个字段都有自己独立的标识。
注意:
文件上传时,默认的 Content-Type 是 application/x-www-form-urlencoded,该格式不支持文件上传,只适用于提交文本数据。而 multipart/form-data 才是文件上传时需要使用的格式,它不仅能传送文件名,还能传送文件的实际内容,所以,如果不使用 multipart/form-data,浏览器将不能正确地发送文件内容,而只能发送文件的文件名,而不是实际的文件内容。)
前端页面可看传参数据类型👇:


商品图片传到咱们的服务器后,我们写个AliOssUtil类,将图片重命名,然后调阿里云OSS的接口上传到阿里云OSS云服务器中,最后存在我们数据库里的就是这个图片在的OSS链接
为什么不用数据库存储?要存云端?(AI生成的非重点)
1. 性能与效率
数据库性能:
① 数据库如果存储大量的图片,会导致数据库变得非常庞大,从而影响查询性能。
② 数据库需要更多的I/O操作来存储和读取大文件(如图片),这会显著降低性能。
应用性能:
① 从数据库读取大文件会占用大量的网络带宽,影响应用的响应时间。
② 部分云存储可以利用CDN(内容分发网络)来加速图片的加载,提高用户体验。
2. 可扩展性
存储容量:
① 云存储服务可以轻松扩展存储容量,并且可以处理大量的文件存储需求。
② 数据库扩展通常更加复杂和昂贵,特别是当需要存储大量的图片数据时。
处理能力:
① 云存储服务通常具有内置的高可用性和灾难恢复功能,可以更好地处理大规模的数据存储和访问需求。
② 数据库扩展不仅限于存储空间,还需要考虑数据库性能和查询效率等问题。
3. 成本
存储成本:
① 云存储服务通常提供按需计费的模式,根据实际使用的存储空间和带宽付费,成本更低且更灵活。
② 数据库存储大量图片不仅需要更大的存储容量,还可能需要更多的数据库实例或更高性能的数据库来处理数据,成本较高。
4.安全性:
① 云存储服务提供完善的安全措施,如数据加密、访问控制、审计日志等。
② 数据库需要额外的安全措施来保护图片数据,增加了管理的复杂性。
5. 职责分离:
① 数据库主要负责存储和管理结构化数据,而云存储主要负责管理文件和非结构化数据。将两者分离可以让系统设计更加清晰,职责更加明确。
数据库设计

根据产品原型分析数据:
① 菜品信息插入到dish表(新菜品要设置一个分类(category_id))
② 如果添加了口味做法,还需要向dish_flavor表插入数据。
所以在新增菜品时,涉及到两个表(dish菜品表和dish_flavor口味表):

逻辑外键:
逻辑外键(在应用层面通过代码逻辑实现的外键关系)
物理外键(数据库中的实际外键约束)
在开发中,尽量避免使用物理外键而推荐使用逻辑外键,这种做法主要基于以下几个方面:
1. 性能影响:
逻辑外键不会引起数据库层额外检查(数据完整性)和约束,减少了数据库的性能开销。
2. 数据库迁移和重构:
逻辑外键在代码层面实现,使数据库结构更加灵活,易于重构和迁移。
3. 并发事务:
在高并发场景下,逻辑外键减少了数据库层面的锁争用问题。
4. 数据一致性:
逻辑外键允许通过应用层面的一致性检查和事务管理(例如分布式事务、最终一致性策略等),更加适应分布式系统和微服务架构的需求。
(补充说明:分布系统中,不同服务器布置的数据库往往是独立的,使物理外键的联系难以保证。)
5. 代码维护和测试
逻辑外键在代码中可以明确体现业务逻辑和数据关系,有助于代码的可读性和维护性。
在测试环境中,逻辑外键更易于模拟和控制,开发人员可以通过代码方便地进行数据关系的测试。
实现逻辑
准备:创建阿里云OSS
① 创建阿里云ID
进入aliyun.com,注册账号并登录
② 进入OSS控制台
在搜索栏输入 对象存储,点击 立即开通
(我开过了,没有图片)

③ 创建budket
名称 :自己定
地域 :自己定

修改访问权限(一定要是公共读)


④ 创建密钥

千万记住密钥哦!!!!!后续无法查看的

⑤ 在dev进行配置👉调服务
路径:src/main/resources/application.yml

将数据修改为自己的
endpoint 👇 cn后面的拼音换为自己选的地域,比如我的是guangzhou

access-key-id & access-key-secret 👇 填入自己的密钥
bucket-name 👇 填入自己定义的名称

⑥ 编写配置类,创建实例并注入spring
Properties类前文介绍过咯

路径:src/main/java/com/sky/config/OssConfiguration.java
通过OSS配置类创建配置项,在项目启动时就会加载出来,通过spring管理
@Configuration // 1. 该注解表示这是一个配置类,它会被 Spring 容器扫描并注册为一个配置类。配置类通常用于定义 beans 和进行相关配置。
@Slf4j // 2. 该注解来自 Lombok,用于自动生成一个名为 'log' 的日志记录器对象(Logger)。该对象可以用于在类中进行日志打印。
public class OssConfiguration { // 3. 定义一个类 `OssConfiguration`,它将包含阿里云 OSS 工具类的配置方法。
/**
* 通过 Spring 管理对象(即将对象注入到 Spring 容器中),以便在整个应用中可以通过依赖注入获取。
* @param aliOssProperties // 4. 这是方法的参数,表示一个包含阿里云 OSS 配置的类(如:endpoint, accessKeyId 等),它会通过 Spring 注入(Dependency Injection)。
* @return AliOssUtil // 5. 方法的返回类型是 `AliOssUtil`,表示一个工具类实例,用于与阿里云 OSS 进行交互。
*/
@Bean // 6. `@Bean` 注解表示这个方法返回的对象会被注册到 Spring 容器中,作为一个 Bean 管理。
@ConditionalOnMissingBean // 7. `@ConditionalOnMissingBean` 注解表示只有在 Spring 容器中没有该类型的 Bean(即 `AliOssUtil`)时,才会创建并返回新的 `AliOssUtil` 实例。这避免了重复创建相同类型的 Bean。
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { // 8. 定义了一个方法 `aliOssUtil`,该方法会根据提供的 `AliOssProperties` 配置信息创建并返回 `AliOssUtil` 实例。
log.info("开始创建阿里云OSS工具类..."); // 9. 使用 Lombok 生成的日志对象 `log` 输出一条信息,表示正在创建阿里云 OSS 工具类。适合用于调试和追踪代码执行。
// 10. 创建并返回 `AliOssUtil` 实例,构造函数需要传入 OSS 的相关配置信息,这些配置信息来自 `AliOssProperties` 对象。
return new AliOssUtil(
aliOssProperties.getEndpoint(), // 11. 获取 `aliOssProperties` 中的 `endpoint` 配置,用于连接 OSS 的服务端。
aliOssProperties.getAccessKeyId(), // 12. 获取 `aliOssProperties` 中的 `accessKeyId`,阿里云的访问密钥 ID,用于身份认证。
aliOssProperties.getAccessKeySecret(), // 13. 获取 `aliOssProperties` 中的 `accessKeySecret`,阿里云的访问密钥 Secret,用于身份认证。
aliOssProperties.getBucketName() // 14. 获取 `aliOssProperties` 中的 `bucketName`,OSS 的存储空间名称。
); // 15. 返回创建好的 `AliOssUtil` 实例,Spring 会管理这个实例,并提供依赖注入功能。
}
}
为什么要创建配置类?
(以OssConfiguration为例)
目的:
为了在 Spring 应用程序中集中管理和配置各类对象的创建,特别是第三方服务或工具类的配置。
使用配置类来创建对象、注入依赖并注册为 Spring 的 Bean,从而使得这些对象能够被整个应用程序的其他组件访问和使用。
通过这种方式,Spring 能够根据配置自动生成和管理所需的对象。
- OssConfiguration配置类 创建一个 AliOssUtil 对象,并通过 Spring 容器来管理它
- 通过从 AliOssProperties 中读取阿里云 OSS 的配置信息来初始化 AliOssUtil
配置类的优势:
- 集中配置管理:将与阿里云 OSS 相关的所有配置信息集中管理,使得第三方服务的连接、初始化等操作更加简洁,不需要在应用的各个位置重复配置。
- 解耦和可扩展性:可以轻松替换不同的 AliOssUtil 实现,或者根据不同的环境(如开发环境、生产环境)进行配置切换。如后续需要支持其他云存储服务(如腾讯云 COS 或 AWS S3),只需编写新的配置类并注入新的工具类,而不需要修改现有的业务代码。
- 自动化和条件注入:确保了 Spring 只会在容器中创建并注入一个 AliOssUtil Bean 的实例,避免了重复创建 AliOssUtil 对象,保持了容器的干净和高效。避免了冗余的配置,降低了不必要的复杂度。
- 与 Spring 生态系统的整合:通过 @Bean 注解将对象注册到 Spring 容器中,使得 AliOssUtil 对象可以通过依赖注入(DI)在整个应用中使用。无需在每个类中new新的 AliOssUtil 实例,而是通过注入方式实现自动化管理。(编写错误更少)
接口① :上传文件接口
前端浏览器提交图片👉后端获取服务 👉 调官方接口上传到阿里云
创建新CommonController类
for:获取图片是一个全应用程序可用的服务 👉 写在通用接口里
路径:src/main/java/com/sky/controller/admin/CommonController.java
- 添加@RestController注解在MVC中声明Controller类
- 添加@RequestMapping("/admin/common")请求路径
- API注解,在swagger测试中显示模块名称
- 使用@Slf4j注解,可用于管理输出日志
- @Autowired注入aliOssUtil调用阿里云工具类

CommonController.upload()
实现逻辑 👇
- 根据接口文档的路径和请求方法 ==> 定义注解
- 定义返回值 ==> <String> == 返回图片的云端的绝对路径(URL)
- 使用MultipartFile类型接收前端传来的参数(SpringMVC已完成对传来的数据进行封装)
- 调整参数 ==> 调用aliOssUtil阿里云工具类
- 获取前端传来的文件名用UUID方法重命名
- 将文件转为byte类型
- 将byte类型的文件和文件名作为参数,调用aliOssUtil的upload方法
- 在工具类内实现上传,并返回文件上传至云端的路径
- try,catch捕获异常
文件处理可能出现的异常: file.getBytes() 可能因为文件太大或其他 I/O 错误抛出 IOException。
网络请求可能抛出的异常:aliOssUtil.upload(file.getBytes(), objectName) 这个方法可能涉及到网络请求或云存储操作,可能会遇到网络中断、目标服务器不可达、权限不足等问题,导致上传失败,这些错误通常会抛出异常。
(简言之,上传图片时断网会导致上传失败,出现异常。)
上传成功,返回图片云端的路径,用于回显图片
aliOssUtil阿里云工具类
路径:src/main/java/com/sky/utils/AliOssUtil.java
aliOssUtil对象在配置类里已完成初始化。
- 注解:(老生常谈)
- 该类 AliOssUtil 被标记为 @Data,这意味着 Lombok 会自动生成该类的 getter、setter、toString、equals、hashCode 等方法。
- @AllArgsConstructor :生成一个包含所有字段的构造函数。
- @Slf4j :为类添加日志记录功能,方便后续记录操作信息和异常日志。
- 类的成员变量包括:
- endpoint: OSS 的服务地址。
- accessKeyId: 用于身份认证的阿里云 Access Key ID。
- accessKeySecret: 用于身份认证的阿里云 Access Key Secret。
- bucketName: 要上传文件的目标 OSS 桶(Bucket)的名称。
- upload 方法
- 该方法实现了文件上传的功能,它接收两个参数:
- bytes: 文件的字节数组 , objectName: 上传到 OSS 后的文件名称。
- 创建 OSS 客户端:
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
- 使用 OSSClientBuilder 构建一个 OSS 客户端实例,传入 endpoint、accessKeyId 和 accessKeySecret,这是连接阿里云 OSS 的基础设置。
- 文件上传操作:
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
- 在 try 块中,使用 ossClient.putObject() 方法上传文件。这个方法将文件的字节数组(bytes)封装成 ByteArrayInputStream,然后传到指定的 OSS 桶(bucketName)中。
- 异常处理:
- OSSException: 捕获阿里云 OSS 返回的异常,表示请求成功到达 OSS,但由于某些原因被拒绝。捕获后会打印出错误信息、错误码、请求 ID 和主机 ID。
- ClientException: 捕获客户端异常,表示客户端在与 OSS 通信时遇到的问题,例如网络不可达或权限问题。
- 资源清理:
- 在 finally 块中,确保在完成文件上传操作后关闭 OSS 客户端,以释放资源。即使在异常发生时也会执行清理操作。
- 生成文件访问 URL:
- 文件上传到 OSS 后,会通过构造 URL 生成文件的外部访问路径。URL 格式👇
//文件访问路径规则 https://BucketName.Endpoint/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName);- 日志记录:
- 使用 log.info() 打印出文件上传后的访问 URL,帮助开发者了解文件上传的结果。
- 返回文件 URL:
- 最后,方法返回生成的文件访问 URL,供CommonController.upload()使用。
MessageConstant类:
路径:src/main/java/com/sky/constant/MessageConstant.java
普普通通常量类
测试
swagger对文件上传测试不友好 ==> 前后端连调


阿里云的bucket里可见上传的文件 👇

接口② :新增菜品接口
前端传来的数据封装进DTO 👉 口味封装为list数组
路径:src/main/java/com/sky/dto/DishDTO.java

DishController:
路径:src/main/java/com/sky/controller/admin/DishController.java
- 接收菜品DTO参数
- 将DTO作为参数,调用(稍后定义的)DishService的saveWithFlavor方法
DishService& DishServiceImpl :
操作菜品时与口味相关,所以方法名起的贴一点,提高可读性。
路径:
- src/main/java/com/sky/service/DishService.java
- src/main/java/com/sky/service/impl/DishServiceImpl.java
saveWithFlavor() 实现逻辑
- 创建一个新的Dish实体对象
- 将DishDTO的属性复制到该实体对象中
- 将该实体作为参数,调用mapper的insert方法向dish表插入一条数据
- 返回该数据的键值 👉 根据该键值处理口味表
- 通过 dishDTO.getFlavors() 获取到菜品的口味列表
- 判空口味列表,非空则对每个口味对象进行遍历,并设置 dishId
- 调用dishFlavorMapper插入口味数据
DishService的saveWithFlavor方法代码
DishServiceImpl 的saveWithFlavor方法代码
Lambda 表达式:
flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);});
- dishFlavor 是 flavors 列表中的每个元素,它代表了当前遍历到的口味对象。
- -> 是 Lambda 表达式的分隔符,表示“对于每个 dishFlavor 执行下面的操作”。
- { dishFlavor.setDishId(dishId); } 是 Lambda 表达式的主体,它表示为每个 dishFlavor 对象调用 setDishId(dishId) 方法,将 dishId 设置为口味的 dishId。
Mapper&Mapper.xml
对象与数据库的操作,需要处理菜品数据和口味数据,涉及2个mapper
DishMapper&DishMapper.xml代码
定义insert方法插入菜品数据
如何获取新增数据的id值?
- useGeneratedKeys="true":这个属性告诉 MyBatis,在插入数据时,数据库自动生成主键值时将其返回给 Java 对象。通常用于自增主键的情况。
- keyProperty="id":这个属性指定了插入数据后,数据库自动生成的主键值应该存储到哪个对象的属性中。这里是将数据库生成的主键值存储到插入对象的 id 属性中。
DishFlavorMapper&DishMapper.xml代码
测试
前后端联调测试


魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐









所有评论(0)