尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ai全栈软件开发day18

ai全栈软件开发day18 今日学习内容今天是 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、第三方 JARgroup组仓库将多个仓库合并为一个访问地址统一入口 maven-publicvirtual虚拟仓库适配不同格式的仓库视图Maven 1 兼容现代项目很少用核心概念Nexus 默认创建了一个maven-public组仓库它包含了maven-central代理中央仓库、maven-releases内部 Release 制品、maven-snapshots内部 Snapshot 制品。日常开发只需要配置指向maven-public即可。Docker 部署 NexusNexus 的安装方式很多但 Docker 部署是最干净的方式docker-compose.ymlYAMLversion: 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 中配置 distributionManagementcommon-api/pom.xmlXMLdistributionManagement repository idnexus-releases/id nameNexus Release Repository/name urlhttp://nexus.example.com:8081/repository/maven-releases//url /repository snapshotRepository idnexus-snapshots/id nameNexus Snapshot Repository/name urlhttp://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 命令行安装到本地再部署TerminalShellmvn install:install-file \ -Dfilethird-party-sdk-1.0.jar \ -DgroupIdcom.example.thirdparty \ -DartifactIdthird-party-sdk \ -Dversion1.0.0 \ -DpackagingjarMaven settings.xml 配置私服要让所有项目都从 Nexus 私服下载依赖需要配置 Maven 的settings.xml位于~/.m2/settings.xml配置镜像mirrors~/.m2/settings.xmlXMLsettings mirrors mirror idnexus-mirror/id mirrorOfcentral/mirrorOf nameNexus Maven Mirror/name urlhttp://nexus.example.com:8081/repository/maven-public//url /mirror /mirrors !-- 发布认证信息 -- servers server idnexus-releases/id usernameadmin/username passwordadmin123/password /server server idnexus-snapshots/id usernameadmin/username passwordadmin123/password /server /servers !-- 激活 profile -- activeProfiles activeProfilenexus/activeProfile /activeProfiles profiles profile idnexus/id repositories repository idnexus-public/id urlhttp://nexus.example.com:8081/repository/maven-public//url releasesenabledtrue/enabled/releases snapshotsenabledtrue/enabled/snapshots /repository /repositories /profile /profiles /settings关键理解mirrorOf为central表示拦截所有对 Maven Central 的请求转向 Nexus 私服。server.id必须与distributionManagement.repository.id一致Maven 才能根据 ID 匹配到认证信息进行发布。父 POM 统一管理版本在微服务项目中通常有一个父 POM 管理所有依赖版本各子模块继承即可parent-pom/pom.xmlXMLdependencyManagement dependencies !-- 团队内部模块 -- dependency groupIdcom.example.common/groupId artifactIdcommon-api/artifactId version${common.version}/version /dependency !-- Spring Boot -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-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-TypeEndpoint访问域名如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 SDKpom.xml — OSS 依赖XMLdependency groupIdcom.aliyun.oss/groupId artifactIdaliyun-sdk-oss/artifactId version3.17.4/version /dependencySpring Boot 整合 OSS — 文件上传服务OSS 客户端配置类首先创建 OSS 配置类初始化 OSSClient 实例config/OssConfig.javaJavapackage 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 配置YAMLoss: 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.javaJavapackage 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); } }文件上传 Controllercontroller/FileController.javaJavapackage 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 ResultString 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 ResultString getSignedUrl(RequestParam String key) { String url ossService.getSignedUrl(key, 60); // 60 分钟有效期 return Result.ok(url); } /** * 删除文件 */ DeleteMapping(/delete) public ResultVoid 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泄露风险太大而是通过STSSecurity Token Service获取临时凭证流程图STS 直传流程① 前端调用后端接口获取 STS Token → ② 后端调用阿里云 STS API 获取临时 AccessKeyId/Secret/SecurityToken → ③ 返回给前端 → ④ 前端用临时凭证直传文件到 OSS → ⑤ 前端上传完成后通知后端记录文件信息后端 STS 凭证签发接口service/StsService.javaJavapackage 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 SDKutils/ossUpload.jsJavaScriptimport 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 {Promisestring} - 文件的 objectNamekey */ 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 ResultString 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. 上传到 OSSavatar 目录 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 3template div classcover-upload el-upload classcover-uploader :action :http-requestcustomUpload :show-file-listfalse acceptimage/* :before-uploadbeforeUpload img v-ifcoverUrl :srccoverUrl classcover-image / el-icon v-else classcover-placeholder Plus / /el-icon /el-upload el-button v-ifcoverUrl typedanger sizesmall clickremoveCover移除封面/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 分钟。如果宿主机内存不足低于 1GBJVM 会因 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中serverid与pom.xml中distributionManagementrepositoryid不匹配。Maven 通过 ID 查找对应的认证信息。解决方案确保两边 ID 完全一致。例如pom.xml写idnexus-releases/idsettings.xml也必须写serveridnexus-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*- 暴露 HeadersETag, x-oss-request-id收获总结今日学习收获今天在 Day 18 中我们完成了两大块企业级基础设施的搭建Maven 私服Nexus理解了四种仓库类型的区别掌握了 Docker 部署 Nexus、settings.xml 镜像配置、制品发布流程、SNAPSHOT 与 RELEASE 版本管理。私服是团队协作的基础设施解决了依赖缓存、内部模块共享、第三方 JAR 管理三大痛点。阿里云 OSS 对象存储掌握了 OSS 的核心概念Bucket/Object/Endpoint/ACLSpring 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 配置、在线聊天/消息推送实战
返回列表