苍穹外卖项目实战阿里云 OSS 文件上传完整开发指南技术栈Spring Boot 3.4.4 阿里云 OSS SDK 3.17.4 Maven 多模块前置依赖员工管理模块JWT 认证、全局异常处理已就绪适用场景菜品图片上传、套餐图片上传、店铺Logo等所有需要文件存储的场景目录一、业务背景与架构设计二、接口总览三、阿里云 OSS 准备工作四、配置文件五、核心工具类 AliOssUtil六、Controller 层七、完整数据流解析八、接口测试示例九、踩坑记录与注意事项十、后续复用场景一、业务背景与架构设计为什么需要 OSS苍穹外卖中菜品图片、套餐图片等文件需要持久化存储。直接存服务器本地磁盘存在以下问题问题说明服务器磁盘空间有限图片占用空间大长期积累会撑爆部署多台机器时文件不同步A 机上传了B 机访问不到无法直接被浏览器访问需要额外写下载接口备份/迁移麻烦换服务器要搬全部文件架构图前端Vue3/小程序 │ │ POST /admin/common/upload (multipart/form-data) │ file: 图片文件 ▼ ┌─────────────────────────────┐ │ CommonController │ │ 接收 MultipartFile │ │ → 调用 AliOssUtil.upload()│ └──────────┬──────────────────┘ │ │ byte[] UUID文件名 ▼ ┌─────────────────────────────┐ │ AliOssUtil (工具类) │ │ 构建 OSSClient │ │ → ossClient.putObject() │ │ → 返回 URL 字符串 │ └──────────┬──────────────────┘ │ │ HTTPS URL ▼ ┌─────────────────────────────┐ │ 阿里云 OSS │ │ Bucket: dddbucket │ │ Region: 北京 │ │ 文件公开可读 │ └─────────────────────────────┘二、接口总览序号请求方式接口路径功能说明Content-Type1POST/admin/common/upload通用文件上传图片/文档等multipart/form-data⚠️ 注意这是一个通用接口所有需要上传文件的模块菜品、套餐等都复用这一个接口。三、阿里云 OSS 准备工作3.1 注册与开通步骤操作说明1登录 阿里云官网注册账号并完成实名认证2开通 OSS 服务搜索对象存储OSS点击开通按量付费新用户有免费额度3创建 Bucket进入 OSS 控制台 → 创建 Bucket → 选择地域、设置权限3.2 Bucket 关键设置创建 Bucket 时注意以下配置配置项建议值原因Bucket 名称全局唯一如dddbucket用于拼接访问 URL地域选择离用户近的区域如北京影响上传/下载速度读写权限公共读上传后 URL 可直接在浏览器打开版本控制不开启简单场景不需要为什么选公共读因为返回给前端的 URL 需要能直接被img src...访问。如果设为私有每次访问都需要签名增加复杂度。3.3 获取 AccessKey步骤操作路径1进入 RAM 控制台2创建 AccessKey或使用已有的3保存AccessKey ID和AccessKey Secret⚠️安全建议不要使用主账号的 AccessKey建议创建 RAM 子账号仅授予AliyunOSSFullAccess权限。四、配置文件application.yml关键配置sky:jwt:admin-secret-key:itcast_sky_take_out_2026_secret_key_1234567890abcdefadmin-ttl:7200000admin-token-name:token# 阿里云 OSS 配置 oss:endpoint:oss-cn-beijing.aliyuncs.com# OSS 服务端点含地域bucket-name:dddbucket# Bucket 名称全局唯一access-key-id:LTAI5tAphPRow2yWbxBwvP9L# AccessKey IDaccess-key-secret:6YORxuksSDSwd9N64Z8yicOiObNemV# AccessKey Secret配置项说明配置项示例值用途endpointoss-cn-beijing.aliyuncs.comOSS 服务地址决定数据存储在哪个区域bucket-namedddbucket存储桶名称会出现在访问 URL 中access-key-idLTAI5t...身份认证 IDaccess-key-secret6YORx...身份认证密钥⚠️ 不可泄露Maven 依赖pom.xml!-- 阿里云 OSS SDK --dependencygroupIdcom.aliyun.oss/groupIdartifactIdaliyun-sdk-oss/artifactIdversion3.17.4/version/dependency!-- Spring Boot Configuration Processor支持 ConfigurationProperties 提示--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency五、核心工具类 AliOssUtil完整代码packagecom.sky.utils;importcom.aliyun.oss.ClientException;importcom.aliyun.oss.OSS;importcom.aliyun.oss.OSSClientBuilder;importcom.aliyun.oss.OSSException;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;importjava.io.ByteArrayInputStream;importjava.io.InputStream;/** * 阿里云 OSS 文件上传工具类 * * 核心职责 * 1. 封装阿里云 SDK 的客户端构建与连接管理 * 2. 提供 upload() 方法供所有 Controller 调用 * 3. 自动生成 UUID 文件名防止文件名冲突和中文乱码 */DataNoArgsConstructorAllArgsConstructorComponentConfigurationProperties(prefixsky.oss)// ← 关键注解将 yml 配置注入到字段publicclassAliOssUtil{privateStringendpoint;// OSS 端点privateStringaccessKeyId;// AccessKey IDprivateStringaccessKeySecret;// AccessKey SecretprivateStringbucketName;// Bucket 名称/** * 文件上传 * * param bytes 文件字节数组从 MultipartFile.getBytes() 获取 * param objectName 上传后的文件名含路径前缀如 2024/06/01/uuid.png * return 文件在 OSS 中的完整可访问 URL */publicStringupload(byte[]bytes,StringobjectName){// 第1步构建 OSS 客户端 // 使用 Builder 模式创建客户端实例OSSossClientnewOSSClientBuilder().build(endpoint,accessKeyId,accessKeySecret);try{// 第2步执行上传 // 将字节数组包装成输入流上传到指定 Bucket 的指定路径InputStreaminputStreamnewByteArrayInputStream(bytes);ossClient.putObject(bucketName,objectName,inputStream);// 第3步拼接待返回的访问 URL // 格式https://{bucketName}.{endpoint}/{objectName}returnhttps://bucketName.endpoint/objectName;}catch(OSSExceptionoe){// OSS 服务端异常如 Bucket 不存在、权限不足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());thrownewRuntimeException(OSS 上传失败oe.getErrorMessage());}catch(ClientExceptionce){// 客户端异常如网络不通、参数非法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());thrownewRuntimeException(OSS 客户端异常ce.getMessage());}finally{// 第4步关闭客户端必须// OSSClient 内部维护了 HTTP 连接池不关闭会导致连接泄漏if(ossClient!null){ossClient.shutdown();}}}}关键注解解析注解作用必要性Component将类注册为 Spring Bean支持Autowired注入✅ 必须ConfigurationProperties(prefix sky.oss)将 YAML 中sky.oss.*的值自动绑定到同名字段✅必须漏掉会导致字段全为nullDataLombok 自动生成 getter/setter✅ 必须配置绑定依赖 setterNoArgsConstructor/AllArgsConstructorLombok 生成构造器✅ Spring 反射需要无参构造URL 拼接规则详解配置值 bucketName dddbucket endpoint oss-cn-beijing.aliyuncs.com objectName 6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx 拼接结果 https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx ├────────┤├─────────────────────────┤├──────────────────────────────┤ bucket名 . endpoint 文件路径六、Controller 层完整代码packagecom.sky.controller.admin;importcom.sky.result.Result;importcom.sky.utils.AliOssUtil;importio.swagger.annotations.Api;importio.swagger.annotations.ApiOperation;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.multipart.MultipartFile;importjava.util.UUID;/** * 通用接口控制器 * * 包含文件上传等通用功能不隶属于特定业务模块。 * 所有 /admin/** 路径均受 JWT 拦截器保护。 */RestControllerRequestMapping(/admin/common)Slf4jApi(tags通用接口)publicclassCommonController{AutowiredprivateAliOssUtilaliOssUtil;/** * 文件上传 * * param file 上传的文件表单字段名必须为 file * return OSS 文件访问 URL */PostMapping(/upload)ApiOperation(文件上传)publicResultStringupload(MultipartFilefile){log.info(文件上传{},file.getOriginalFilename());try{// 1. 校验文件是否为空if(filenull||file.isEmpty()){returnResult.error(上传文件不能为空);}// 2. 获取原始文件扩展名如 .png / .jpg / .xlsxStringoriginalFilenamefile.getOriginalFilename();// 例模块备份 9 (1).pngStringextensionoriginalFilename.substring(originalFilename.lastIndexOf(.));// .png// 3. 生成 UUID 新文件名防止重名覆盖 解决中文乱码问题// 例6540ff10-7ae2-40c3-9c49-58fe10908706.pngStringobjectNameUUID.randomUUID().toString()extension;// 4. 调用工具类上传到 OSSStringfilePathaliOssUtil.upload(file.getBytes(),objectName);// 5. 返回可访问的 URL 给前端returnResult.success(filePath);}catch(Exceptione){log.error(文件上传失败,e);returnResult.error(文件上传失败e.getMessage());}}}请求与响应格式请求POST http://localhost:8080/admin/common/upload Content-Type: multipart/form-data; boundary----WebKitFormBoundary token: eyJhbGciOiJIUzI1NiJ9... ------WebKitFormBoundary Content-Disposition: form-data; namefile; filename测试图片.png Content-Type: image/png 二进制文件数据 ------WebKitFormBoundary--成功响应{code:1,msg:success,data:https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.png}失败响应{code:0,msg:上传文件不能为空,data:null}七、完整数据流解析从前端到 OSS 的完整链路┌──────────┐ ┌──────────────────┐ ┌────────────────┐ ┌───────────┐ │ 前端页面 │ ──→ │ CommonController │ ──→ │ AliOssUtil │ ──→ │ 阿里云 OSS │ │ 选择文件 │ │ │ │ │ │ │ └──────────┘ └──────────────────┘ └────────────────┘ └───────────┘ 步骤详情 ① 前端选择文件构造 multipart/form-data 请求 ↓ ② Spring 接收 MultipartFile字段名 file ↓ ③ 获取原始文件名 → 截取后缀名.png ↓ ④ 生成 UUID 作为新文件名 → 避免中文乱码和文件名冲突 ↓ ⑤ 调用 MultipartFile.getBytes() 转为字节数组 ↓ ⑥ AliOssUtil 构建 OSSClient使用 yml 中的 AK/SK ↓ ⑦ 将字节数组写入 InputStream 并调用 putObject() ↓ ⑧ 拼接 URLhttps://{bucket}.{endpoint}/{uuidext} ↓ ⑨ 通过 Result.success(url) 返回给前端 ↓ ⑩ 前端拿到 URL存入数据库的 image 字段后续菜品/套餐新增时UUID 重命名的作用问题不使用 UUID使用 UUID中文乱码❌ “宫保鸡丁.png” 在 URL 中可能编码异常✅a1b2c3d4.png纯英文文件覆盖❌ 两张图同名会互相覆盖✅ 全球唯一不会冲突安全性❌ 暴露原始文件名信息✅ 无法猜测文件路径八、接口测试示例测试前置条件项目已启动端口 8080已通过登录接口获取有效 JWT Token阿里云 OSS 已正确配置endpoint、bucket、AK/SKHeader 中携带 token8.1 使用 Postman / Apifox 测试操作步骤步骤操作说明1选择POST方法请求方式2输入 URLhttp://localhost:8080/admin/common/upload3设置 Headertoken: 你的JWT4Body 选form-data切换到表单模式5添加 Keyfile类型选File字段名必须是file6Value 处选择本地文件点击上传任意图片7点击 Send 发送请求—成功响应{code:1,msg:success,data:https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx}8.2 使用 Swagger UI 测试访问http://localhost:8080/doc.html Knife4j找到「通用接口」→ 「文件上传」在线上传测试。8.3 验证上传结果在浏览器中直接访问返回的 URL确认可以看到上传的文件。九、踩坑记录与注意事项 P0 — 必须解决#问题现象错误信息原因分析解决方案1MultipartFile 为 nullNullPointerException: Cannot invoke getOriginalFilename()because “file” is null前端请求未使用multipart/form-data格式或字段名不是file① 前端确保用 FormData 上传② 字段名必须匹配RequestParam(file)或方法参数名2AccessKey id should not be nullInvalidCredentialsException: Access key id should not be null or empty.AliOssUtil缺少ConfigurationProperties(prefix sky.oss)注解导致 4 个配置字段全部为 null加上该注解光有Component不够它不会自动读取 yml 配置 P1 — 功能影响#问题现象原因解决方案3OSSClient 连接泄漏高并发下频繁上传导致服务变慢甚至 OOM每次build()创建的新客户端必须在finally中调用shutdown()4Bucket 权限问题上传成功但浏览器访问 URL 返回 403Bucket 默认是私有的5Endpoint 格式错误UnknownHostExceptionendpoint 写成了https://oss-cn-beijing.aliyuncs.com带了协议头 P2 — 规范建议#建议说明6文件大小限制Spring Boot 默认限制单文件 1MB、总请求 10MB可在 yml 中调大spring.servlet.multipart.max-file-size10MB7文件类型校验当前实现允许上传任何类型生产环境应校验 MIME 类型如只允许 image/*8AccessKey 安全绝对不要把 AK/SK 写死在代码里或提交到 Git必须放在配置文件或环境变量中9UUID 原始扩展名组合不要丢弃原始扩展名浏览器根据扩展名判断如何渲染.jpgvs.txt表现完全不同10OSSClient 是否应该做成单例当前每次上传都 build 一个新客户端性能略差但简单安全。高并发场景可用单例 连接池优化初学阶段不建议过早优化十、后续复用场景本模块完成后以下模块将直接复用/admin/common/upload接口模块用途说明复用方式菜品管理新增/编辑菜品时上传菜品图片先调上传接口拿 URL → 再把 URL 存入 dish.image 字段套餐管理新增/编辑套餐时上传套餐图片同上URL 存入 setmeal.image 字段店铺设置店铺 Logo 等如有同理典型前端交互流程以新增菜品为例① 用户在表单中选择图片文件 ↓ ② 前端先调用 POST /admin/common/upload ↓ ③ 后端返回 URLhttps://dddbucket.oss-cn-beijing.aliyuncs.com/xxx.png ↓ ④ 前端将 URL 填入表单的 image 字段 ↓ ⑤ 用户填写其他菜品信息名称、价格、分类等点击提交 ↓ ⑥ 前端调用 POST /admin/dishJSON body 中包含 image: https://... ↓ ⑦ 后端将完整数据写入 dish 数据库表 ↓ ⑧ 前端展示菜品列表时img :srcdish.image 直接从 OSS 加载图片核心思路上传接口只负责把文件放到 OSS 并返回 URL具体这个 URL 怎么用、存到哪张表由各个业务模块自己决定。十一、涉及的新增文件清单文件路径所属模块说明AliOssUtil.javasky-commonutils包OSS 工具类核心上传逻辑CommonController.javasky-servercontroller.admin包通用控制器文件上传入口application.ymlsky-serverresources新增sky.oss.*配置节pom.xmlsky-server 或父 pom新增 aliyun-sdk-oss 依赖本文基于苍穹外卖项目实战整理紧接分类管理模块之后。OSS 是独立的基础能力模块不依赖任何业务表。掌握后将直接服务于菜品管理和套餐管理模块的开发。
苍穹外卖项目实战:阿里云 OSS 文件上传完整开发指南
发布时间:2026/6/4 1:45:07
苍穹外卖项目实战阿里云 OSS 文件上传完整开发指南技术栈Spring Boot 3.4.4 阿里云 OSS SDK 3.17.4 Maven 多模块前置依赖员工管理模块JWT 认证、全局异常处理已就绪适用场景菜品图片上传、套餐图片上传、店铺Logo等所有需要文件存储的场景目录一、业务背景与架构设计二、接口总览三、阿里云 OSS 准备工作四、配置文件五、核心工具类 AliOssUtil六、Controller 层七、完整数据流解析八、接口测试示例九、踩坑记录与注意事项十、后续复用场景一、业务背景与架构设计为什么需要 OSS苍穹外卖中菜品图片、套餐图片等文件需要持久化存储。直接存服务器本地磁盘存在以下问题问题说明服务器磁盘空间有限图片占用空间大长期积累会撑爆部署多台机器时文件不同步A 机上传了B 机访问不到无法直接被浏览器访问需要额外写下载接口备份/迁移麻烦换服务器要搬全部文件架构图前端Vue3/小程序 │ │ POST /admin/common/upload (multipart/form-data) │ file: 图片文件 ▼ ┌─────────────────────────────┐ │ CommonController │ │ 接收 MultipartFile │ │ → 调用 AliOssUtil.upload()│ └──────────┬──────────────────┘ │ │ byte[] UUID文件名 ▼ ┌─────────────────────────────┐ │ AliOssUtil (工具类) │ │ 构建 OSSClient │ │ → ossClient.putObject() │ │ → 返回 URL 字符串 │ └──────────┬──────────────────┘ │ │ HTTPS URL ▼ ┌─────────────────────────────┐ │ 阿里云 OSS │ │ Bucket: dddbucket │ │ Region: 北京 │ │ 文件公开可读 │ └─────────────────────────────┘二、接口总览序号请求方式接口路径功能说明Content-Type1POST/admin/common/upload通用文件上传图片/文档等multipart/form-data⚠️ 注意这是一个通用接口所有需要上传文件的模块菜品、套餐等都复用这一个接口。三、阿里云 OSS 准备工作3.1 注册与开通步骤操作说明1登录 阿里云官网注册账号并完成实名认证2开通 OSS 服务搜索对象存储OSS点击开通按量付费新用户有免费额度3创建 Bucket进入 OSS 控制台 → 创建 Bucket → 选择地域、设置权限3.2 Bucket 关键设置创建 Bucket 时注意以下配置配置项建议值原因Bucket 名称全局唯一如dddbucket用于拼接访问 URL地域选择离用户近的区域如北京影响上传/下载速度读写权限公共读上传后 URL 可直接在浏览器打开版本控制不开启简单场景不需要为什么选公共读因为返回给前端的 URL 需要能直接被img src...访问。如果设为私有每次访问都需要签名增加复杂度。3.3 获取 AccessKey步骤操作路径1进入 RAM 控制台2创建 AccessKey或使用已有的3保存AccessKey ID和AccessKey Secret⚠️安全建议不要使用主账号的 AccessKey建议创建 RAM 子账号仅授予AliyunOSSFullAccess权限。四、配置文件application.yml关键配置sky:jwt:admin-secret-key:itcast_sky_take_out_2026_secret_key_1234567890abcdefadmin-ttl:7200000admin-token-name:token# 阿里云 OSS 配置 oss:endpoint:oss-cn-beijing.aliyuncs.com# OSS 服务端点含地域bucket-name:dddbucket# Bucket 名称全局唯一access-key-id:LTAI5tAphPRow2yWbxBwvP9L# AccessKey IDaccess-key-secret:6YORxuksSDSwd9N64Z8yicOiObNemV# AccessKey Secret配置项说明配置项示例值用途endpointoss-cn-beijing.aliyuncs.comOSS 服务地址决定数据存储在哪个区域bucket-namedddbucket存储桶名称会出现在访问 URL 中access-key-idLTAI5t...身份认证 IDaccess-key-secret6YORx...身份认证密钥⚠️ 不可泄露Maven 依赖pom.xml!-- 阿里云 OSS SDK --dependencygroupIdcom.aliyun.oss/groupIdartifactIdaliyun-sdk-oss/artifactIdversion3.17.4/version/dependency!-- Spring Boot Configuration Processor支持 ConfigurationProperties 提示--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency五、核心工具类 AliOssUtil完整代码packagecom.sky.utils;importcom.aliyun.oss.ClientException;importcom.aliyun.oss.OSS;importcom.aliyun.oss.OSSClientBuilder;importcom.aliyun.oss.OSSException;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;importjava.io.ByteArrayInputStream;importjava.io.InputStream;/** * 阿里云 OSS 文件上传工具类 * * 核心职责 * 1. 封装阿里云 SDK 的客户端构建与连接管理 * 2. 提供 upload() 方法供所有 Controller 调用 * 3. 自动生成 UUID 文件名防止文件名冲突和中文乱码 */DataNoArgsConstructorAllArgsConstructorComponentConfigurationProperties(prefixsky.oss)// ← 关键注解将 yml 配置注入到字段publicclassAliOssUtil{privateStringendpoint;// OSS 端点privateStringaccessKeyId;// AccessKey IDprivateStringaccessKeySecret;// AccessKey SecretprivateStringbucketName;// Bucket 名称/** * 文件上传 * * param bytes 文件字节数组从 MultipartFile.getBytes() 获取 * param objectName 上传后的文件名含路径前缀如 2024/06/01/uuid.png * return 文件在 OSS 中的完整可访问 URL */publicStringupload(byte[]bytes,StringobjectName){// 第1步构建 OSS 客户端 // 使用 Builder 模式创建客户端实例OSSossClientnewOSSClientBuilder().build(endpoint,accessKeyId,accessKeySecret);try{// 第2步执行上传 // 将字节数组包装成输入流上传到指定 Bucket 的指定路径InputStreaminputStreamnewByteArrayInputStream(bytes);ossClient.putObject(bucketName,objectName,inputStream);// 第3步拼接待返回的访问 URL // 格式https://{bucketName}.{endpoint}/{objectName}returnhttps://bucketName.endpoint/objectName;}catch(OSSExceptionoe){// OSS 服务端异常如 Bucket 不存在、权限不足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());thrownewRuntimeException(OSS 上传失败oe.getErrorMessage());}catch(ClientExceptionce){// 客户端异常如网络不通、参数非法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());thrownewRuntimeException(OSS 客户端异常ce.getMessage());}finally{// 第4步关闭客户端必须// OSSClient 内部维护了 HTTP 连接池不关闭会导致连接泄漏if(ossClient!null){ossClient.shutdown();}}}}关键注解解析注解作用必要性Component将类注册为 Spring Bean支持Autowired注入✅ 必须ConfigurationProperties(prefix sky.oss)将 YAML 中sky.oss.*的值自动绑定到同名字段✅必须漏掉会导致字段全为nullDataLombok 自动生成 getter/setter✅ 必须配置绑定依赖 setterNoArgsConstructor/AllArgsConstructorLombok 生成构造器✅ Spring 反射需要无参构造URL 拼接规则详解配置值 bucketName dddbucket endpoint oss-cn-beijing.aliyuncs.com objectName 6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx 拼接结果 https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx ├────────┤├─────────────────────────┤├──────────────────────────────┤ bucket名 . endpoint 文件路径六、Controller 层完整代码packagecom.sky.controller.admin;importcom.sky.result.Result;importcom.sky.utils.AliOssUtil;importio.swagger.annotations.Api;importio.swagger.annotations.ApiOperation;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.multipart.MultipartFile;importjava.util.UUID;/** * 通用接口控制器 * * 包含文件上传等通用功能不隶属于特定业务模块。 * 所有 /admin/** 路径均受 JWT 拦截器保护。 */RestControllerRequestMapping(/admin/common)Slf4jApi(tags通用接口)publicclassCommonController{AutowiredprivateAliOssUtilaliOssUtil;/** * 文件上传 * * param file 上传的文件表单字段名必须为 file * return OSS 文件访问 URL */PostMapping(/upload)ApiOperation(文件上传)publicResultStringupload(MultipartFilefile){log.info(文件上传{},file.getOriginalFilename());try{// 1. 校验文件是否为空if(filenull||file.isEmpty()){returnResult.error(上传文件不能为空);}// 2. 获取原始文件扩展名如 .png / .jpg / .xlsxStringoriginalFilenamefile.getOriginalFilename();// 例模块备份 9 (1).pngStringextensionoriginalFilename.substring(originalFilename.lastIndexOf(.));// .png// 3. 生成 UUID 新文件名防止重名覆盖 解决中文乱码问题// 例6540ff10-7ae2-40c3-9c49-58fe10908706.pngStringobjectNameUUID.randomUUID().toString()extension;// 4. 调用工具类上传到 OSSStringfilePathaliOssUtil.upload(file.getBytes(),objectName);// 5. 返回可访问的 URL 给前端returnResult.success(filePath);}catch(Exceptione){log.error(文件上传失败,e);returnResult.error(文件上传失败e.getMessage());}}}请求与响应格式请求POST http://localhost:8080/admin/common/upload Content-Type: multipart/form-data; boundary----WebKitFormBoundary token: eyJhbGciOiJIUzI1NiJ9... ------WebKitFormBoundary Content-Disposition: form-data; namefile; filename测试图片.png Content-Type: image/png 二进制文件数据 ------WebKitFormBoundary--成功响应{code:1,msg:success,data:https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.png}失败响应{code:0,msg:上传文件不能为空,data:null}七、完整数据流解析从前端到 OSS 的完整链路┌──────────┐ ┌──────────────────┐ ┌────────────────┐ ┌───────────┐ │ 前端页面 │ ──→ │ CommonController │ ──→ │ AliOssUtil │ ──→ │ 阿里云 OSS │ │ 选择文件 │ │ │ │ │ │ │ └──────────┘ └──────────────────┘ └────────────────┘ └───────────┘ 步骤详情 ① 前端选择文件构造 multipart/form-data 请求 ↓ ② Spring 接收 MultipartFile字段名 file ↓ ③ 获取原始文件名 → 截取后缀名.png ↓ ④ 生成 UUID 作为新文件名 → 避免中文乱码和文件名冲突 ↓ ⑤ 调用 MultipartFile.getBytes() 转为字节数组 ↓ ⑥ AliOssUtil 构建 OSSClient使用 yml 中的 AK/SK ↓ ⑦ 将字节数组写入 InputStream 并调用 putObject() ↓ ⑧ 拼接 URLhttps://{bucket}.{endpoint}/{uuidext} ↓ ⑨ 通过 Result.success(url) 返回给前端 ↓ ⑩ 前端拿到 URL存入数据库的 image 字段后续菜品/套餐新增时UUID 重命名的作用问题不使用 UUID使用 UUID中文乱码❌ “宫保鸡丁.png” 在 URL 中可能编码异常✅a1b2c3d4.png纯英文文件覆盖❌ 两张图同名会互相覆盖✅ 全球唯一不会冲突安全性❌ 暴露原始文件名信息✅ 无法猜测文件路径八、接口测试示例测试前置条件项目已启动端口 8080已通过登录接口获取有效 JWT Token阿里云 OSS 已正确配置endpoint、bucket、AK/SKHeader 中携带 token8.1 使用 Postman / Apifox 测试操作步骤步骤操作说明1选择POST方法请求方式2输入 URLhttp://localhost:8080/admin/common/upload3设置 Headertoken: 你的JWT4Body 选form-data切换到表单模式5添加 Keyfile类型选File字段名必须是file6Value 处选择本地文件点击上传任意图片7点击 Send 发送请求—成功响应{code:1,msg:success,data:https://dddbucket.oss-cn-beijing.aliyuncs.com/6540ff10-7ae2-40c3-9c49-58fe10908706.xlsx}8.2 使用 Swagger UI 测试访问http://localhost:8080/doc.html Knife4j找到「通用接口」→ 「文件上传」在线上传测试。8.3 验证上传结果在浏览器中直接访问返回的 URL确认可以看到上传的文件。九、踩坑记录与注意事项 P0 — 必须解决#问题现象错误信息原因分析解决方案1MultipartFile 为 nullNullPointerException: Cannot invoke getOriginalFilename()because “file” is null前端请求未使用multipart/form-data格式或字段名不是file① 前端确保用 FormData 上传② 字段名必须匹配RequestParam(file)或方法参数名2AccessKey id should not be nullInvalidCredentialsException: Access key id should not be null or empty.AliOssUtil缺少ConfigurationProperties(prefix sky.oss)注解导致 4 个配置字段全部为 null加上该注解光有Component不够它不会自动读取 yml 配置 P1 — 功能影响#问题现象原因解决方案3OSSClient 连接泄漏高并发下频繁上传导致服务变慢甚至 OOM每次build()创建的新客户端必须在finally中调用shutdown()4Bucket 权限问题上传成功但浏览器访问 URL 返回 403Bucket 默认是私有的5Endpoint 格式错误UnknownHostExceptionendpoint 写成了https://oss-cn-beijing.aliyuncs.com带了协议头 P2 — 规范建议#建议说明6文件大小限制Spring Boot 默认限制单文件 1MB、总请求 10MB可在 yml 中调大spring.servlet.multipart.max-file-size10MB7文件类型校验当前实现允许上传任何类型生产环境应校验 MIME 类型如只允许 image/*8AccessKey 安全绝对不要把 AK/SK 写死在代码里或提交到 Git必须放在配置文件或环境变量中9UUID 原始扩展名组合不要丢弃原始扩展名浏览器根据扩展名判断如何渲染.jpgvs.txt表现完全不同10OSSClient 是否应该做成单例当前每次上传都 build 一个新客户端性能略差但简单安全。高并发场景可用单例 连接池优化初学阶段不建议过早优化十、后续复用场景本模块完成后以下模块将直接复用/admin/common/upload接口模块用途说明复用方式菜品管理新增/编辑菜品时上传菜品图片先调上传接口拿 URL → 再把 URL 存入 dish.image 字段套餐管理新增/编辑套餐时上传套餐图片同上URL 存入 setmeal.image 字段店铺设置店铺 Logo 等如有同理典型前端交互流程以新增菜品为例① 用户在表单中选择图片文件 ↓ ② 前端先调用 POST /admin/common/upload ↓ ③ 后端返回 URLhttps://dddbucket.oss-cn-beijing.aliyuncs.com/xxx.png ↓ ④ 前端将 URL 填入表单的 image 字段 ↓ ⑤ 用户填写其他菜品信息名称、价格、分类等点击提交 ↓ ⑥ 前端调用 POST /admin/dishJSON body 中包含 image: https://... ↓ ⑦ 后端将完整数据写入 dish 数据库表 ↓ ⑧ 前端展示菜品列表时img :srcdish.image 直接从 OSS 加载图片核心思路上传接口只负责把文件放到 OSS 并返回 URL具体这个 URL 怎么用、存到哪张表由各个业务模块自己决定。十一、涉及的新增文件清单文件路径所属模块说明AliOssUtil.javasky-commonutils包OSS 工具类核心上传逻辑CommonController.javasky-servercontroller.admin包通用控制器文件上传入口application.ymlsky-serverresources新增sky.oss.*配置节pom.xmlsky-server 或父 pom新增 aliyun-sdk-oss 依赖本文基于苍穹外卖项目实战整理紧接分类管理模块之后。OSS 是独立的基础能力模块不依赖任何业务表。掌握后将直接服务于菜品管理和套餐管理模块的开发。