阿里云 OSS 文件上传笔记
商品主图、管理员/用户头像等文件不存应用服务器(多实例不共享、重新部署会丢),而是上传到阿里云 OSS 对象存储,数据库里只存图片访问 URL。上传接口在 mall-service,OSS 工具类在 mall-common,孤儿图片由定时任务清理(见《定时任务.md》)。
一、为什么用 OSS(对比本地上传)
| 方案 | 问题 |
|---|---|
| 存数据库 BLOB | 撑大数据库、备份慢、读写都压在 DB |
| 存应用服务器本地磁盘 | 微服务多实例不共享;重新部署/扩容文件丢失;占用应用带宽 |
| OSS 对象存储 | 与应用解耦、可 CDN 加速、高可用、按量付费,DB 只存一个 URL |
product.main_image、admin.avatar 字段存的就是 OSS 返回的 https 地址。
二、配置(密钥走环境变量,不留明文)
各服务 application.yml(OSS 配置定义在父/各服务里):
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucket-name: maall
access-key-id: ${ACCESS_KEY_ID:} # 冒号后默认空,强制由环境变量注入
access-key-secret: ${ACCESS_KEY_SECRET:}
AliOSSUtil 用 @Value 读取这四项。密钥不写死、不提交到 Git,和支付宝私钥、DeepSeek api-key 的处理方式一致。
三、上传接口(mall-service)
mall-service/.../controller/UploadController.java:
@RestController
@RequestMapping("/service")
public class UploadController {
@Autowired
private AliOSSUtil aliOSSUtil;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
// 1.UUID 生成唯一文件名,避免同名覆盖
String uuid = UUID.randomUUID().toString().replace("-", "");
// 2.保留原始扩展名,如 .png
String filename = file.getOriginalFilename();
String extension = filename.substring(filename.lastIndexOf("."));
String newFilename = uuid + extension; // 例如 7c45...abe.png
// 3.以流的形式上传到 OSS,返回公网访问 URL
String url = aliOSSUtil.uploadFile(newFilename, file.getInputStream());
return Result.ok("上传成功", url);
}
}
要点:
MultipartFile是 Spring 封装的上传文件对象,getInputStream()拿文件流,getOriginalFilename()拿原始文件名;- UUID 重命名:防止不同用户上传同名文件互相覆盖,也规避中文/特殊字符文件名问题;
- 返回的 URL 由前端填进表单(商品主图字段),最后随表单一起入库。
四、OSS 工具类(mall-common)
mall-common/.../util/AliOSSUtil.java:
@Component
public class AliOSSUtil {
@Value("${oss.endpoint}") private String endpoint;
@Value("${oss.bucket-name}") private String bucketName;
@Value("${oss.access-key-id}") private String accessKeyId;
@Value("${oss.access-key-secret}") private String accessKeySecret;
public String uploadFile(String objectName, InputStream in) throws Exception {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
PutObjectRequest req = new PutObjectRequest(bucketName, objectName, in);
ossClient.putObject(req);
// URL 拼接:https://bucket名.endpoint/objectName
return "https://" + bucketName + "."
+ endpoint.substring(endpoint.lastIndexOf("/") + 1) + "/" + objectName;
} finally {
ossClient.shutdown(); // 用完释放连接
}
}
public void deleteFile(String url) { // 按 objectName 删除
ossClient.deleteObject(bucketName, url.substring(url.lastIndexOf("/") + 1));
}
public Set<String> listFile() { // 分页列举 bucket 内全部 objectName
// marker 翻页循环直到 !isTruncated()
}
}
- SDK 依赖
com.aliyun.oss:aliyun-sdk-oss(父 pom 管理,3.15.1); - 三个方法分别对应:上传
putObject、删除deleteObject、列举listObjects(定时清理孤儿图用)。
五、前端怎么传(el-upload)
Element Plus 的 el-upload 自带文件选择和上传,关键是要带 JWT(请求经网关,网关要验 token)。el-upload 不走 axios 拦截器,所以用 :headers 手动加:
<el-upload
action="/service/upload"
name="file"
:headers="{ Authorization: tokenStore.token }"
:show-file-list="false"
:on-success="handleSuccess">
<el-button>上传图片</el-button>
</el-upload>
action是上传接口(经 vite 代理/网关);name="file"要和后端MultipartFile file参数名一致;on-success回调里把返回的图片 URL 赋给表单字段(如form.mainImage = res.data)。
六、孤儿图片清理(与定时任务联动)
商品删除/换图后,旧图片成了 OSS 里”数据库不再引用”的孤儿文件,持续占空间。DeleteImageJob 每 2 分钟(cron 可配)做差集清理:
DB 在用图片名(product 主图 + admin 头像,经 Feign 聚合)
⊖
OSS 全部文件名(aliOSSUtil.listFile())
= 孤儿文件 → aliOSSUtil.deleteFile(name)
详见《定时任务.md》。这是”上传”之外保证 OSS 不堆积垃圾的闭环。
七、生产环境改进点(面试可主动说)
- 不用主账号 AK/SK:当前 AccessKey 权限过大。生产用 STS 临时凭证 或只授权 OSS 的 RAM 子账号,泄露面最小。
- 前端直传(推荐):后端只生成签名/STS,浏览器直传 OSS,文件流量不经过应用服务器,大幅降低后端带宽与压力。当前是”浏览器→应用→OSS”的中转上传。
- OSSClient 复用:现在每次上传都 new 一个 client 再 shutdown,连接无法复用。生产把
OSS做成单例 Bean(线程安全),容器关闭时统一 shutdown。 - 校验文件:限制类型(白名单扩展名/Content-Type)、大小,图片可做压缩;防止上传可执行文件。
- 私有 Bucket + 预签名 URL:敏感文件私有读,用限时签名 URL 访问;公开图片才用公共读。
- 可配合 CDN 加速、图片处理服务(缩略图/水印)。
八、一句话总结
前端用 el-upload 带 JWT 把文件 POST 到 mall-service 的
/service/upload;后端用MultipartFile接流、UUID 重命名防覆盖,调AliOSSUtil.putObject上传到阿里云 OSS 并拼出访问 URL 返回;密钥由环境变量注入不落明文;数据库只存 URL;定时任务再用”OSS 全集 − DB 在用集”清理孤儿图片。生产应升级为 STS 临时凭证 + 前端直传 + 单例 client + 文件类型校验。