跳到主要内容

文件模块 (File Module)

📖 概述

文件模块是博客系统中负责文件存储与管理的核心模块,实现了基于对象存储的现代化文件管理方案。

核心特性

  • 客户端直传 - 使用预签名URL,文件直接上传到云端,减轻服务器压力
  • 秒传功能 - 基于MD5哈希检测,相同文件无需重复上传
  • 多种存储 - 策略模式设计,支持Bitiful S4、阿里云OSS、MinIO等
  • 动态URL - 访问URL按需生成,支持自定义过期时间
  • 精度保护 - JavaScript Number精度保护,确保ID准确传输

技术选型

组件技术说明
对象存储Bitiful S4S3兼容协议,国内访问快
SDKAWS SDK V2官方S3客户端,功能完整
前端MD5SparkMD5支持大文件分块计算
数据库MySQL 9.4文件元数据存储

适用场景

  • 📝 博客文章图片上传
  • 📎 附件管理(PDF、DOCX等)
  • 👤 用户头像上传
  • 🖼️ 图片CDN分发

🎯 学习路线图

建议按以下顺序学习本模块:

graph LR
A[架构设计] --> B[数据库设计]
B --> C[配置管理]
C --> D[存储策略]
D --> E[上传流程]
E --> F[秒传功能]
F --> G[前端集成]

style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#f0f0f0
style D fill:#e8f5e9
style E fill:#ffe8f0
style F fill:#ffe8e8
style G fill:#e8f5ff

推荐学习顺序

  1. 架构设计 - 了解整体架构和设计模式
  2. 上传流程 - 掌握完整上传流程
  3. API参考 - 查阅API接口文档
  4. 代码深度分析 - 1000+行源码解读

🚀 快速开始

5分钟快速上传

第一步:配置Bitiful

# application.yaml
oss:
type: BITIFUL
bitiful:
endpoint: https://s3.bitiful.net
access-key: ${BITIFUL_ACCESS_KEY}
secret-key: ${BITIFUL_SECRET_KEY}
bucket: your-bucket-name

第二步:调用上传API

// 前端代码
async function uploadFile(file) {
// 1. 计算MD5
const md5 = await calculateMD5(file);

// 2. 获取预签名URL
const response = await fetch('/api/v1/files/presigned', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileSize: file.size,
fileType: file.type,
md5: md5
})
});

const { uploadUrl, fileId, instant } = await response.json();

// 3. 秒传检测
if (instant) {
console.log('⚡ 秒传成功!无需上传');
return fileId;
}

// 4. 直接上传到云端
await fetch(uploadUrl, {
method: 'PUT',
body: file
});

// 5. 确认上传
await fetch(`/api/v1/files/${fileId}/confirm`, {
method: 'PATCH'
});

return fileId;
}

第三步:获取访问URL

// 获取文件访问链接(60分钟有效)
const response = await fetch(`/api/v1/files/${fileId}/access-url`);
const { data: accessUrl } = await response.json();

// 显示或下载文件
window.open(accessUrl);

💡 核心概念

1. 预签名URL (Presigned URL)

什么是预签名URL?

预签名URL是一个包含身份验证信息的临时URL,允许客户端直接与云存储服务交互,无需通过后端中转。

优势

  • 减轻服务器带宽压力
  • 提升上传速度(客户端直连云端)
  • 降低服务器成本

工作原理

sequenceDiagram
participant 客户端
participant 后端
participant 云存储

客户端->>后端: 1. 请求上传权限
后端->>后端: 2. 生成预签名URL(30分钟有效)
后端-->>客户端: 3. 返回预签名URL
客户端->>云存储: 4. 直接上传文件 (PUT)
云存储-->>客户端: 5. 上传成功
客户端->>后端: 6. 通知后端保存记录

2. 秒传 (Instant Upload)

什么是秒传?

通过文件MD5哈希值检测,如果服务器已存在相同内容的文件,直接复用,跳过实际上传。

优势

  • 节省带宽和时间
  • 减少存储空间占用
  • 提升用户体验

实现原理

flowchart TB
A[用户选择文件] --> B[计算MD5哈希]
B --> C{后端检测MD5是否存在?}
C -->|存在| D[⚡ 秒传成功]
C -->|不存在| E[生成预签名URL]
E --> F[上传文件到云端]
F --> G[保存文件记录]
D --> H[返回已有文件ID]
G --> H

3. 存储策略模式

什么是策略模式?

策略模式将不同的存储实现(Bitiful、阿里云OSS、MinIO)封装成独立的策略类,业务代码只依赖抽象接口,实现松耦合。

优势

  • 方便切换存储服务
  • 支持运行时动态选择
  • 易于扩展新的存储类型

架构图

classDiagram
class FileStorageStrategy {
<<interface>>
+upload(file, fileKey) String
+generatePresignedUrl(fileKey, minutes) String
+generateDownloadUrl(fileKey, minutes) String
+delete(fileKey) void
}

class BitifulStorage {
-S3Client s3Client
-S3Presigner s3Presigner
+upload() String
+generatePresignedUrl() String
+generateDownloadUrl() String
}

class AliyunOSSStorage {
-OSSClient ossClient
+upload() String
+generatePresignedUrl() String
}

FileStorageStrategy <|.. BitifulStorage
FileStorageStrategy <|.. AliyunOSSStorage

📊 数据流向

完整上传流程数据流

graph TB
subgraph 前端
A[用户选择文件] --> B[SparkMD5计算]
B --> C[POST /presigned]
end

subgraph 后端
C --> D{MD5检测}
D -->|命中| E[返回instant=true]
D -->|未命中| F[创建file_file记录]
F --> G[调用S3Presigner]
G --> H[返回预签名URL]
end

subgraph 云存储
H --> I[前端PUT上传]
I --> J[文件存储成功]
end

subgraph 数据库
E --> K[(复用已有记录)]
J --> L[PATCH /confirm]
L --> M[(更新状态:COMPLETED)]
end

style A fill:#e1f5ff
style D fill:#ffe8e8
style E fill:#90EE90
style I fill:#fff4e1

🎓 进阶学习

深入理解

实战案例

可参考 test-tools/file-upload-test.html 中的完整实现示例。


❓ 常见问题

Q1: 为什么使用预签名URL而不是直接上传到后端?

A: 预签名URL有以下优势:

  1. 减轻服务器压力 - 文件直接传到云端,不占用服务器带宽
  2. 提升上传速度 - 客户端直连云存储,延迟更低
  3. 降低成本 - 服务器流量费用大幅降低
  4. 更好的扩展性 - 服务器无需处理大文件IO

Q2: MD5计算是否会影响用户体验?

A: 我们采用了以下优化:

  • 使用SparkMD5的分块计算(2MB/chunk),避免内存溢出
  • 异步计算,不阻塞UI
  • 对于小文件(<10MB),计算时间可忽略
  • 计算完成后可直接秒传,反而节省时间

Q3: 如何保证文件ID的精度不丢失?

A: 我们配置了Jackson全局序列化:

// JacksonConfig.java
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);

将Long类型序列化为String,避免JavaScript Number(53位)精度丢失。

Q4: 支持哪些文件类型?

A: 当前支持:

  • 图片:JPG, PNG, GIF, WebP
  • 文档:PDF, DOCX

可通过配置扩展:

oss:
bitiful:
allowed-extensions:
- jpg
- png
- pdf
- mp4 # 新增视频支持

🛠️ 相关资源

配置文档

  • 参考 application.yaml 中的 oss.bitiful 配置段

API文档

开发工具

  • 测试工具:test-tools/ 目录
  • API验证脚本:test-tools/api-verification-tests.sh

📝 下一步

需要帮助? 查看 代码分析文档 或项目README。