今日学习内容
今天是 Day 18,继续在第三阶段「AI 应用 Web 开发」中深度前进。昨天我们学了 MongoDB 文档数据库、Spring Data MongoDB 集成与 Docker 镜像构建,今天按照 Day 17 结尾的计划,重点攻克两个非常实用的工程化主题:
- Maven 私服搭建(Nexus):为什么需要私服、Nexus 仓库架构、Docker 部署 Nexus、Maven settings.xml 配置私服镜像与发布、SNAPSHOT 与 RELEASE 版本管理
- 阿里云 OSS 对象存储:OSS 核心概念(Bucket/Object/Endpoint)、SDK 集成、文件上传/下载/删除/签名 URL、前端直传方案 STS 临时凭证、头像上传与图片管理完整实战
前置知识回顾:Day 11 我们用 Maven 搭建了 Spring Boot 后端项目;Day 17 用 Docker 构建了微服务镜像。今天将 Maven 制品管理(Nexus 私服)和文件存储(OSS 对象存储)两个企业级基础设施纳入技术栈,为后续项目实战做好支撑。
Maven 私服概述与 Nexus 搭建
为什么需要 Maven 私服?
在正式的企业开发中,团队协作和依赖管理面临几个核心问题:
- 依赖下载慢:每次从 Maven Central 下载依赖,网络不稳定时构建非常慢
- 内部组件共享:自己封装的公共模块(如 common-api、common-util)怎么让多个项目复用?
- 第三方依赖不可用:某些私有 JAR 包或已下架的依赖需要统一管理
- 安全合规:避免开发人员直接连接外部仓库,所有依赖经过私服统一审核
Maven 私服(Nexus Repository Manager)就是解决这些问题的利器。它充当本地 Maven 与远程仓库之间的代理缓存层,同时也是团队内部制品的统一发布中心。
Nexus 仓库架构
Nexus 仓库分为四种类型,理解它们的区别是使用 Nexus 的基础:
| 仓库类型 | 说明 | 典型用途 |
|---|---|---|
| proxy(代理仓库) | 代理远程仓库,缓存下载的依赖 | 代理 Maven Central、阿里云镜像 |
| hosted(宿主仓库) | 存储团队自己发布的制品 | releases、snapshots、第三方 JAR |
| group(组仓库) | 将多个仓库合并为一个访问地址 | 统一入口 maven-public |
| virtual(虚拟仓库) | 适配不同格式的仓库视图 | Maven 1 兼容,现代项目很少用 |
核心概念:Nexus 默认创建了一个maven-public组仓库,它包含了maven-central(代理中央仓库)、maven-releases(内部 Release 制品)、maven-snapshots(内部 Snapshot 制品)。日常开发只需要配置指向maven-public即可。
Docker 部署 Nexus
Nexus 的安装方式很多,但 Docker 部署是最干净的方式:
docker-compose.ymlYAML
version: '3.8' services: nexus: image: sonatype/nexus3:3.72.0 container_name: nexus ports: - "8081:8081" # Nexus Web 界面 volumes: - nexus-data:/nexus-data restart: unless-stopped environment: - INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx512m volumes: nexus-data: driver: local注意:Nexus 3.x 首次启动需要 1-2 分钟,内存建议至少 1GB。生产环境建议调大 JVM 参数-Xms1g -Xmx1g。启动后通过docker logs nexus查看日志,出现Started Sonatype Nexus即启动成功。默认管理员密码在/nexus-data/admin.password文件中。
初始化配置
首次访问http://localhost:8081后:
- 获取初始密码:执行
docker exec nexus cat /nexus-data/admin.password查看随机生成的管理员密码 - 设置新密码:登录后强制要求修改 admin 密码
- 配置匿名访问:取消勾选「Enable anonymous access」(安全考虑,生产环境关闭)
- 配置代理仓库:将
maven-central的 Remote URL 改为阿里云镜像https://maven.aliyun.com/repository/central,加速国内下载
Nexus 仓库管理与制品上传
发布制品到 Nexus
当我们开发了一个公共模块(比如common-api),想让其他项目引用它,就需要将制品发布到 Nexus 私服:
第一步:在 common-api 的 pom.xml 中配置 distributionManagement
common-api/pom.xmlXML
<distributionManagement> <repository> <id>nexus-releases</id> <name>Nexus Release Repository</name> <url>http://nexus.example.com:8081/repository/maven-releases/</url> </repository> <snapshotRepository> <id>nexus-snapshots</id> <name>Nexus Snapshot Repository</name> <url>http://nexus.example.com:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>第二步:执行发布命令
TerminalShell
# 跳过测试发布到私服 mvn deploy -DskipTestsSNAPSHOT vs RELEASE:版本号以-SNAPSHOT结尾的会发布到 snapshots 仓库,每次发布会带时间戳,允许覆盖更新;不带-SNAPSHOT的版本会发布到 releases 仓库,同一版本不允许重复发布。
手动上传第三方 JAR
有些第三方 JAR 包在 Maven Central 上找不到(比如某厂商提供的 SDK),可以通过 Nexus Web 界面上传:
- 进入
maven-releases仓库 → Upload 界面 - 选择 JAR 文件,填写 Group ID、Artifact ID、Version
- 点击 Upload 完成上传
也可以通过 Maven 命令行安装到本地再部署:
TerminalShell
mvn install:install-file \ -Dfile=third-party-sdk-1.0.jar \ -DgroupId=com.example.thirdparty \ -DartifactId=third-party-sdk \ -Dversion=1.0.0 \ -Dpackaging=jarMaven settings.xml 配置私服
要让所有项目都从 Nexus 私服下载依赖,需要配置 Maven 的settings.xml(位于~/.m2/settings.xml):
配置镜像(mirrors)
~/.m2/settings.xmlXML
<settings> <mirrors> <mirror> <id>nexus-mirror</id> <mirrorOf>central</mirrorOf> <name>Nexus Maven Mirror</name> <url>http://nexus.example.com:8081/repository/maven-public/</url> </mirror> </mirrors> <!-- 发布认证信息 --> <servers> <server> <id>nexus-releases</id> <username>admin</username> <password>admin123</password> </server> <server> <id>nexus-snapshots</id> <username>admin</username> <password>admin123</password> </server> </servers> <!-- 激活 profile --> <activeProfiles> <activeProfile>nexus</activeProfile> </activeProfiles> <profiles> <profile> <id>nexus</id> <repositories> <repository> <id>nexus-public</id> <url>http://nexus.example.com:8081/repository/maven-public/</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>true</enabled></snapshots> </repository> </repositories> </profile> </profiles> </settings>关键理解:mirrorOf为central表示拦截所有对 Maven Central 的请求,转向 Nexus 私服。server.id必须与distributionManagement.repository.id一致,Maven 才能根据 ID 匹配到认证信息进行发布。
父 POM 统一管理版本
在微服务项目中,通常有一个父 POM 管理所有依赖版本,各子模块继承即可:
parent-pom/pom.xmlXML
<dependencyManagement> <dependencies> <!-- 团队内部模块 --> <dependency> <groupId>com.example.common</groupId> <artifactId>common-api</artifactId> <version>${common.version}</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> </dependencies> </dependencyManagement>阿里云 OSS 对象存储入门
为什么用 OSS 而不是本地文件存储?
在项目中,用户头像、文章封面、附件等文件存储方案有三种选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地磁盘 | 简单、零成本 | 不扩展、不持久、CDN 弱 | 开发调试 |
| FastDFS/MinIO | 自建可控、可扩展 | 运维成本高、需自建 CDN | 大型企业自建 |
| 阿里云 OSS | 高可用、CDN 加速、免运维 | 按量付费、有网络依赖 | 绝大多数互联网项目 |
阿里云 OSS 是国内最主流的对象存储服务,提供高可用(99.995%)、高持久(99.9999999999%)的数据存储能力,天然支持 CDN 加速和图片处理。
OSS 核心概念
- Bucket:存储空间,类似文件系统的「根目录」,全局唯一名称,支持设置区域、访问权限、生命周期
- Object:文件对象,包含 Key(文件路径)、Data(文件内容)、Meta(元数据如 Content-Type)
- Endpoint:访问域名,如
oss-cn-hangzhou.aliyuncs.com,根据 Bucket 所在区域选择 - AccessKey:访问密钥(AccessKeyId + AccessKeySecret),用于 API 认证
- ACL:访问控制,Bucket 级别和 Object 级别,支持 private / public-read / public-read-write
创建 OSS Bucket
- 登录阿里云控制台 → 对象存储 OSS → 创建 Bucket
- 填写 Bucket 名称(全局唯一,如
edu-blog-assets) - 选择区域(就近原则,如
华东1(杭州)) - 存储类型选择「标准存储」
- 读写权限选择「私有」(安全考虑,通过签名 URL 提供访问)
引入 OSS SDK
pom.xml — OSS 依赖XML
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.17.4</version> </dependency>Spring Boot 整合 OSS — 文件上传服务
OSS 客户端配置类
首先创建 OSS 配置类,初始化 OSSClient 实例:
config/OssConfig.javaJava
package com.example.blog.config; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Data @Configuration @ConfigurationProperties(prefix = "oss") public class OssConfig { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; private String dir; // 上传目录前缀,如 "blog/2026/" @Bean public OSS ossClient() { return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } }application.yml — OSS 配置YAML
oss: endpoint: oss-cn-hangzhou.aliyuncs.com access-key-id: your-access-key-id access-key-secret: your-access-key-secret bucket-name: edu-blog-assets dir: blog/2026/安全提示:AccessKeyId/AccessKeySecret 绝对不能写在代码中硬编码!应通过环境变量或 Nacos 配置中心注入。开发环境可以放 application-dev.yml(不提交 Git),生产环境通过-Doss.access-key-id=${OSS_AK}传入。
OSS 服务层封装
service/OssService.javaJava
package com.example.blog.service; import com.aliyun.oss.OSS; import com.aliyun.oss.model.*; import com.example.blog.config.OssConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; @Slf4j @Service @RequiredArgsConstructor public class OssService { private final OSS ossClient; private final OssConfig ossConfig; /** * 上传文件到 OSS * @param file 上传的文件 * @param folder 文件夹名(如 "avatar" / "article" / "attachment") * @return 文件的完整访问路径 */ public String uploadFile(MultipartFile file, String folder) { try { // 1. 校验文件 String originalFilename = file.getOriginalFilename(); if (originalFilename == null || originalFilename.isEmpty()) { throw new RuntimeException("文件名不能为空"); } // 2. 生成唯一文件名:日期目录/UUID.扩展名 String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String datePath = LocalDate.now().format( DateTimeFormatter.ofPattern("yyyy/MM/dd")); String objectName = String.format("%s/%s/%s/%s%s", ossConfig.getDir(), folder, datePath, UUID.randomUUID().toString().replace("-", ""), ext); // 3. 设置 Content-Type ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(file.getContentType()); metadata.setContentLength(file.getSize()); // 4. 上传文件流 try (InputStream is = file.getInputStream()) { PutObjectResult result = ossClient.putObject( ossConfig.getBucketName(), objectName, is, metadata); log.info("文件上传成功, objectName={}, ETag={}", objectName, result.getETag()); } return objectName; } catch (Exception e) { log.error("文件上传失败", e); throw new RuntimeException("文件上传失败: " + e.getMessage()); } } /** * 生成文件的签名访问 URL(有效期 1 小时) */ public String getSignedUrl(String objectName, int expireMinutes) { long expiration = System.currentTimeMillis() + expireMinutes * 60 * 1000; URL url = ossClient.generatePresignedUrl( ossConfig.getBucketName(), objectName, new java.util.Date(expiration)); return url.toString(); } /** * 删除文件 */ public void deleteFile(String objectName) { ossClient.deleteObject(ossConfig.getBucketName(), objectName); log.info("文件删除成功, objectName={}", objectName); } }文件上传 Controller
controller/FileController.javaJava
package com.example.blog.controller; import com.example.blog.common.Result; import com.example.blog.service.OssService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/file") @RequiredArgsConstructor public class FileController { private final OssService ossService; /** * 通用文件上传接口 * @param file 文件(multipart/form-data) * @param folder 文件夹(avatar/article/attachment) */ @PostMapping("/upload") public Result<String> upload( @RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "article") String folder) { // 文件大小校验(最大 10MB) if (file.getSize() > 10 * 1024 * 1024) { return Result.fail("文件大小不能超过 10MB"); } // 文件类型校验 String contentType = file.getContentType(); if (contentType == null || !isAllowedType(contentType)) { return Result.fail("不支持的文件类型"); } String objectName = ossService.uploadFile(file, folder); return Result.ok(objectName); } /** * 获取文件签名 URL */ @GetMapping("/url") public Result<String> getSignedUrl(@RequestParam String key) { String url = ossService.getSignedUrl(key, 60); // 60 分钟有效期 return Result.ok(url); } /** * 删除文件 */ @DeleteMapping("/delete") public Result<Void> delete(@RequestParam String key) { ossService.deleteFile(key); return Result.ok(); } private boolean isAllowedType(String contentType) { return contentType.startsWith("image/") || contentType.startsWith("application/pdf") || contentType.equals("application/zip"); } }前端 OSS 直传方案
上面的方案是前端先将文件传到 Spring Boot 后端,后端再上传到 OSS。文件会经过两次网络传输,后端服务器还承担了带宽压力。更高效的方案是前端直传 OSS:
方案对比
| 维度 | 服务端中转 | 前端直传 OSS |
|---|---|---|
| 传输路径 | 前端 → 后端 → OSS | 前端 → OSS(一次直达) |
| 后端带宽 | 压力大(所有文件流经后端) | 零带宽消耗 |
| 上传速度 | 受后端出口带宽限制 | 直接到 OSS,速度更快 |
| 安全性 | AK/SK 在服务端,安全 | 用 STS 临时凭证,安全可控 |
| 适用场景 | 文件小、需后端校验处理 | 文件大、高并发上传 |
STS 临时凭证方案
前端直传 OSS 不能使用长期 AK/SK(泄露风险太大),而是通过STS(Security Token Service)获取临时凭证:
流程图:
STS 直传流程:① 前端调用后端接口获取 STS Token → ② 后端调用阿里云 STS API 获取临时 AccessKeyId/Secret/SecurityToken → ③ 返回给前端 → ④ 前端用临时凭证直传文件到 OSS → ⑤ 前端上传完成后通知后端记录文件信息
后端 STS 凭证签发接口:
service/StsService.javaJava
package com.example.blog.service; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.auth.sts.AssumeRoleRequest; import com.aliyuncs.auth.sts.AssumeRoleResponse; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class StsService { @Value("${oss.sts.endpoint:sts.cn-hangzhou.aliyuncs.com}") private String stsEndpoint; @Value("${oss.access-key-id}") private String accessKeyId; @Value("${oss.access-key-secret}") private String accessKeySecret; @Value("${oss.sts.role-arn}") private String roleArn; // RAM 角色 ARN public StsCredential getStsCredential() { try { DefaultProfile profile = DefaultProfile.getProfile( stsEndpoint, accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile); AssumeRoleRequest request = new AssumeRoleRequest(); request.setRoleArn(roleArn); request.setRoleSessionName("blog-upload-" + System.currentTimeMillis()); // 临时凭证有效期 15 分钟 request.setDurationSeconds(900L); AssumeRoleResponse response = client.getAcsResponse(request); AssumeRoleResponse.Credential cred = response.getCredentials(); StsCredential result = new StsCredential(); result.setAccessKeyId(cred.getAccessKeyId()); result.setAccessKeySecret(cred.getAccessKeySecret()); result.setSecurityToken(cred.getSecurityToken()); result.setExpiration(cred.getExpiration()); result.setEndpoint(ossConfig.getEndpoint()); result.setBucketName(ossConfig.getBucketName()); return result; } catch (ClientException e) { throw new RuntimeException("获取 STS 凭证失败", e); } } @Data public static class StsCredential { private String accessKeyId; private String accessKeySecret; private String securityToken; private String expiration; private String endpoint; private String bucketName; } }前端直传代码(Vue 3 + ali-oss SDK):
utils/ossUpload.jsJavaScript
import OSS from 'ali-oss' import request from '@/utils/request' /** * 获取 STS 临时凭证 */ async function getStsToken() { const res = await request.get('/api/file/sts-token') return res.data } /** * 前端直传文件到 OSS * @param {File} file - 原生 File 对象 * @param {string} folder - OSS 目录(avatar/article) * @returns {Promise<string>} - 文件的 objectName(key) */ export async function uploadToOss(file, folder = 'article') { // 1. 获取 STS 临时凭证 const token = await getStsToken() // 2. 创建 OSS Client(使用临时凭证) const client = new OSS({ region: token.endpoint.replace('oss-', '').split('.')[0], accessKeyId: token.accessKeyId, accessKeySecret: token.accessKeySecret, stsToken: token.securityToken, bucket: token.bucketName, }) // 3. 生成唯一文件名 const ext = file.name.substring(file.name.lastIndexOf('.')) const date = new Date() const datePath = `${date.getFullYear()}/${ String(date.getMonth() + 1).padStart(2, '0')}/${ String(date.getDate()).padStart(2, '0')}` const objectName = `blog/2026/${folder}/${datePath}/${ crypto.randomUUID()}${ext}` // 4. 直传到 OSS const result = await client.put(objectName, file, { // 可选:进度回调 progress: (p) => { console.log(`上传进度: ${Math.round(p * 100)}%`) } }) // 5. 通知后端记录文件信息(可选) await request.post('/api/file/callback', { objectName: result.name, size: file.size, type: file.type }) return result.name }完整项目实战 — 头像上传与图片管理
现在把所有知识串联起来,实现一个完整的「用户头像上传」功能:
头像上传接口
controller/UserController.java — 头像上传Java
/** * 上传用户头像 * 流程:接收文件 → 校验 → 上传 OSS → 更新用户头像 URL */ @PostMapping("/api/user/avatar") public Result<String> uploadAvatar(@RequestParam MultipartFile file) { // 1. 只允许图片类型 String contentType = file.getContentType(); if (contentType == null || !contentType.startsWith("image/")) { return Result.fail("只能上传图片文件"); } // 2. 文件大小限制 2MB if (file.getSize() > 2 * 1024 * 1024) { return Result.fail("头像图片不能超过 2MB"); } // 3. 上传到 OSS(avatar 目录) String objectName = ossService.uploadFile(file, "avatar"); // 4. 删除旧头像(如果存在) User currentUser = getCurrentUser(); if (currentUser.getAvatar() != null && !currentUser.getAvatar().isEmpty()) { ossService.deleteFile(currentUser.getAvatar()); } // 5. 更新数据库中的头像地址 userService.updateAvatar(currentUser.getId(), objectName); // 6. 返回签名访问 URL String signedUrl = ossService.getSignedUrl(objectName, 24 * 60); return Result.ok(signedUrl); }文章封面图上传
前端组件 — 文章封面图上传Vue 3
<template> <div class="cover-upload"> <el-upload class="cover-uploader" :action="''" :http-request="customUpload" :show-file-list="false" accept="image/*" :before-upload="beforeUpload" > <img v-if="coverUrl" :src="coverUrl" class="cover-image" /> <el-icon v-else class="cover-placeholder"> <Plus /> </el-icon> </el-upload> <el-button v-if="coverUrl" type="danger" size="small" @click="removeCover">移除封面</el-button> </div> </template> <script setup> import { ref } from 'vue' import { uploadToOss } from '@/utils/ossUpload' import { ElMessage } from 'element-plus' const coverUrl = ref('') const emit = defineEmits(['update:cover']) function beforeUpload(file) { const isImage = file.type.startsWith('image/') const isLt5M = file.size / 1024 / 1024 < 5 if (!isImage) ElMessage.error('只能上传图片!') if (!isLt5M) ElMessage.error('图片大小不能超过 5MB!') return isImage && isLt5M } async function customUpload({ file }) { try { const objectName = await uploadToOss(file, 'article') // 通过后端接口获取签名 URL 来展示 const res = await request.get('/api/file/url', { params: { key: objectName } }) coverUrl.value = res.data emit('update:cover', objectName) ElMessage.success('封面上传成功') } catch (e) { ElMessage.error('上传失败: ' + e.message) } } function removeCover() { coverUrl.value = '' emit('update:cover', '') } </script>踩坑记录
坑一:Nexus 启动后无法访问 Web 界面
现象:docker-compose up启动成功,但浏览器访问http://localhost:8081一直无法连接。
原因:Nexus 3.x 首次启动需要初始化大量数据,启动过程可能需要 2-3 分钟。如果宿主机内存不足(低于 1GB),JVM 会因 OOM 反复重启。
解决方案:docker logs nexus查看日志。如果看到Started Sonatype Nexus OSS 3.x.x表示启动完成。内存不足时调大 Docker 内存限制,或将INSTALL4J_ADD_VM_PARAMS调整为-Xms256m -Xmx256m。
坑二:mvn deploy 失败 401 Unauthorized
现象:mvn deploy时报错Return code is: 401, ReasonPhrase: Unauthorized。
原因:settings.xml中<server><id>与pom.xml中<distributionManagement><repository><id>不匹配。Maven 通过 ID 查找对应的认证信息。
解决方案:确保两边 ID 完全一致。例如pom.xml写<id>nexus-releases</id>,settings.xml也必须写<server><id>nexus-releases</id>。另外检查用户名密码是否正确。
坑三:OSS 上传后无法通过 URL 直接访问
现象:文件上传成功,但通过https://bucket.oss-cn-hangzhou.aliyuncs.com/objectName访问返回 403。
原因:Bucket ACL 设置为「私有」,不允许匿名访问。需要通过签名 URL 或设置 Bucket 为「公共读」。
解决方案:生产环境推荐「私有 Bucket + 签名 URL」方案(更安全)。开发测试可以将 Bucket 设为「公共读」或通过ossClient.generatePresignedUrl()生成带签名的临时访问链接。注意签名 URL 过期后需要重新生成。
坑四:前端直传时 CORS 跨域被拦截
现象:前端使用 ali-oss SDK 直传时,浏览器控制台报 CORS 错误:Access-Control-Allow-Origin缺失。
原因:OSS Bucket 没有配置跨域规则(CORS),浏览器拦截了跨域的 PUT 请求。
解决方案:在阿里云 OSS 控制台 → 权限管理 → 跨域设置(CORS),添加以下规则:
- 允许来源:*(生产环境改为具体域名)
- 允许方法:GET, POST, PUT, DELETE, HEAD
- 允许 Headers:*
- 暴露 Headers:ETag, x-oss-request-id
收获总结
今日学习收获
今天在 Day 18 中,我们完成了两大块企业级基础设施的搭建:
Maven 私服(Nexus):理解了四种仓库类型的区别,掌握了 Docker 部署 Nexus、settings.xml 镜像配置、制品发布流程、SNAPSHOT 与 RELEASE 版本管理。私服是团队协作的基础设施,解决了依赖缓存、内部模块共享、第三方 JAR 管理三大痛点。
阿里云 OSS 对象存储:掌握了 OSS 的核心概念(Bucket/Object/Endpoint/ACL),Spring Boot 整合 OSS SDK 实现文件上传/下载/删除/签名 URL,以及前端直传方案(STS 临时凭证 + CORS 配置)。OSS 直传避免了服务端带宽瓶颈,是企业级项目的标准文件存储方案。
关键收获:今天的内容高度实用,属于「项目中一定会用到的工程化知识」。Nexus 私服确保团队依赖管理规范化;OSS + STS 直传解决了文件存储的性能与安全问题。这两个技术点在后续的 EduRAG 项目和企业级项目中都会持续使用。
明日计划
Day 19 学习规划
第三阶段「AI 应用 Web 开发」进入后半段,计划学习:
- 消息队列 RabbitMQ 基础:消息模型(Direct/Fanout/Topic)、Spring AMQP 整合、异步解耦实战(文章发布通知、邮件发送)
- 分布式定时任务:XXL-JOB 分布式任务调度平台、Spring Boot 整合、定时任务可视化管理和执行日志
- WebSocket 实时通信:WebSocket 协议原理、Spring Boot WebSocket 配置、在线聊天/消息推送实战