当前位置: 首页 > news >正文

java每日精进 5.18【文件存储】

1.文件存储思路

支持将文件上传到三类存储器:

  1. 兼容 S3 协议的对象存储:支持 MinIO、腾讯云 COS、七牛云 Kodo、华为云 OBS、亚马逊 S3 等等。
  2. 磁盘存储:本地、FTP 服务器、SFTP 服务器。
  3. 数据库存储:MySQL、Oracle、PostgreSQL、SQL Server 等等。

技术选型?

  • 优先,✔ 推荐方案 1。如果无法使用云服务,可以自己搭建一个 MinIO 服务。
  • 其次,推荐方案 3。数据库的主从机制可以实现高可用,备份也方便,少量小文件问题不大。
  • 最后,× 不推荐方案 2。主要是实现高可用比较困难,无法实现故障转移。

2.MinIo服务

1. 创建存储目录

命令

mkdir E:\youkeProject\Minio\data -Force

说明

  • 作用:在 E:\youkeProject\Minio\data 创建用于存储 MinIO 数据的目录,-Force 确保即使目录已存在也不会报错。
  • 注意事项
    • 确保 E:\ 磁盘存在且有足够空间(建议至少预留几 GB,视存储需求而定)。
    • 磁盘需有写入权限,当前用户必须能访问 E:\youkeProject。
  • 错误排查
    • 错误:mkdir : Access is denied(权限不足)
      • 解决:以管理员身份运行 PowerShell(右键 PowerShell,选择“以管理员身份运行”)。
      • 验证:运行 whoami 检查用户是否为管理员(如 NT AUTHORITY\SYSTEM 或 yourdomain\admin)。
    • 错误:mkdir : The device is not ready(磁盘不可用)
      • 解决:检查 E:\ 是否挂载(运行 Get-Disk 或 Get-Volume),确保磁盘可用。
      • 验证:运行 Test-Path E:\,返回 True 表示路径有效。

验证

Test-Path E:\youkeProject\Minio\data

  • 预期输出:True

2. 下载 MinIO 可执行文件

命令

Invoke-WebRequest -Uri "https://mirrors.tuna.tsinghua.edu.cn/minio/releases/windows-amd64/minio.exe" -OutFile "E:\youkeProject\Minio\minio.exe"

说明

  • 作用:从清华大学镜像源下载 MinIO 的 Windows 可执行文件,保存到 E:\youkeProject\Minio\minio.exe。
  • 注意事项
    • 确保网络连接稳定,清华大学镜像源通常比官方源更快。
    • 确认系统为 64 位 Windows(minio.exe 为 windows-amd64 架构)。
    • 下载路径 E:\youkeProject\Minio 必须存在且可写。
  • 错误排查
    • 错误:Invoke-WebRequest : The remote server returned an error: (404) Not Found
      • 解决:镜像源 URL 可能失效,尝试官方源:

        Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-amd64/minio.exe" -OutFile "E:\youkeProject\Minio\minio.exe"

      • 验证:检查 URL 是否可访问(在浏览器中打开)。
    • 错误:Invoke-WebRequest : Access is denied
      • 解决:以管理员身份运行 PowerShell,或检查 E:\youkeProject\Minio 目录权限:

        icacls "E:\youkeProject\Minio" /grant "Users:(W)"

    • 错误:网络连接失败
      • 解决:检查网络(运行 ping mirrors.tuna.tsinghua.edu.cn),或使用代理(如有):

        $env:HTTP_PROXY="http://proxy:port" $env:HTTPS_PROXY="http://proxy:port"

验证

Test-Path E:\youkeProject\Minio\minio.exe

  • 预期输出:True
  • 检查文件版本:

    (Get-Item E:\youkeProject\Minio\minio.exe).VersionInfo


3. 设置环境变量

命令

setx MINIO_ROOT_USER "admin" /M setx MINIO_ROOT_PASSWORD "password123" /M

说明

  • 作用:设置 MinIO 的管理员用户名和密码为系统环境变量,/M 表示设置系统级变量(需要管理员权限)。
  • 注意事项
    • MINIO_ROOT_PASSWORD 必须至少 8 位,建议包含大小写字母、数字和符号,例如 P@ssw0rd123。
    • 环境变量在当前 PowerShell 会话中不会立即生效,需重启 PowerShell 或系统。
    • 运行命令后,变量会存储在注册表(HKLM\System\CurrentControlSet\Control\Session Manager\Environment)。
  • 错误排查
    • 错误:setx : Access is denied
      • 解决:以管理员身份运行 PowerShell。
      • 验证:运行 whoami 确认用户为管理员。
    • 错误:密码不符合要求
      • 解决:确保密码长度 ≥ 8 位,包含复杂字符:

        setx MINIO_ROOT_PASSWORD "P@ssw0rd123" /M

    • 问题:环境变量未生效
      • 解决:重启 PowerShell 或运行以下命令刷新:

        $env:MINIO_ROOT_USER = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") $env:MINIO_ROOT_PASSWORD = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")

验证

[System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")

  • 预期输出:admin 和 P@ssw0rd123(或您设置的密码)

4. 注册为 Windows 服务

命令

# 创建服务
sc.exe create MinIO binPath= "E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001" start= auto displayname= "MinIO Object Storage"

# 启动服务
sc start MinIO

说明

  • 作用
    • sc.exe create:创建名为 MinIO 的 Windows 服务,指定可执行文件路径和参数。
    • binPath:运行 MinIO 服务器,数据目录为 E:\youkeProject\Minio\data,Web 控制台端口为 9001。
    • start= auto:服务随系统启动自动运行。
    • displayname:服务在服务管理器中的显示名称。
    • sc start:启动 MinIO 服务。
  • 注意事项
    • 必须以管理员身份运行 sc.exe。
    • 确保 E:\youkeProject\Minio\minio.exe 和 E:\youkeProject\Minio\data 路径正确。
    • 端口 9001(控制台)和 9000(API)不能被占用。
  • 错误排查
    • 错误:sc.exe : [SC] CreateService FAILED 5: Access is denied
      • 解决:以管理员身份运行 PowerShell。
    • 错误:sc.exe : [SC] CreateService FAILED 1053: The service did not respond
      • 解决:检查 binPath 是否正确,路径或文件可能有误:

        Test-Path E:\youkeProject\Minio\minio.exe Test-Path E:\youkeProject\Minio\data

      • 验证:手动运行命令检查错误:

        E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001

    • 错误:sc start : [SC] StartService FAILED 1057: The account name is invalid
      • 解决:确保服务以正确账户运行,默认使用 LocalSystem:

        sc.exe config MinIO obj= LocalSystem

    • 错误:端口被占用
      • 解决:检查端口 9000 和 9001:

        netstat -ano | findstr ":9000" netstat -ano | findstr ":9001"

      • 如果占用,修改端口(例如 --console-address :9002)并重新创建服务:

        sc.exe delete MinIO sc.exe create MinIO binPath= "E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9002" start= auto displayname= "MinIO Object Storage"

验证

Get-Service MinIO | Select-Object Name, Status, StartType

  • 预期输出:

    Name Status StartType ---- ------ --------- MinIO Running Automatic


5. 验证安装

步骤

  1. 访问 Web 管理界面
    • 打开浏览器,访问 http://localhost:9001。
    • 使用用户名 admin 和密码(例如 P@ssw0rd123)登录。
    • 登录成功后,应看到 MinIO 的 Web 控制台,可管理存储桶和文件。
  2. 检查服务状态

    Get-Service MinIO

    • 预期输出:Status 为 Running。

错误排查

  • 问题:无法访问 http://localhost:9001
    • 解决:
      • 确认服务是否运行:

        Get-Service MinIO

      • 检查端口是否监听:

        netstat -ano | findstr ":9001"

      • 如果端口未监听,尝试手动启动:

        E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001

      • 检查防火墙是否阻止访问(见步骤 6)。
  • 问题:登录失败
    • 解决:
      • 确认环境变量:

        [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")

      • 如果密码错误,重新设置:

        setx MINIO_ROOT_PASSWORD "P@ssw0rd123" /M

      • 重启服务:

        sc stop MinIO sc start MinIO

  • 问题:服务未启动
    • 解决:
      • 查看服务日志:

        Get-EventLog -LogName System -Newest 100 | Where-Object { $_.Source -eq "Service Control Manager" -and $_.Message -like "*MinIO*" } | Format-List

      • 检查 MinIO 日志(如果有文件输出):

        Get-Content -Path "E:\youkeProject\Minio\logs\minio.log" -Tail 10


6. 配置防火墙规则(可选)

命令

New-NetFirewallRule -DisplayName "MinIO Console" -Direction Inbound -Protocol TCP -LocalPort 9001 -Action Allow New-NetFirewallRule -DisplayName "MinIO API" -Direction Inbound -Protocol TCP -LocalPort 9000 -Action Allow

说明

  • 作用:开放 MinIO 的控制台端口(9001)和 API 端口(9000),允许外部访问。
  • 注意事项
    • 仅当需要从其他机器访问 MinIO 时执行。
    • 确保防火墙规则不会影响其他服务。
  • 错误排查
    • 错误:New-NetFirewallRule : Access is denied
      • 解决:以管理员身份运行 PowerShell。
    • 问题:规则未生效
      • 解决:检查防火墙状态:

        Get-NetFirewallProfile | Select-Object Name, Enabled

      • 启用防火墙(如果禁用):

        powershell

        Copy

        Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True

      • 验证规则:

        powershell

        Copy

        Get-NetFirewallRule -DisplayName "MinIO*" | Format-Table Name, DisplayName, Enabled, Direction, Action

验证

  • 从另一台机器访问 http://<your-ip>:9001 或 http://<your-ip>:9000。
  • 检查防火墙规则:

    Get-NetFirewallRule -DisplayName "MinIO*" | Format-List


7. 服务管理命令

命令

# 停止服务 sc stop MinIO

# 重启服务 sc restart MinIO

# 删除服务(卸载时使用) sc delete MinIO

说明

  • 作用
    • sc stop:停止 MinIO 服务。
    • sc restart:重启服务(等效于 stop 后 start)。
    • sc delete:移除 MinIO 服务(用于卸载)。
  • 注意事项
    • 所有命令需以管理员身份运行。
    • 删除服务前确保服务已停止:

      sc query MinIO

  • 错误排查
    • 错误:sc stop : [SC] ControlService FAILED 1062: The service has not been started
      • 解决:确认服务状态:

        Get-Service MinIO

      • 如果已停止,无需再次停止。
    • 错误:sc delete : [SC] DeleteService FAILED 1072: The specified service has been marked for deletion
      • 解决:等待几秒或重启系统后重试,或者手动删除注册表项(谨慎操作):

        Remove-Item -Path "HKLM:\System\CurrentControlSet\Services\MinIO" -Force

验证

  • 停止后:

    Get-Service MinIO | Select-Object Status

    • 预期输出:Stopped
  • 删除后:

    Get-Service MinIO -ErrorAction SilentlyContinue

    • 预期输出:无结果(服务不存在)

8. 使用 MinIO 客户端(mc)验证(可选)

步骤

  1. 安装 mc 客户端

    Invoke-WebRequest -Uri "https://dl.min.io/client/mc/release/windows-amd64/mc.exe" -OutFile "C:\Windows\mc.exe"

  2. 配置别名

    mc alias set myminio http://localhost:9000 admin P@ssw0rd123

  3. 列出存储桶

    mc ls myminio

错误排查

  • 错误:mc: Unable to initialize new alias
    • 解决:确认 MinIO 服务运行,端口 9000 可访问,用户名和密码正确。
    • 验证:

      Test-NetConnection -ComputerName localhost -Port 9000

  • 错误:mc: No such file or directory
    • 解决:确保 mc.exe 在 PATH 中:

      setx PATH "%PATH%;C:\Windows" /M

验证

  • 预期输出示例:

    [2025-05-18 16:00:00 JST] 0B mybucket/


9. 手动启动 MinIO(调试用)

命令

E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001

说明

  • 作用:在命令行手动启动 MinIO,用于调试或临时运行。
  • 注意事项
    • 需保持命令行窗口打开,关闭窗口会停止 MinIO。
    • 确保环境变量 MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD 已设置。
  • 错误排查
    • 错误:ERROR Unable to validate credentials
      • 解决:检查环境变量:

        $env:MINIO_ROOT_USER $env:MINIO_ROOT_PASSWORD

      • 手动指定:

        $env:MINIO_ROOT_USER="admin" $env:MINIO_ROOT_PASSWORD="P@ssw0rd123"

    • 错误:ERROR Unable to use the drive
      • 解决:检查 E:\youkeProject\Minio\data 权限:

        icacls "E:\youkeProject\Minio\data" /grant "Users:(F)"

验证

  • 访问 http://localhost:9001,登录控制台。
  • 检查日志输出,确认启动成功。

10.Java交互

pom:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version><relativePath/></parent><groupId>com.example</groupId><artifactId>minio-file-manager</artifactId><version>0.0.1-SNAPSHOT</version><name>minio-file-manager</name><description>Spring Boot MinIO File Management System</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.13</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

application.yml:

spring:servlet:multipart:max-file-size: 10MBmax-request-size: 10MB
minio:endpoint: http://127.0.0.1:9000access-key: adminsecret-key: password123bucket-name: bucketone

config:

@Configuration
public class MinioConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.access-key}")private String accessKey;@Value("${minio.secret-key}")private String secretKey;@Value("${minio.bucket-name}")private String bucketName;@Beanpublic MinioClient minioClient() throws Exception {// 创建 MinIO 客户端MinioClient minioClient = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();// 检查存储桶是否存在,不存在则创建boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (!bucketExists) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}return minioClient;}
}

comtroller:

// 标记为 Spring REST 控制器,返回 JSON 响应
@RestController
// 设置控制器基础路径,所有端点以 /files 开头
@RequestMapping("/files")
public class FileController {// 声明 MinIO 文件服务依赖,通过构造函数注入private final MinioFileService minioFileService;// 构造函数,注入 MinioFileService 实例public FileController(MinioFileService minioFileService) {this.minioFileService = minioFileService;}// 处理文件上传请求,POST /files/upload@PostMapping("/upload")// 接收上传的文件,绑定请求中的 file 字段,抛出异常由全局处理器处理public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception {// 调用服务层上传文件,返回生成的文件名return minioFileService.uploadFile(file);}// 处理文件下载请求,GET /files/download/{fileName}@GetMapping("/download/{fileName}")// 接收路径中的文件名,返回文件流响应public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String fileName) throws Exception {// 调用服务层获取文件输入流InputStream inputStream = minioFileService.downloadFile(fileName);// 构建 HTTP 响应,设置下载头和二进制流return ResponseEntity.ok()// 设置 Content-Disposition 头,提示浏览器下载文件.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")// 设置内容类型为通用二进制流.contentType(MediaType.APPLICATION_OCTET_STREAM)// 包装输入流为响应体.body(new InputStreamResource(inputStream));}// 处理文件删除请求,DELETE /files/{fileName}@DeleteMapping("/{fileName}")// 接收路径中的文件名,删除文件并返回提示public String deleteFile(@PathVariable String fileName) throws Exception {// 调用服务层删除文件minioFileService.deleteFile(fileName);// 返回删除成功的提示消息return "File deleted: " + fileName;}// 处理文件列表请求,GET /files/list@GetMapping("/list")// 返回存储桶中的所有文件名列表public List<String> listFiles() throws Exception {// 调用服务层获取文件列表return minioFileService.listFiles();}
}

service:

// 标记为 Spring 服务组件,封装 MinIO 文件操作
@Service
public class MinioFileService {// 声明 MinIO 客户端实例,用于与 MinIO 服务器交互private final MinioClient minioClient;// 从配置文件注入存储桶名称(如 application.yml 中的 minio.bucket-name)@Value("${minio.bucket-name}")private String bucketName;// 构造函数,注入 MinioClient 实例public MinioFileService(MinioClient minioClient) {this.minioClient = minioClient;}// 上传文件到 MinIO 存储桶public String uploadFile(MultipartFile file) throws Exception {// 生成唯一文件名:时间戳 + 原始文件名,防止冲突String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();// 调用 MinIO 客户端上传文件minioClient.putObject(// 配置上传参数PutObjectArgs.builder()// 指定存储桶.bucket(bucketName)// 指定对象名(文件名).object(fileName)// 设置文件流、大小和分片阈值(-1 表示自动分片).stream(file.getInputStream(), file.getSize(), -1)// 设置文件 MIME 类型.contentType(file.getContentType()).build());// 返回上传后的文件名return fileName;}// 从 MinIO 下载文件,返回输入流public InputStream downloadFile(String fileName) throws Exception {// 调用 MinIO 客户端获取文件流return minioClient.getObject(// 配置下载参数GetObjectArgs.builder()// 指定存储桶.bucket(bucketName)// 指定对象名(文件名).object(fileName).build());}// 从 MinIO 删除指定文件public void deleteFile(String fileName) throws Exception {// 调用 MinIO 客户端删除文件minioClient.removeObject(// 配置删除参数RemoveObjectArgs.builder()// 指定存储桶.bucket(bucketName)// 指定对象名(文件名).object(fileName).build());}// 列出存储桶中的所有文件名public List<String> listFiles() throws Exception {// 创建文件名列表List<String> fileNames = new ArrayList<>();// 调用 MinIO 客户端列出存储桶中的对象Iterable<Result<Item>> results = minioClient.listObjects(// 配置列表参数ListObjectsArgs.builder().bucket(bucketName).build());// 迭代对象列表,提取文件名for (Result<Item> result : results) {fileNames.add(result.get().objectName());}// 返回文件名列表return fileNames;}
}

错误排查总结

以下是常见错误及其解决方法的汇总:

错误可能原因解决方法
mkdir : Access is denied权限不足以管理员身份运行 PowerShell。
Invoke-WebRequest : 404 Not Found镜像源失效使用官方源或检查 URL。
setx : Access is denied非管理员运行以管理员身份运行 PowerShell。
sc.exe : CreateService FAILED 1053binPath 错误验证路径和文件存在,尝试手动运行命令。
sc start : StartService FAILED 1057服务账户无效设置服务账户为 LocalSystem。
无法访问 http://localhost:9001服务未启动/端口被占检查服务状态、端口占用,开放防火墙规则。
登录失败用户名/密码错误验证环境变量,重新设置密码并重启服务。
mc: Unable to initialize alias服务不可达确认 MinIO 运行,端口 9000 可访问。

完整验证流程

  1. 检查目录和文件

    Test-Path E:\youkeProject\Minio\minio.exe Test-Path E:\youkeProject\Minio\data

  2. 检查环境变量

    [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")

  3. 检查服务状态

    Get-Service MinIO | Select-Object Name, Status, StartType

  4. 访问 Web 界面
    • 浏览器打开 http://localhost:9001,登录。
  5. 使用 mc 客户端

    mc alias set myminio http://localhost:9000 admin P@ssw0rd123 mc ls myminio


注意事项

  • 管理员权限:所有命令需在管理员权限的 PowerShell 中运行。
  • 密码安全:MINIO_ROOT_PASSWORD 需满足 ≥8 位,包含大小写、数字和符号,避免弱密码(如 password123)。
  • 端口冲突:检查 9000 和 9001 端口是否被占用,若冲突可修改为其他端口(如 9002)。
  • 日志查看
    • MinIO 默认日志可能在 E:\youkeProject\Minio\logs\minio.log:

      Get-Content -Path "E:\youkeProject\Minio\logs\minio.log" -Tail 10

    • 检查 Windows 事件日志(如果配置了事件日志输出):

      Get-WinEvent -LogName System -MaxEvents 100 | Where-Object { $_.ProviderName -like "*minio*" } | Format-List

  • 备份数据:定期备份 E:\youkeProject\Minio\data 中的数据,避免意外丢失。

总结

通过以上步骤,您可以在 Windows 系统上成功安装并启动 MinIO 作为服务,数据存储在 E:\youkeProject\Minio\data,Web 控制台通过 http://localhost:9001 访问。

3.系统实现

3.1文件上传代码实现

3.1.1 方式一:前端上传

前端代码(Vue 组件):

  • 文件:InfraFile.vue 和头像上传组件(未命名,假设为 AvatarUpload.vue)。
  • 功能
    • 文件列表展示:显示文件列表(el-table),支持搜索(路径、创建时间)、分页、删除。
    • 文件上传:通过 <el-upload> 组件实现文件上传,支持拖拽、类型限制(.jpg, .png, .gif)。
    • 头像上传:使用 vue-cropper 裁剪图片后上传。
  • 关键代码
    • 文件上传
      <el-uploadref="upload":limit="1"accept=".jpg, .png, .gif":auto-upload="false":headers="upload.headers":action="upload.url":data="upload.data":on-change="handleFileChange":on-progress="handleFileUploadProgress":on-success="handleFileSuccess"><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div><div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入 jpg、png、gif 格式文件!</div>
      </el-upload>
      • 作用:用户选择文件后,点击“确定”按钮触发上传,发送 POST /admin-api/infra/file/upload 请求。
      • 配置
        • action:上传接口 URL(/admin-api/infra/file/upload)。
        • headers:携带认证 token(Authorization: Bearer xxx)。
        • data:附加参数(如路径)。
        • on-success:处理上传成功的响应,显示 URL 并刷新列表。
    • 头像上传
      <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"><el-button size="small">选择<i class="el-icon-upload el-icon--right"></i></el-button>
      </el-upload>
      • 作用:选择图片后裁剪,上传到 /admin-api/system/user/avatar。
      • 配置
        • http-request:自定义上传逻辑,调用 uploadAvatar API。
        • before-upload:验证文件类型(必须为图片)。
        • uploadImg:将裁剪后的图片作为 FormData 上传。
  • 后端代码(FileController.java):
    @PostMapping("/upload")
    @Operation(summary = "上传文件")
    public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {MultipartFile file = uploadReqVO.getFile();String path = uploadReqVO.getPath();return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
    }
    • 作用:接收前端上传的 MultipartFile 和路径参数,调用 fileService.createFile 存储文件,返回文件 URL。
    • 流程
      1. 解析 uploadReqVO 获取文件和路径。
      2. 使用 IoUtil.readBytes 转换文件流为字节数组。
      3. 调用 fileService.createFile 上传文件到 MinIO,返回 URL。
      4. 封装响应为 CommonResult<String>。
      5. 方法详解
  • 文件上传:createFile 方法实现文件的上传,接收文件名、路径和内容,上传到存储器并保存元数据到数据库,返回文件访问 URL。
  • 文件客户端管理
    • getMasterFileClient:获取主文件客户端(FileClient),从缓存中加载。
    • clientCache:缓存文件客户端,支持异步刷新,减少数据库查询。
  • 文件客户端工厂
    • FileClientFactoryImpl:管理文件客户端的创建和更新,基于配置动态生成客户端实例(如 S3FileClient、LocalFileClient)。
    • 支持多种存储器,通过 FileStorageEnum 映射存储类型到具体客户端类。

3.1.2调用关系

  1. 外部调用:createFile 方法由上层(如 FileController 或其他服务)调用,用于上传文件。
  2. 内部调用
    • createFile 调用 fileConfigService.getMasterFileClient 获取客户端。
    • getMasterFileClient 通过 clientCache 获取缓存的 FileClient。
    • clientCache 的 load 方法调用 fileConfigService 和 fileClientFactory 创建或更新客户端。
    • FileClientFactoryImpl 的 createOrUpdateFileClient 和 getFileClient 方法管理客户端实例。
  3. 依赖
    • FileConfigService:提供存储配置查询。
    • FileClientFactory:创建和管理文件客户端。
    • FileMapper:操作数据库,保存文件元数据。

3.1.3主要类和接口

  • FileServiceImpl:文件服务实现类,包含 createFile 和 getMasterFileClient。
  • FileClient:文件客户端接口,定义上传、删除、获取内容等方法。
  • FileClientFactoryImpl:文件客户端工厂,动态创建客户端。
  • AbstractFileClient:抽象文件客户端,提供通用逻辑。
  • FileConfigDO:文件配置实体,存储存储器配置。
  • FileDO:文件元数据实体,存储文件名、路径、URL 等。

3.1.4代码逐行分析与调用链

3.1.4.1 FileServiceImpl.createFile

@Override @SneakyThrows public String createFile(String name, String path, byte[] content) {

  • 作用:上传文件到存储器,保存元数据到数据库,返回文件 URL。
  • 入参
    • name:文件名(如 image.jpg)。
    • path:存储路径(如 /avatars/image.jpg)。
    • content:文件内容(字节数组)。
  • 返回值:String,文件访问 URL(如 http://minio.example.com/mybucket/avatars/image.jpg)。
  • 注解
    • @Override:实现 FileService 接口的 createFile 方法。
    • @SneakyThrows:Lombok 注解,简化异常处理,抛出 Exception。
  • 调用者:FileController(前端上传)、其他服务(如后端上传)。
  • 调用链:FileController.uploadFile -> FileServiceImpl.createFile。

String type = FileTypeUtils.getMineType(content, name);

  • 作用:获取文件的 MIME 类型(如 image/jpeg)。
  • 逻辑:使用 FileTypeUtils(可能是 Hutool 或自定义工具)分析文件内容和名称。
  • 调用:FileTypeUtils.getMineType(静态方法)。
  • 示例:name="image.jpg", content 为 JPEG 数据,返回 image/jpeg。

if (StrUtil.isEmpty(path)) { path = FileUtils.generatePath(content, name); }

  • 作用:如果 path 为空,生成默认存储路径。
  • 逻辑
    • StrUtil.isEmpty:Hutool 工具,检查 path 是否为空。
    • FileUtils.generatePath:生成路径,可能基于文件名或日期(如 /2025/05/image.jpg)。
  • 调用:FileUtils.generatePath(静态方法)。
  • 示例:name="image.jpg", 返回 /2025/05/image.jpg。

if (StrUtil.isEmpty(name)) { name = path; }

  • 作用:如果 name 为空,使用 path 作为文件名。
  • 逻辑:确保文件名有效,避免空值。
  • 示例:name="", path="/avatars/image.jpg", 设置 name="/avatars/image.jpg"。

FileClient client = fileConfigService.getMasterFileClient();

  • 作用:获取主文件客户端(FileClient),用于上传文件。
  • 调用:FileConfigService.getMasterFileClient,实际调用 FileServiceImpl.getMasterFileClient。
  • 返回值:FileClient 实例(如 S3FileClient)。
  • 调用链:createFile -> getMasterFileClient -> clientCache.getUnchecked。

Assert.notNull(client, "客户端(master) 不能为空");

  • 作用:校验客户端是否为空,若为空抛出异常。
  • 逻辑:Spring 的 Assert 工具,确保 client 有效。
  • 示例:若 client=null,抛出 IllegalArgumentException: 客户端(master) 不能为空。

String url = client.upload(content, path, type);

  • 作用:调用文件客户端上传文件,返回文件 URL。
  • 调用:FileClient.upload,由具体实现(如 S3FileClient)处理。
  • 入参
    • content:文件内容。
    • path:存储路径。
    • type:MIME 类型。
  • 返回值:文件 URL。
  • 示例:上传 image.jpg,返回 http://minio.example.com/mybucket/avatars/image.jpg。

FileDO file = new FileDO();

  • 作用:创建文件元数据实体,准备保存到数据库。
  • 逻辑:FileDO 是文件表对应的实体类,包含配置 ID、名称、路径等字段。

file.setConfigId(client.getId());

  • 作用:设置文件配置 ID。
  • 调用:FileClient.getId,返回客户端的配置 ID。
  • 示例:client.getId() 返回 1(主配置 ID)。

file.setName(name); file.setPath(path); file.setUrl(url); file.setType(type); file.setSize(content.length);

  • 作用:设置文件元数据,包括文件名、路径、URL、类型和大小。
  • 示例
    • name="image.jpg"
    • path="/avatars/image.jpg"
    • url="http://minio.example.com/mybucket/avatars/image.jpg"
    • type="image/jpeg"
    • size=102400(100KB)

fileMapper.insert(file);

  • 作用:将文件元数据插入数据库。
  • 调用:FileMapper.insert,MyBatis 的 Mapper 方法。
  • 逻辑:保存 FileDO 到 infra_file 表。
  • 示例:插入记录,生成自增 ID。

return url;

  • 作用:返回文件 URL,供调用者使用。
  • 示例:返回 http://minio.example.com/mybucket/avatars/image.jpg。
3.1.4.2 FileServiceImpl.getMasterFileClient

@Override public FileClient getMasterFileClient() {

  • 作用:获取主文件客户端,从缓存中加载。
  • 返回值:FileClient 实例。
  • 调用者:createFile 方法。
  • 调用链:createFile -> getMasterFileClient -> clientCache.getUnchecked。

return clientCache.getUnchecked(CACHE_MASTER_ID);

  • 作用:从缓存获取主客户端,CACHE_MASTER_ID 表示主配置 ID(通常为固定值,如 0)。
  • 调用:LoadingCache.getUnchecked,Guava 缓存方法。
  • 逻辑:若缓存命中,直接返回;若未命中,调用缓存的 load 方法。
  • 示例:返回 S3FileClient 实例。
3.1.4.3 FileServiceImpl.clientCache

@Getter private final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),

  • 作用:定义文件客户端缓存,支持异步刷新。
  • 字段
    • clientCache:Guava 的 LoadingCache,键为配置 ID,值为 FileClient。
    • Duration.ofSeconds(10L):缓存刷新间隔为 10 秒。
  • 逻辑:缓存避免频繁查询数据库或创建客户端。
  • 调用:buildAsyncReloadingCache
  • 调用者:getMasterFileClient

new CacheLoader<Long, FileClient>() {

  • 作用:定义缓存加载器,当缓存未命中时加载 FileClient。
  • 逻辑:实现 load 方法,动态创建客户端。

@Override public FileClient load(Long id) {

  • 作用:加载指定 ID 的文件客户端。
  • 入参:id,配置 ID(CACHE_MASTER_ID 表示主配置)。
  • 返回值:FileClient 实例。
  • 调用者:clientCache.getUnchecked。

FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ? fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);

  • 作用:查询存储配置。
  • 逻辑
    • 若 id 是 CACHE_MASTER_ID,调用 fileConfigMapper.selectByMaster 获取主配置。
    • 否则,调用 fileConfigMapper.selectById 获取指定 ID 的配置。
  • 调用
    • fileConfigMapper.selectByMaster:MyBatis 查询主配置。
    • fileConfigMapper.selectById:MyBatis 查询指定配置。
  • 示例:返回 FileConfigDO(包含 endpoint、bucket 等)。

if (config != null) { fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); }

  • 作用:若配置存在,创建或更新文件客户端。
  • 调用:FileClientFactory.createOrUpdateFileClient。
  • 入参
    • config.getId():配置 ID。
    • config.getStorage():存储类型(如 S3、本地)。
    • config.getConfig():存储配置(如 MinIO 的 endpoint、accessKey)。
  • 示例:创建 S3FileClient。

return fileClientFactory.getFileClient(null == config ? id : config.getId());

  • 作用:获取文件客户端。
  • 调用:FileClientFactory.getFileClient。
  • 逻辑
    • 若 config 为空,使用原始 id。
    • 否则,使用 config.getId()。
  • 示例:返回 S3FileClient。
3.1.4.4 FileClientFactoryImpl

@Slf4j public class FileClientFactoryImpl implements FileClientFactory {

  • 作用:文件客户端工厂实现类,管理客户端的创建和更新。
  • 注解
    • @Slf4j:Lombok 注解,提供日志记录器(log)。
  • 调用者:clientCache.load。

private final ConcurrentMap<Long, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();

  • 作用:存储文件客户端实例,键为配置 ID,值为客户端。
  • 逻辑:ConcurrentHashMap 确保线程安全。

@Override public FileClient getFileClient(Long configId) {

  • 作用:获取指定配置 ID 的文件客户端。
  • 入参:configId,配置 ID。
  • 返回值:FileClient 实例。
  • 调用者:clientCache.load。

AbstractFileClient<?> client = clients.get(configId);

  • 作用:从 clients 映射获取客户端。
  • 示例:configId=1,返回 S3FileClient。

if (client == null) { log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); }

  • 作用:若客户端不存在,记录错误日志。
  • 示例:configId=999,日志输出 [getFileClient][配置编号(999) 找不到客户端]。

return client;

  • 作用:返回客户端实例(可能为 null)。
  • 示例:返回 S3FileClient。

@Override @SuppressWarnings("unchecked") public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {

  • 作用:创建或更新文件客户端。
  • 入参
    • configId:配置 ID。
    • storage:存储类型(如 S3、本地)。
    • config:存储配置(泛型,子类如 S3FileClientConfig)。
  • 调用者:clientCache.load。
  • 注解:@SuppressWarnings("unchecked") 抑制类型转换警告。

AbstractFileClient<Config> client = (AbstractFileClient<Config>) clients.get(configId);

  • 作用:尝试从 clients 获取现有客户端。
  • 逻辑:强制类型转换为 AbstractFileClient<Config>。

if (client == null) { client = this.createFileClient(configId, storage, config); client.init(); clients.put(client.getId(), client);

  • 作用:若客户端不存在,创建新客户端。
  • 逻辑
    • 调用 createFileClient 创建客户端。
    • 调用 client.init 初始化客户端(如连接 MinIO)。
    • 将客户端存入 clients 映射。
  • 调用
    • createFileClient
    • AbstractFileClient.init

} else { client.refresh(config); }

  • 作用:若客户端存在,刷新配置。
  • 调用:AbstractFileClient.refresh。
  • 示例:更新 MinIO 的 endpoint 或 accessKey。

@SuppressWarnings("unchecked") private <Config extends FileClientConfig> AbstractFileClient<Config> createFileClient( Long configId, Integer storage, Config config) {

  • 作用:创建文件客户端实例。
  • 入参
    • configId:配置 ID。
    • storage:存储类型。
    • config:存储配置。
  • 返回值:AbstractFileClient<Config>。
  • 调用者:createOrUpdateFileClient。

FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage);

  • 作用:根据存储类型获取枚举值。
  • 调用:FileStorageEnum.getByStorage(静态方法)。
  • 示例:storage=1,返回 FileStorageEnum.S3。

Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum));

  • 作用:校验存储类型是否有效。
  • 示例:若 storageEnum=null,抛出 IllegalArgumentException: 文件配置(null) 为空。

return (AbstractFileClient<Config>) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config);

  • 作用:创建客户端实例。
  • 调用
    • FileStorageEnum.getClientClass:获取客户端类(如 S3FileClient.class)。
    • ReflectUtil.newInstance:Hutool 工具,通过反射创建实例。
  • 逻辑:调用客户端构造函数,传入 configId 和 config。
  • 示例:storageEnum=S3,创建 S3FileClient。
3.1.4.5 整体调用流程
  1. 外部调用
    • FileController.uploadFile 调用 FileServiceImpl.createFile,传递文件名、路径和内容。
  2. 文件上传(createFile)
    • 计算 MIME 类型(FileTypeUtils.getMineType)。
    • 生成默认路径(FileUtils.generatePath)。
    • 获取主客户端(getMasterFileClient)。
    • 上传文件(FileClient.upload)。
    • 保存元数据(FileMapper.insert)。
    • 返回 URL。
  3. 获取客户端(getMasterFileClient)
    • 从 clientCache 获取客户端(clientCache.getUnchecked)。
  4. 缓存加载(clientCache.load)
    • 查询配置(fileConfigMapper.selectByMaster 或 selectById)。
    • 创建或更新客户端(FileClientFactory.createOrUpdateFileClient)。
    • 返回客户端(FileClientFactory.getFileClient)。
  5. 客户端工厂(FileClientFactoryImpl)
    • getFileClient:从 clients 获取客户端。
    • createOrUpdateFileClient:
      • 若客户端不存在,调用 createFileClient 创建。
      • 若存在,调用 refresh 更新。
    • createFileClient:通过反射创建客户端实例(如 S3FileClient)。

3.1.4.6 每一步作用总结

代码部分作用调用关系
createFile上传文件,保存元数据,返回 URL调用 getMasterFileClient, FileClient.upload, FileMapper.insert
getMasterFileClient获取主文件客户端调用 clientCache.getUnchecked
clientCache缓存文件客户端,支持异步刷新调用 load(查询配置,创建客户端)
clientCache.load加载客户端,查询配置并创建调用 fileConfigMapper, fileClientFactory.createOrUpdateFileClient
FileClientFactoryImpl.getFileClient获取客户端实例从 clients 获取
createOrUpdateFileClient创建或更新客户端调用 createFileClient, AbstractFileClient.init/refresh
createFileClient通过反射创建客户端调用 FileStorageEnum, Refl
  • 前端主导:用户通过浏览器界面选择文件,触发 HTTP 请求,上传过程由前端组件控制(<el-upload>)。
  • 后端辅助:后端仅负责接收文件、存储到 MinIO、返回 URL,不主动发起上传。
  • 特点:上传流程由前端用户交互驱动,适合需要用户选择文件的场景(如上传头像、文档)。

3.1.5方式二:后端上传

后端代码:

  • 文件:FileApiImpl.java 和 FileServiceImpl.java。
  • 功能
    • 提供文件管理 API,包括创建、删除、查询文件内容和生成签名 URL。
    • 支持直接通过字节数组上传文件(无需 MultipartFile)。
  • 关键代码
    • FileApiImpl
      /*** 文件 API 实现类*/
      @Service
      @Validated
      public class FileApiImpl implements FileApi {@Resourceprivate FileService fileService;@Overridepublic String createFile(String name, String path, byte[] content) {return fileService.createFile(name, path, content);}}
      • 作用:实现 FileApi 接口,提供 createFile 方法,接收文件名、路径和字节数组,调用 fileService 上传文件。
    • FileServiceImpl
      @Override@SneakyThrowspublic String createFile(String name, String path, byte[] content) {// 计算默认的 path 名String type = FileTypeUtils.getMineType(content, name);if (StrUtil.isEmpty(path)) {path = FileUtils.generatePath(content, name);}// 如果 name 为空,则使用 path 填充if (StrUtil.isEmpty(name)) {name = path;}// 上传到文件存储器FileClient client = fileConfigService.getMasterFileClient();Assert.notNull(client, "客户端(master) 不能为空");String url = client.upload(content, path, type);// 保存到数据库FileDO file = new FileDO();file.setConfigId(client.getId());file.setName(name);file.setPath(path);file.setUrl(url);file.setType(type);file.setSize(content.length);fileMapper.insert(file);return url;}
      • 作用:处理文件上传逻辑,保存文件到 MinIO,记录元数据到数据库,返回 URL。
      • 流程
        1. 推断文件类型(FileTypeUtils.getMineType)。
        2. 生成默认路径(如果未提供)。
        3. 获取 MinIO 客户端(FileClient),上传文件(client.upload)。
        4. 保存文件元数据(FileDO)到数据库。
        5. 返回文件 URL。

解析

  • 后端主导:上传由后端代码或服务调用触发,文件内容以字节数组形式传入,无需用户通过浏览器选择文件。
  • 特点:适合服务器端批量处理、自动上传或从其他来源(如本地文件、URL 下载)获取文件的场景。
  • 无前端交互:不依赖浏览器,直接由后端 API 或服务调用。

3.1.6. 回答问题

前端上传和后端上传的区别
  1. 触发方式
    • 前端上传:由用户通过浏览器界面(<el-upload>、裁剪组件)选择文件,触发 HTTP 请求。
      • 场景:用户上传头像、文档、图片等。
      • 示例:用户点击“上传文件”按钮,发送 POST /admin-api/infra/file/upload。
    • 后端上传:由后端代码或服务调用触发,文件内容以字节数组形式传入,通常从服务器本地、数据库或其他来源获取。
      • 场景:批量导入文件、服务器端文件处理、从 URL 下载并上传。
      • 示例:后端从本地磁盘读取文件,调用 fileService.createFile。
  2. 文件来源
    • 前端上传:文件来自用户设备(通过浏览器选择)。
      • 示例:用户选择 C:\test.jpg。
    • 后端上传:文件来自服务器环境(如本地文件、远程 URL、数据库)。
      • 示例:后端读取 /tmp/test.jpg 或从 URL 下载文件。
  3. 请求格式
    • 前端上传:使用 multipart/form-data 格式,文件通过 MultipartFile 传输。
      • 示例:form-data: file=(binary), path=/avatars/.
    • 后端上传:文件以字节数组(byte[])传入,通常通过内部方法调用或 API(如 FileApi.createFile)。
      • 示例:createFile("test.jpg", "/avatars/", fileBytes)。
  4. 用户交互
    • 前端上传:需要用户交互(如选择文件、点击上传),前端提供界面和反馈(如进度条、成功提示)。
      • 示例:<el-upload> 显示上传进度,成功后弹出 上传成功。
    • 后端上传:无用户交互,后端自动处理,适合后台任务。
      • 示例:定时任务批量上传文件。
  5. 适用场景
    • 前端上传:用户驱动的场景,如个人中心上传头像、文件管理系统上传文档。
    • 后端上传:系统驱动的场景,如服务器迁移文件、API 集成、自动化脚本。

命名原因

  • 前端上传:被称为“前端上传”,因为上传流程由前端用户交互发起,文件通过前端组件(如 <el-upload>)选择并发送到后端。后端仅处理请求,核心动作(文件选择、触发上传)发生在前端。
  • 后端上传:被称为“后端上传”,因为上传由后端代码或服务主动调用,文件来源和上传逻辑完全由后端控制,无需前端参与。核心动作(文件读取、上传)发生在后端。

3.2 文件下载

// 标记为 Spring REST 控制器,返回 JSON 或文件流响应
@RestController
// 设置日志记录器
@Slf4j
public class FileController {// 处理文件下载请求,GET /admin-api/infra/file/{configId}/get/**@GetMapping("/{configId}/get/**")// 允许未认证用户访问@PermitAll// Swagger 文档:接口描述@Operation(summary = "下载文件")// Swagger 文档:参数描述@Parameter(name = "configId", description = "配置编号", required = true)// 接收请求、响应和配置 ID,处理文件下载public void getFileContent(HttpServletRequest request,HttpServletResponse response,@PathVariable("configId") Long configId) throws Exception {// 从请求 URI 中提取文件路径(/get/ 之后的部分)String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);// 校验路径是否为空,若为空抛出异常if (StrUtil.isEmpty(path)) {throw new IllegalArgumentException("结尾的 path 路径必须传递");}// 解码路径,解决中文路径的编码问题path = URLUtil.decode(path);// 调用文件服务获取文件内容byte[] content = fileService.getFileContent(configId, path);// 若文件不存在,记录警告日志并返回 404 状态if (content == null) {log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);response.setStatus(HttpStatus.NOT_FOUND.value());return;}// 返回文件内容作为附件FileTypeUtils.writeAttachment(response, path, content);}
}// Hutool 的 StrUtil 工具类,提供字符串操作
class StrUtil {// 从字符串中提取指定分隔符后的子串public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) {// 若输入字符串为空,返回空或 nullif (isEmpty(string)) {return null == string ? null : "";} else if (separator == null) {// 若分隔符为空,返回空字符串return "";} else {// 转换为字符串String str = string.toString();String sep = separator.toString();// 根据 isLastSeparator 决定使用最后一个或第一个分隔符int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);// 若找到分隔符且不是字符串末尾,返回分隔符后的子串return -1 != pos && string.length() - 1 != pos ? str.substring(pos + separator.length()) : "";}}// 检查字符串是否为空(Hutool 工具方法)public static boolean isEmpty(CharSequence str) {return str == null || str.length() == 0;}
}// 文件服务接口实现类
class FileServiceImpl {// 获取文件内容@Overridepublic byte[] getFileContent(Long configId, String path) throws Exception {// 获取指定配置 ID 的文件客户端FileClient client = fileConfigService.getFileClient(configId);// 校验客户端是否存在Assert.notNull(client, "客户端({}) 不能为空", configId);// 调用客户端获取文件内容return client.getContent(path);}
}// Servlet 工具类,提供响应处理方法
class ServletUtils {// 将文件内容作为附件写入响应public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {// 设置 Content-Disposition 头,指定文件名(UTF-8 编码)response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtil.encodeUtf8(filename));// 获取文件 MIME 类型String contentType = FileTypeUtils.getMineType(content, filename);// 设置响应内容类型response.setContentType(contentType);// 针对视频文件的特殊处理,解决移动端播放兼容性if (StrUtil.containsIgnoreCase(contentType, "video")) {response.setHeader("Content-Length", String.valueOf(content.length - 1));response.setHeader("Content-Range", String.valueOf(content.length - 1));response.setHeader("Accept-Ranges", "bytes");}// 将文件内容写入响应输出流IoUtil.write(response.getOutputStream(), false, content);}
}

3.3 文件客户端

// 定义文件客户端接口,抽象文件操作方法,支持多种存储器(如 S3、本地磁盘、数据库等)
public interface FileClient {/*** 获取客户端编号** @return 客户端编号,用于标识存储配置(如 MinIO、S3 的配置 ID)*/Long getId();/*** 上传文件到存储器** @param content 文件内容,字节数组形式* @param path 相对路径,指定文件在存储器中的位置(如 "/avatars/image.jpg")* @return 完整路径,即文件的 HTTP 访问地址(如 "http://minio.example.com/mybucket/avatars/image.jpg")*/String upload(byte[] content, String path);/*** 从存储器删除指定文件** @param path 相对路径,指定要删除的文件位置(如 "/avatars/image.jpg")*/void delete(String path);/*** 获取存储器中指定文件的内容** @param path 相对路径,指定要读取的文件位置(如 "/avatars/image.jpg")* @return 文件内容,字节数组形式,若文件不存在可能返回 null 或抛出异常*/byte[] getContent(String path);}

4.前端直传S3存储

1. S3 存储和前端直传 S3 的解释

1.1 什么是 S3 存储?

  • 定义:S3(Simple Storage Service)是 Amazon 提供的一种对象存储服务,广泛用于存储文件(如图片、视频、文档)。七牛云、阿里云 OSS、腾讯云 COS 等提供了 S3 兼容的存储服务,允许使用类似 AWS S3 的 API 操作文件。
  • 核心概念
    • Bucket:存储文件的容器,类似文件夹,名称全局唯一。
    • Object:存储的文件,每个对象有唯一的 Key(路径,如 avatars/image.jpg)。
    • URL 访问:文件通过 HTTP URL 访问(如 http://bucket.qiniucs.com/avatars/image.jpg)。
  • 特点
    • 高可用性:数据多副本存储,耐久性达 99.999999999%。
    • 可扩展性:支持无限存储容量,适合大文件和海量数据。
    • 安全性:通过访问密钥(Access Key/Secret Key)和权限策略控制访问。
  • 适用场景:用户头像上传、视频存储、静态网站托管、数据备份等。

1.2 什么是前端直传 S3?

  • 定义:前端直传 S3 是指前端(浏览器或客户端)直接将文件上传到 S3 存储(如七牛云),而不通过后端服务器中转。相比传统方式(前端 → 后端 → S3),它减少了后端带宽压力。
  • 传统上传(前端 → 后端 → S3)
    • 流程:前端将文件发送到后端,后端再上传到 S3。
    • 问题:文件流量经过后端,若后端带宽有限(如 1MB/s),上传大文件(如 10MB)会很慢(需 10 秒),多用户上传可能导致带宽瓶颈。
  • 前端直传(前端 → S3)
    • 流程:
      1. 前端向后端请求预签名 URL(Presigned URL),包含临时访问权限。
      2. 前端使用预签名 URL 直接上传文件到 S3。
      3. 上传成功后,通知后端记录文件信息(如 URL、路径)。
    • 优势:
      • 速度快:文件直接上传到 S3,利用用户带宽(如 100MB/s,10MB 文件只需 0.1 秒)。
      • 减轻后端压力:后端仅处理轻量请求(如生成预签名 URL),无需传输大文件。
      • 高并发:S3 天然支持高并发上传,适合多用户场景。
  • 适用场景:大文件上传(如视频、图片)、高并发上传(如社交平台用户上传头像)。

1.3 七牛云 S3 存储的特点

  • 七牛云提供 S3 兼容的对象存储服务,支持 AWS S3 的 API 和 SDK。
  • 需要配置:
    • Endpoint:存储服务地址(如 s3-cn-south-1.qiniucs.com)。
    • Bucket:存储桶名称。
    • Access Key/Secret Key:用于认证的密钥。
    • Domain:自定义访问域名(如 http://bucket.qiniucs.com)。
  • 七牛云要求配置自定义域名(domain),否则无法生成可访问的 URL。

2. 代码分析

2.1 前端代码(yudao-ui-admin-vue3)

前端代码基于 Vue3 和 Element Plus,使用 ElUpload 组件实现文件上传,支持两种模式:前端直传(client)和后端上传(server)。

关键代码:useUpload 方法
export const useUpload = () => {const uploadUrl = getUploadUrl() // 获取上传 URLconst isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE // 判断是否前端直传// 重写 ElUpload 的上传方法const httpRequest = async (options: UploadRequestOptions) => {if (isClientUpload) {// 模式一:前端直传// 1. 生成唯一文件名(基于 SHA256)const fileName = await generateFileName(options.file)// 2. 请求后端获取预签名 URLconst presignedInfo = await FileApi.getFilePresignedUrl(fileName)// 3. 直接上传文件到 S3return axios.put(presignedInfo.uploadUrl, options.file, {headers: { 'Content-Type': options.file.type }}).then(() => {// 4. 异步记录文件信息到后端createFile(presignedInfo, fileName, options.file)// 返回与后端上传一致的格式return { data: presignedInfo.url }})} else {// 模式二:后端上传return new Promise((resolve, reject) => {FileApi.updateFile({ file: options.file }).then((res) => {if (res.code === 0) resolve(res)else reject(res)}).catch((res) => reject(res))})}}return { uploadUrl, httpRequest }
}
  • 逻辑
    1. 检查模式:通过 VITE_UPLOAD_TYPE 判断是否为 client 模式。
    2. 生成文件名:使用 generateFileName 计算文件的 SHA256 哈希值,拼接后缀(如 .jpg),生成唯一文件名。
    3. 获取预签名 URL:调用后端 /presigned-url 接口,获取上传用的临时 URL。
    4. 上传文件:使用 axios.put 直接将文件上传到预签名 URL,设置 Content-Type 为文件类型。
    5. 记录文件:上传成功后,调用 createFile 通知后端保存文件信息(如 URL、路径)。
  • 注意
    • 不使用 FormData 上传,因为 MinIO(或七牛云)不支持 multipart/form-data 格式。
    • 返回格式与后端上传一致,确保 ElUpload 组件兼容。
生成文件名:generateFileName
async function generateFileName(file: UploadRawFile) {const data = await file.arrayBuffer() // 读取文件内容const wordArray = CryptoJS.lib.WordArray.create(data) // 转换为 CryptoJS 格式const sha256 = CryptoJS.SHA256(wordArray).toString() // 计算 SHA256const ext = file.name.substring(file.name.lastIndexOf('.')) // 获取文件后缀return `${sha256}${ext}` // 返回唯一文件名
}
  • 作用:基于文件内容的 SHA256 哈希生成唯一文件名,避免文件名冲突。
  • 优点:即使文件名相同,内容不同也会生成不同文件名,确保文件不被覆盖。

记录文件信息:createFile

function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {const fileVo = {configId: vo.configId, // 存储配置 IDurl: vo.url, // 文件访问 URLpath: name, // 文件路径name: file.name, // 原始文件名type: file.type, // 文件类型size: file.size // 文件大小}FileApi.createFile(fileVo) // 调用后端保存文件信息return fileVo
}
  • 作用:将文件信息(如 URL、路径、大小)发送到后端,保存到数据库,便于后续管理。

2.2 后端代码

后端基于 Spring Boot,提供预签名 URL 和文件信息保存接口,支持七牛云等 S3 兼容存储。

获取预签名 URL:FileController.getFilePresignedUrl
@GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {return success(fileService.getFilePresignedUrl(path));
}
  • 作用:接收前端请求的文件路径,返回预签名 URL 和相关信息。
  • 输入:path(如 avatars/abc123.jpg)。
  • 输出:FilePresignedUrlRespVO(包含 uploadUrl、url、configId)。

文件服务:FileServiceImpl.getFilePresignedUrl

  • 逻辑
    1. 获取默认 FileClient(如七牛云的 S3 客户端)。
    2. 调用 fileClient.getPresignedObjectUrl 生成预签名 URL。
    3. 返回包含 uploadUrl(上传地址)、url(访问地址)和 configId 的对象。
S3 配置类:S3FileClientConfig
@Data
public class S3FileClientConfig implements FileClientConfig {public static final String ENDPOINT_QINIU = "qiniucs.com";@NotNull(message = "endpoint 不能为空") private String endpoint; // 节点地址@URL(message = "domain 必须是 URL 格式") private String domain; // 自定义域名@NotNull(message = "bucket 不能为空") private String bucket; // 存储桶@NotNull(message = "accessKey 不能为空") private String accessKey; // 访问密钥@NotNull(message = "accessSecret 不能为空") private String accessSecret; // 秘密密钥@AssertTrue(message = "domain 不能为空")@JsonIgnorepublic boolean isDomainValid() {if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {return false; // 七牛云必须配置 domain}return true;}
}
  • 作用:定义 S3 存储的配置参数,支持七牛云、阿里云等。
  • 字段
    • endpoint:存储服务地址(如 s3-cn-south-1.qiniucs.com)。
    • domain:访问域名(如 http://bucket.qiniucs.com)。
    • bucket:存储桶名称。
    • accessKey/accessSecret:认证密钥。
  • 校验:七牛云要求 domain 必填。

2.3 七牛云 S3 客户端(假设实现)

虽然您未提供 S3FileClient 的具体实现,但可以参考之前的 S3FileClient(基于 MinIO SDK)。七牛云的实现类似,使用 AWS SDK 或七牛云 SDK 生成预签名 URL。

public class S3FileClient extends AbstractFileClient<S3FileClientConfig> implements FileClient {private final AmazonS3 s3Client;public S3FileClient(Long configId, S3FileClientConfig config) {super(configId, config);s3Client = AmazonS3ClientBuilder.standard().withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(), "auto")).withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()))).build();}@Overridepublic FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {// 生成预签名 URL(有效期 1 小时)GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(config.getBucket(), path).withMethod(HttpMethod.PUT).withExpiration(Date.from(Instant.now().plusSeconds(3600)));URL uploadUrl = s3Client.generatePresignedUrl(request);// 返回预签名 URL 和访问 URLreturn new FilePresignedUrlRespDTO().setUploadUrl(uploadUrl.toString()).setUrl(config.getDomain() + "/" + path);}
}

逻辑:使用 AWS SDK 生成预签名 URL,设置 HTTP 方法为 PUT,有效期 1 小时。

总结:

前端代码作用

前端基于 Vue3 和 Element Plus,使用 ElUpload 组件实现文件上传,核心代码在 useUpload 方法中。支持两种模式:前端直传(client)和后端上传(server)。以下是前端直传的逻辑和作用:

  • 配置文件(.env.local):
    • 设置 VITE_UPLOAD_TYPE=client,启用前端直传。
    • 配置后端 API 地址(VITE_BASE_URL 和 VITE_API_URL)。
  • 生成文件名(generateFileName):
    • 使用 SHA256 算法基于文件内容生成唯一文件名,防止冲突。
    • 作用:确保文件名全局唯一,避免覆盖。
  • 获取预签名 URL(FileApi.getFilePresignedUrl):
    • 调用后端 /presigned-url 接口,获取上传用的预签名 URL 和访问 URL。
    • 作用:获得 S3 存储的临时上传权限。
  • 上传文件(axios.put):
    • 使用预签名 URL 直接将文件上传到 S3,设置 Content-Type 为文件类型。
    • 作用:将文件存储到 S3,无需后端中转。
  • 记录文件信息(createFile):
    • 将文件信息(路径、URL、大小等)发送到后端,保存到数据库。
    • 作用:确保后端能跟踪和管理文件。

后端代码作用

后端基于 Spring Boot,提供预签名 URL 和文件信息保存接口,支持 S3 兼容存储(如七牛云)。核心代码在 FileController 和 FileServiceImpl 中。

  • 获取预签名 URL(FileController.getFilePresignedUrl):
    • 接收前端的路径参数,返回预签名 URL 和访问 URL。
    • 作用:为前端提供上传 S3 的临时权限。
  • 生成预签名 URL(FileServiceImpl.getFilePresignedUrl):
    • 使用默认 FileClient(如七牛云的 S3 客户端)生成预签名 URL。
    • 作用:调用 S3 客户端生成安全的上传地址。
  • S3 客户端(S3FileClient.getPresignedObjectUrl):
    • 使用 AWS SDK 或七牛云 SDK 生成预签名 URL,设置上传方法(PUT)和有效期(如 1 小时)。
    • 作用:与 S3 存储交互,生成临时访问令牌。
  • 保存文件信息(FileApi.createFile):
    • 接收前端发送的文件元数据,保存到数据库。
    • 作用:记录文件信息,便于后续查询和管理。
  • S3 配置(S3FileClientConfig):
    • 定义 S3 存储的配置参数(endpoint、bucket、accessKey 等)。
    • 作用:初始化 S3 客户端,确保连接正确。

整体流程

S3 直传通过前端直接上传文件到七牛云 S3 存储,极大提升上传效率。流程如下:

  1. 用户选择图片 avatar.jpg(100KB)。
  2. 前端生成唯一文件名(abc1234567890.jpg),请求后端预签名 URL。
  3. 后端使用七牛云 S3 客户端生成预签名 URL,返回上传地址和访问地址。
  4. 前端通过 axios.put 上传文件到七牛云。
  5. 上传成功后,前端通知后端保存文件信息到数据库。
  6. 用户获得文件 URL,可直接访问图片。
http://www.xdnf.cn/news/514657.html

相关文章:

  • Ubuntu 18.04设置静态IP的方法(图形化操作)
  • 美丽的独处时光
  • 菱形继承原理
  • java集合相关的api-总结
  • 2025年- H27-Lc135- 239.滑动窗口最大值(自定义双端队列)---java版
  • 量子计算在金融科技中的应用前景
  • [Codeforce刷题8]
  • 无废话离线大模型安装
  • 【随机过程】贝叶斯估计
  • 游戏引擎学习第292天:实现蛇
  • es聚合-词条统计
  • 量子计算 | 量子密码学的挑战和机遇
  • LWIP的NETCONN接口
  • APP手机端测试覆盖点
  • 专业漏洞扫描机构如何助力企业保障安全并提升竞争力?
  • 【MySQL】库与表的操作
  • 力扣热题——数组的最小相等和
  • 关于 Web 漏洞原理与利用:1. SQL 注入(SQLi)
  • 基于FPGA的电子万年历系统开发,包含各模块testbench
  • ​Docker 网络
  • 前端三剑客之HTML
  • 深入解析Python中的Vector2d类:从基础实现到特殊方法的应用
  • nginx服务器实验
  • 23种设计模式解释+记忆
  • 虚幻引擎5-Unreal Engine笔记之`GameMode`、`关卡(Level)` 和 `关卡蓝图(Level Blueprint)`的关系
  • 快速上手SElinux
  • 第8章 常用实用类
  • 基于shardingsphere的分库分表方案
  • redis读写一致问题
  • Visual Studio已更新为17.14+集成deepseek实现高效编程