S3 兼容对象存储服务 —— 基于 Spring Cloud Alibaba 微服务架构
| 软件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 21+ | 项目使用 Java 21,包含虚拟线程等新特性 |
| Maven | 3.8+ | 项目构建工具,推荐 3.9.x |
| Docker Desktop | 最新稳定版 | 运行基础设施(MySQL、Redis、MinIO 等) |
| Git | 最新稳定版 | 版本控制 |
推荐使用 IntelliJ IDEA Ultimate(2024.1 或更新版本),社区版亦可但缺少 Spring 相关辅助功能。
必备插件:
@Data、@Slf4j、@RequiredArgsConstructor,无此插件无法编译建议安装:
# 克隆仓库
git clone <仓库地址> cloud-disk
cd cloud-disk
# IDEA 导入:File -> Open -> 选择 cloud-disk 目录
# IDEA 会自动识别 Maven 多模块项目,等待依赖下载完成
首次打开项目后,在 IDEA 右侧 Maven 面板中点击 Reload All Projects(刷新按钮),等待依赖下载完毕。
注意:如果 IDEA 提示 JDK 未配置,请在
File -> Project Structure -> Project中设置 SDK 为 JDK 21。
cloud-disk/
├── .mvn/ # Maven Wrapper 配置
├── common/ # 公共模块(DTO、异常、常量、存储后端接口)
│ └── src/main/java/com/clouddisk/common/
│ ├── config/ # Redisson Cluster 配置
│ ├── constant/ # 业务常量和 Redis Key 常量
│ ├── dto/ # 统一响应体 Result<T>
│ ├── enums/ # S3 错误码枚举
│ ├── exception/ # 异常体系(BaseException 及其子类)
│ ├── storage/ # 存储后端接口与模型(StorageBackend, StorageType 等)
│ └── util/ # 工具类(HotKeyUtil 热点打散)
├── gateway/ # API 网关
│ └── src/main/java/com/clouddisk/gateway/
│ ├── config/ # CORS 配置、Sentinel+Nacos 流控规则、Netty 参数调优
│ ├── filter/ # JWT 鉴权全局过滤器
│ └── handler/ # Gateway 级全局异常处理
├── admin-ui/ # 管理后台前端 (React 19 + Vite + shadcn/ui)
│ └── src/
│ ├── api/ # API 客户端、各模块接口、base URL 配置
│ ├── components/ # 布局、UI 组件、分享对话框
│ ├── pages/ # 登录、文件浏览、Bucket/分享/回收站管理、404
│ ├── stores/ # Zustand 状态管理(认证、国际化)
│ └── i18n/ # 中英文国际化资源
├── object-storage/ # 对象存储数据面服务
│ └── src/main/java/com/clouddisk/storage/
│ ├── backend/ # 存储后端实现(MinIO/Local/OSS)及工厂
│ ├── config/ # MinIO 客户端、RabbitMQ、ShardingSphere、XXL-Job 配置
│ ├── controller/ # 对象操作和分片上传 REST 接口
│ ├── dto/ # 请求/响应 DTO
│ ├── entity/ # Outbox 消息实体
│ ├── mapper/ # Outbox MyBatis-Plus Mapper
│ ├── producer/ # RabbitMQ 事件生产者 + Outbox 重试服务
│ └── service/ # 业务逻辑(ObjectService、MultipartUploadService)
├── object-metadata/ # 对象元数据控制面服务
│ └── src/main/java/com/clouddisk/metadata/
│ ├── config/ # MyBatis-Plus 自动填充、RabbitMQ、RestTemplate、ShardingSphere、XXL-Job 配置
│ ├── consumer/ # RabbitMQ 事件消费者
│ ├── controller/ # 认证、Bucket、回收站、分享链接接口
│ ├── dto/ # 请求 DTO
│ ├── entity/ # BucketInfo、ObjectInfo、ShareLink、UserInfo 实体
│ ├── mapper/ # MyBatis-Plus Mapper
│ └── service/ # 业务逻辑(Auth、Bucket、Recycle、Share)
├── sql/ # 数据库初始化脚本
├── docs/ # 项目文档
├── docker-compose.yml # 全服务编排(基础设施 + 业务服务)
├── Dockerfile # 业务服务容器化构建模板
└── pom.xml # 父 POM(统一版本管理)
| 模块 | 角色 | 技术栈 | 端口 | 职责 |
|---|---|---|---|---|
common |
公共库 | — | — | 共享 DTO、异常枚举、常量定义,不独立运行 |
gateway |
API 网关 | Spring Cloud Gateway + JWT + Sentinel | 38080 | 统一入口、路由转发、JWT 鉴权、Sentinel 流控熔断 |
object-storage |
数据面 | StorageBackend (MinIO/本地/OSS) + Redis + RabbitMQ + MySQL | 38081 | 文件上传/下载/删除/复制、秒传判定、分片上传、多存储后端路由、Outbox 事件发送 |
object-metadata |
控制面 | MySQL + MyBatis-Plus + Redis + RabbitMQ | 38082 | Bucket 管理(增删查)、对象元数据索引、回收站、分享链接、RabbitMQ 事件消费 |
admin-ui |
管理后台 | React 19 + Vite + @tanstack/react-router + zustand + shadcn/ui | 5173 (dev) | 用户登录、文件浏览与上传/下载/删除、分片上传、Bucket/分享/回收站管理、中英文国际化 |
com.clouddisk.common
├── config
│ └── RedissonClusterConfig.java # Redis Cluster 响应式客户端配置
├── constant
│ ├── BusinessConstants.java # 业务常量(默认 region、回收站保留天数、分享有效期等)
│ └── RedisKeyConstants.java # Redis Key 前缀和过期时间(秒传 hash、分片状态、存储类型等)
├── dto
│ └── Result.java # 统一响应体,含 code/message/errorCode/data
├── enums
│ └── S3ErrorCode.java # S3 标准错误码枚举(NoSuchBucket、InvalidPart 等)
├── storage
│ ├── StorageBackend.java # 统一存储后端接口(CRUD + Multipart)
│ ├── StorageType.java # 存储类型枚举(MINIO / LOCAL / OSS)
│ ├── ObjectStat.java # 对象元数据模型
│ └── PartInfo.java # 分片信息模型
├── util
│ └── HotKeyUtil.java # Redis Cluster 热点打散工具(3-shard hash-tag)
└── exception
├── BaseException.java # 业务异常基类,持有 S3ErrorCode
├── BucketAlreadyExistsException.java # Bucket 已存在
├── BucketNotEmptyException.java # Bucket 非空
├── BucketNotFoundException.java # Bucket 不存在
├── EntityTooLargeException.java # 实体过大
├── GlobalExceptionHandler.java # @RestControllerAdvice 全局处理
├── InvalidPartException.java # 非法分片
├── InvalidPartOrderException.java # 分片顺序错误
└── ObjectNotFoundException.java # 对象不存在
com.clouddisk.gateway
├── config
│ ├── CorsConfig.java # 跨域配置,暴露 S3 标准响应头
│ ├── GatewayConfig.java # Sentinel 网关流控规则(Nacos 动态数据源 + 硬编码兜底)
│ └── NettyServerConfig.java # Netty worker/select 线程数、TCP backlog 参数调优
├── filter
│ └── AuthFilter.java # JWT 全局过滤器,校验 Bearer Token,透传 X-User-Id
├── handler
│ └── GatewayExceptionHandler.java # Gateway 层异常处理(路由不可达、超时等)
└── GatewayApplication.java # 启动类,@EnableDiscoveryClient
com.clouddisk.storage
├── config
│ ├── MinioConfig.java # MinIO 双客户端配置(内部 + 预签名公开端点)
│ ├── RabbitMqConfig.java # RabbitMQ Topic Exchange + Queue + Binding 声明
│ ├── ShardingSphereConfig.java # ShardingSphere 分库分表 + 读写分离配置
│ └── XxlJobConfig.java # XXL-Job 执行器配置
├── controller
│ ├── ObjectController.java # Put/Get/Head/Delete/Copy/DeleteObjects API
│ └── MultipartController.java # 分片上传 Init/UploadPart/Complete/Abort/ListParts API
├── dto
│ ├── CompleteMultipartUploadRequest.java # 完成分片请求体
│ ├── DeleteObjectsRequest.java # 批量删除请求体
│ ├── ObjectData.java # 对象数据封装(Stream + metadata)
│ ├── PresignedUrlResponse.java # 预签名 URL 响应(url, method, expiresIn)
│ └── UploadInitResponse.java # 上传响应(含秒传标记)
├── backend
│ ├── MinioStorageBackend.java # MinIO 存储后端实现
│ ├── LocalStorageBackend.java # 本地文件系统存储后端实现
│ ├── OssStorageBackend.java # 阿里云 OSS 存储后端实现
│ └── StorageBackendFactory.java # 存储后端工厂(DB 查 storage_type + Redis 缓存路由)
├── entity
│ └── OutboxMessage.java # Outbox 表实体,@Accessors(chain=true)
├── mapper
│ └── OutboxMessageMapper.java # Outbox MyBatis-Plus Mapper
├── producer
│ ├── ObjectEventProducer.java # 事件生产者:先写 Outbox 表(PENDING),再发 MQ,成功改 SUCCESS
│ └── OutboxRetryService.java # XXL-Job 定时扫描 PENDING/死信 重试和清理
├── service
│ ├── ObjectService.java # 对象操作接口
│ ├── MultipartUploadService.java # 分片上传接口
│ ├── impl/ObjectServiceImpl.java # 对象操作实现(委托 StorageBackend)
│ └── impl/MultipartUploadServiceImpl.java # 分片上传实现(Redis 维护状态 + 委托后端合并)
└── StorageApplication.java # 启动类
com.clouddisk.metadata
├── config
│ ├── MyMetaObjectHandler.java # MyBatis-Plus 自动填充 createdAt/updatedAt
│ ├── RabbitMqConfig.java # RabbitMQ 消费者配置
│ ├── RestTemplateConfig.java # RestTemplate 配置(用于跨服务 HTTP 调用)
│ ├── ShardingSphereConfig.java # ShardingSphere 分库分表 + 读写分离
│ └── XxlJobConfig.java # XXL-Job 执行器配置
├── consumer
│ └── ObjectCreatedConsumer.java # RabbitMQ 消费者:处理对象创建事件,写入 ObjectInfo 索引
├── controller
│ ├── AuthController.java # 登录/注册/登出/获取当前用户
│ ├── BucketController.java # Create/List/Delete/Head Bucket + ListObjectsV2
│ ├── RecycleController.java # 回收站:列举/恢复/永久删除
│ └── ShareController.java # 分享链接:创建/验证/查看/撤销
├── dto
│ ├── LoginRequest.java # 登录请求体
│ ├── LoginResponse.java # 登录响应(含 JWT Token)
│ ├── ShareCreateRequest.java # 创建分享请求
│ └── ShareVerifyRequest.java # 验证分享密码请求
├── entity
│ ├── BucketInfo.java # Bucket 信息(状态枚举 ACTIVE/DELETED)
│ ├── ObjectInfo.java # 对象索引(状态枚举 NORMAL/RECYCLED/DELETED)
│ ├── ShareLink.java # 分享链接(状态枚举 ACTIVE/EXPIRED/REVOKED)
│ └── UserInfo.java # 用户信息
├── mapper
│ ├── BucketInfoMapper.java # Bucket MyBatis-Plus Mapper
│ ├── ObjectInfoMapper.java # Object MyBatis-Plus Mapper
│ ├── ShareLinkMapper.java # Share Link MyBatis-Plus Mapper
│ └── UserInfoMapper.java # User Info MyBatis-Plus Mapper
├── service
│ ├── AuthService.java # 认证接口
│ ├── BucketService.java # Bucket CRUD 接口
│ ├── RecycleService.java # 回收站接口
│ ├── ShareService.java # 分享链接接口
│ └── impl/ # 各接口的实现
└── MetadataApplication.java # 启动类
在项目根目录下,使用 Docker Compose 启动所有基础设施服务:
# 首次使用:自动检测本机 IP 并写入 .env(MinIO 预签名 URL 需要外部可达地址)
make config
# 初次启动全部服务(含基础设施和业务服务)
docker compose up -d
# 或仅启动基础设施(业务服务由 IDE 启动时使用)
docker compose up -d mysql mysql-slave mysql-replication-init redis-7000 redis-7001 redis-7002 \
redis-7003 redis-7004 redis-7005 redis-cluster-init rabbitmq minio nacos sentinel
# 查看各服务是否正常启动
docker compose ps
各基础设施的默认访问地址:
| 服务 | 内部端口 | 映射端口 | 控制台地址 |
|---|---|---|---|
| MySQL (master) | 3306 | 33306 | 直连 localhost:33306 |
| MySQL (slave) | 3306 | 33307 | 直连 localhost:33307 (只读) |
| Redis Cluster | 6379 | 37000-37005 | 6 节点集群 |
| RabbitMQ | 5672 / 15672 | 35672 / 45672 | http://localhost:45672 (admin / admin123) |
| MinIO API | 9000 | 39000 | — |
| MinIO Console | 9001 | 39001 | http://localhost:39001 (minioadmin / minioadmin) |
| Nacos | 8848 | 38848 | http://localhost:38848/nacos |
| Sentinel | 8858 | 38858 | http://localhost:38858 (sentinel / sentinel) |
| XXL-Job | 9080 | 39080 | http://localhost:39080/xxl-job-admin (admin / 123456) |
第一次启动前:确保执行过一次
mvn clean package -DskipTests,否则object-storage等业务服务的 Docker 构建可能因缺少 jar 包而失败。
Docker Compose 启动 MySQL 时已自动挂载 ./sql/init.sql 到容器初始化目录,首次启动会自动建库建表并插入测试用户。如果数据库模式后续发生变更,需要手动重建:
# 进入 MySQL 容器执行初始化脚本
docker exec -i cloud-disk-mysql mysql -uroot -proot123 cloud_disk < sql/init.sql
# 或删除旧容器和卷后重新启动
docker compose down -v mysql
docker compose up -d mysql
# 编译所有模块(推荐使用 make,自动加载 .env 中的 JAVA_HOME)
make package-fast
# 编译单个模块及其依赖
mvn compile -pl gateway -am
mvn compile -pl object-storage -am
mvn compile -pl object-metadata -am
# -pl 指定模块名(artifactId),-am 表示同时编译依赖的模块
在开发阶段,推荐以 Maven 方式在 IDE 中或命令行中启动单个服务,而非用 Docker Compose 启动全部业务服务,这样可以热更新代码(需要 spring-boot-devtools)。
# 启动 Gateway(必须先启动 Nacos)
mvn spring-boot:run -pl gateway
# 启动 object-storage(必须先启动 Nacos、MySQL、Redis、RabbitMQ、MinIO)
mvn spring-boot:run -pl object-storage
# 启动 object-metadata(必须先启动 Nacos、MySQL、Redis、RabbitMQ)
mvn spring-boot:run -pl object-metadata
也可以在 IDEA 中直接运行各模块的 *Application.java 主类:
GatewayApplication — 端口 38080StorageApplication — 端口 38081MetadataApplication — 端口 38082启动顺序:MySQL 主从复制、Nacos、Redis Cluster 必须先于业务服务启动。object-storage 和 object-metadata 无先后依赖,可以并行启动。
mysql-replication-init是restart: "no"的一次性容器,只在首次docker compose up -d时自动运行。
# 全量打包(推荐使用 make,自动加载 .env)
make package-fast
# 打包单个模块及其依赖
mvn clean package -pl object-storage -am -DskipTests
# 全量打包并运行测试
make package
打包产物在各模块的 target/ 目录下,例如 gateway/target/gateway-1.0.0-SNAPSHOT.jar。
完成打包后,可以通过以下命令启动全部服务(含业务服务):
# 启动所有服务
docker compose up -d
# 仅启动业务服务(基础设施已在运行中时)
docker compose up -d gateway object-storage object-metadata
Admin UI 在开发模式下通过 Vite dev server 运行,直接请求 Gateway (38080) 的 API。
环境配置:
.env.development:VITE_API_BASE_URL=http://localhost:38080.env.production:VITE_API_BASE_URL=(生产构建后与后端同源部署)# 安装依赖
cd admin-ui
pnpm install
# 启动开发服务器(默认 http://localhost:5173)
pnpm dev
# 生产构建
pnpm build
Admin UI 启动后,浏览器直接向
http://localhost:38080发起 API 请求(跨域)。Gateway 的 CorsConfig 已配置允许所有来源,无需额外处理。
以下以”添加一个列出所有活跃用户的接口”为例,说明完整开发流程。
如果新功能需要新的响应数据结构或错误码,先在 common 模块中定义。
// common/.../dto/UserInfoDTO.java
@Data
public class UserInfoDTO {
private Long id;
private String username;
private Long totalSpace;
private Long usedSpace;
}
// common/.../enums/S3ErrorCode.java — 新增枚举常量
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "UserNotFound", "The specified user does not exist.")
编译验证:
mvn compile -pl common
根据功能所属领域选择模块:
object-storageobject-metadatagateway// object-metadata/.../controller/UserController.java
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/users")
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Result<List<UserInfoDTO>>> listUsers() {
List<UserInfoDTO> users = userService.listAllUsers();
return ResponseEntity.ok(Result.success(users));
}
}
如果新增的接口需要对外暴露,必须在 gateway 的 application.yml 中添加路由规则。
# gateway/.../application.yml — spring.cloud.gateway.routes 下新增
- id: admin-users
uri: lb://object-metadata
predicates:
- Path=/admin/users/**
filters:
- StripPrefix=0
同时也需要在 GatewayConfig.java 中添加对应的 Sentinel 限流规则(如果需要)。
# 编译受影响的模块及其依赖
mvn compile -pl object-metadata,gateway -am
# 确认无编译错误
# 启动所依赖的基础设施(如果尚未启动)
docker compose up -d
# 或以 spring-boot:run 启动修改后的服务
mvn spring-boot:run -pl object-metadata
http://localhost:38848/nacos常见问题:启动业务服务后 Nacos 控制台看不到服务 → 检查 Nacos 是否已启动并可用,检查服务日志中报错的 Nacos 连接地址。
http://localhost:38858Sentinel 控制台采用懒加载机制,仅当有请求经过时,服务才会在控制台中显示。首次启动后需要先发起一次 API 调用。
http://localhost:39001MinIO API 端口为 39000,SDK 连接时使用此端口。
http://localhost:45672使用任意 MySQL 客户端(IDEA Database Tool、Navicat、DBeaver 等)直连:
常用调试 SQL:
-- 查看所有 Bucket
SELECT * FROM bucket_info;
-- 查看指定 Bucket 下的对象
SELECT * FROM object_info WHERE bucket_id = 1;
-- 查看 outbox 消息状态(排查消息发送失败)
SELECT * FROM outbox_table ORDER BY created_at DESC LIMIT 20;
-- 查看分片上传状态
SELECT * FROM multipart_upload;
各服务的 application.yml 中已预设日志级别:
logging:
level:
com.clouddisk: DEBUG # 项目代码输出 DEBUG 级别日志
org.springframework.cloud.gateway: INFO # Gateway 框架日志
io.minio: WARN # MinIO SDK 日志
在本地开发时,可以通过在 application.yml 中临时修改来获取更详细的信息:
logging:
level:
com.clouddisk.storage.service.impl: TRACE # 更详细的文件操作日志
com.baomidou.mybatisplus: DEBUG # 查看 SQL 语句
org.springframework.amqp.rabbit: DEBUG # 查看 MQ 消息收发
# 验证服务是否启动成功 — 登录接口
curl -s -w "\nHTTP:%{http_code}" -X POST http://localhost:38080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 查看 Nacos 中的服务列表
curl http://localhost:38848/nacos/v1/ns/service/list
项目广泛使用 Lombok 减少样板代码,遵循以下约定:
// @Data — 自动生成 Getter/Setter/toString/equals/hashCode
// 用于 DTO、Entity、请求/响应类
@Data
@TableName("bucket_info")
public class BucketInfo {
@TableId(type = IdType.AUTO)
private Long id;
private String bucketName;
}
// @RequiredArgsConstructor — 为 final 字段生成构造器,用于依赖注入
// 替代 @Autowired 字段注入,推荐构造器注入
@Slf4j
@Service
@RequiredArgsConstructor
public class ObjectServiceImpl implements ObjectService {
private final StorageBackendFactory storageBackendFactory;
private final RedissonClient redissonClient;
}
// @Slf4j — 自动生成 log 字段
@Slf4j
@RestController
public class ObjectController {
public void someMethod() {
log.info("操作成功: bucket={}, objectKey={}", bucket, objectKey);
log.error("操作失败", exception);
}
}
// @Accessors(chain = true) — 启用链式 setter,仅用于实体构建场景
@Data
@Accessors(chain = true)
@TableName("outbox_table")
public class OutboxMessage {
// 使用: new OutboxMessage().setMessageType("EVENT").setStatus("PENDING")
}
禁止:
@Accessors(chain = true) 之外的链式调用风格(MyBatis-Plus 更新场景可能有问题)@Data 类中包含 @OneToMany / @ManyToOne 等 JPA 关联注解(本项目使用 MyBatis-Plus,不是 JPA)@AllArgsConstructor(应显式定义需要的依赖)所有 REST 接口的响应体统一使用 Result<T>,定义在 common 模块:
// 成功响应(带数据)
return ResponseEntity.ok(Result.success(data));
// 成功响应(无数据)
return ResponseEntity.ok(Result.success());
// 创建成功
return ResponseEntity.status(HttpStatus.CREATED).body(Result.success(data));
// 错误响应(由 GlobalExceptionHandler 统一处理,Controller 中不要手动构造)
Result<T> 的 JSON 结构:
{
"code": 200,
"message": "OK",
"data": { ... }
}
// 错误时:
{
"code": 404,
"message": "The specified bucket does not exist.",
"errorCode": "NoSuchBucket"
}
项目采用三层异常处理架构:
BaseException — 业务异常基类,持有 S3ErrorCodeBucketNotFoundException、ObjectNotFoundException,继承 BaseExceptionGlobalExceptionHandler — @RestControllerAdvice 全局捕获,统一转换为 Result<T> 错误响应// 抛出异常(在 Service 层使用)
throw new BucketNotFoundException(bucketName);
// BaseException + 自定义消息
throw new BaseException(S3ErrorCode.INTERNAL_ERROR, "Upload failed: " + e.getMessage());
// 自定义异常类示例
public class BucketNotFoundException extends BaseException {
public BucketNotFoundException(String bucketName) {
super(S3ErrorCode.NO_SUCH_BUCKET, "Bucket not found: " + bucketName);
}
}
S3ErrorCode 枚举包含 S3 标准错误码,每个枚举关联 HTTP 状态码、S3 错误码字符串和描述信息。
原则:Controller 中不捕获业务异常,让 GlobalExceptionHandler 统一处理。Controller 中只捕获并处理框架级异常(如参数校验失败)。
所有常量集中在 common 模块的 constant 包下:
// BusinessConstants.java — 业务参数常量
public final class BusinessConstants {
private BusinessConstants() {} // 工具类禁止实例化
public static final String DEFAULT_REGION = "us-east-1";
public static final int RECYCLE_RETENTION_DAYS = 30;
public static final int DEFAULT_MAX_KEYS = 1000;
}
// RedisKeyConstants.java — Redis Key 前缀和 TTL
public final class RedisKeyConstants {
private RedisKeyConstants() {}
public static final String KEY_UPLOAD_HASH = "upload:hash:";
public static final String KEY_PARTS = "parts:";
public static final int UPLOAD_HASH_TTL_DAYS = 365;
}
原则:
final class + private 构造器防止继承和实例化// 1. 类注释 — 用 /** */ 说明职责
/**
* 对象操作 Controller
* 提供 S3 标准的 PutObject、GetObject、HeadObject、DeleteObject、CopyObject 接口
*/
// 2. 方法注释 — 当方法为 @Override 或签名自解释时可省略
/** PutObject — PUT /{bucket}/{*objectKey} */
@PutMapping("/{bucket}/{*objectKey}")
public ResponseEntity<Result<UploadInitResponse>> putObject(...) { ... }
/** 秒传 Redis 命中但 MinIO 文件不存在,清除缓存 */
bucket.delete();
log.warn("秒传 Redis 命中但 MinIO 文件不存在,清除缓存: key={}", redisKey);
// 3. 复杂逻辑 — 在实现内部写行注释
// 计算 usedBytes 增量: 新文件 size 与旧文件 size 之差,用于精确更新 Bucket 存储量
long sizeDelta = size != null ? size : 0L;
// 4. TODO / FIXME 标记
// TODO: 后续改为异步批量删除
// FIXME: 极端并发下可能存在竞态条件
原则:
/** */ 说明语义,实现方法用行内注释补充细节{}(SLF4J 风格),不要使用字符串拼接各业务服务的 application.yml 中,连接外部服务的地址优先通过环境变量注入,本地开发时提供默认值:
spring:
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER:localhost:38848}
minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:39000}
storage:
local:
base-path: ${STORAGE_LOCAL_PATH:./data}
oss:
endpoint: ${OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
Docker 环境中通过 docker-compose.yml 的 environment 字段传入容器内地址:
environment:
NACOS_SERVER: nacos:8848
MINIO_ENDPOINT: http://minio:39000
STORAGE_LOCAL_PATH: /data/local-storage
本地开发时默认使用 localhost 加偏移端口,无需额外配置。
在进行 API 测试前,确保以下服务已启动:
# 启动全部服务(推荐)
docker compose up -d
# 或仅启动基础设施(业务服务由 IDE 以 Debug 模式启动)
docker compose up -d mysql mysql-slave mysql-replication-init redis-7000 redis-7001 redis-7002 \
redis-7003 redis-7004 redis-7005 redis-cluster-init rabbitmq minio nacos sentinel
# 请求登录接口获取 JWT Token
# (示例假设登录接口为 POST /login,请求体中携带用户名和密码)
TOKEN=$(curl -s -X POST http://localhost:38080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}' | jq -r '.data.token')
echo "Token: $TOKEN"
# 设置 Token
TOKEN="<your-jwt-token>"
# 创建 Bucket
curl -v -X PUT http://localhost:38080/my-bucket \
-H "Authorization: Bearer $TOKEN"
# 列出所有 Bucket
curl http://localhost:38080/ \
-H "Authorization: Bearer $TOKEN" | jq
# 查询 Bucket 是否存在(HEAD)
curl -v -X HEAD http://localhost:38080/my-bucket \
-H "Authorization: Bearer $TOKEN"
# 删除 Bucket
curl -v -X DELETE http://localhost:38080/my-bucket \
-H "Authorization: Bearer $TOKEN"
# 上传对象
curl -v -X PUT http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/plain" \
-H "Content-Length: 12" \
-d "Hello World!"
# 秒传(携带 MD5 头)
MD5=$(echo -n "Hello World!" | md5sum | cut -d' ' -f1)
curl -v -X PUT http://localhost:38080/my-bucket/hello2.txt \
-H "Authorization: Bearer $TOKEN" \
-H "x-cloud-disk-md5: $MD5" \
-H "Content-Type: text/plain" \
-H "Content-Length: 12" \
-d "Hello World!"
# 下载对象(默认 302 跳转至预签名 URL)
curl -v -L -X GET http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer $TOKEN" \
-o downloaded.txt
# 强制代理模式(数据流经 Gateway,响应 200 + 流式 body)
curl -v -X GET "http://localhost:38080/my-bucket/hello.txt?proxy=true" \
-H "Authorization: Bearer $TOKEN" \
-o downloaded.txt
# HEAD 对象
curl -v -X HEAD http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer $TOKEN"
# 删除对象
curl -v -X DELETE http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer $TOKEN"
# 批量删除
curl -v -X POST "http://localhost:38080/my-bucket?delete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d '<Delete><Object><Key>hello.txt</Key></Object></Delete>'
# 复制对象
curl -v -X PUT http://localhost:38080/my-bucket/hello-copy.txt \
-H "Authorization: Bearer $TOKEN" \
-H "x-copy-source: /my-bucket/hello.txt"
# 1. 初始化分片上传
INIT_RESP=$(curl -s -X POST "http://localhost:38080/my-bucket/large.iso?uploads" \
-H "Authorization: Bearer $TOKEN")
UPLOAD_ID=$(echo $INIT_RESP | jq -r '.data.uploadId')
echo "UploadId: $UPLOAD_ID"
# 2. 上传分片(partNumber=1)
dd if=/dev/urandom bs=1M count=5 of=part1.bin 2>/dev/null
curl -v -X PUT "http://localhost:38080/my-bucket/large.iso?partNumber=1&uploadId=$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @part1.bin
# 3. 上传分片(partNumber=2)
dd if=/dev/urandom bs=1M count=5 of=part2.bin 2>/dev/null
curl -v -X PUT "http://localhost:38080/my-bucket/large.iso?partNumber=2&uploadId=$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @part2.bin
# 4. 列出已上传分片
curl -s "http://localhost:38080/my-bucket/large.iso?uploadId=$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN" | jq
# 5. 完成分片上传
# 注意:ETag 需从 UploadPart 的响应头中获取
curl -v -X POST "http://localhost:38080/my-bucket/large.iso?uploadId=$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d '<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>"<etag1>"</ETag></Part><Part><PartNumber>2</PartNumber><ETag>"<etag2>"</ETag></Part></CompleteMultipartUpload>'
# 6. 取消分片上传(可选)
curl -v -X DELETE "http://localhost:38080/my-bucket/large.iso?uploadId=$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN"
# 列举 Bucket 中的所有对象(ListObjectsV2)
curl "http://localhost:38080/my-bucket?list-type=2" \
-H "Authorization: Bearer $TOKEN"
# 按前缀过滤
curl "http://localhost:38080/my-bucket?list-type=2&prefix=images/" \
-H "Authorization: Bearer $TOKEN"
# 带分隔符(模拟目录层级)
curl "http://localhost:38080/my-bucket?list-type=2&delimiter=/" \
-H "Authorization: Bearer $TOKEN"
# 限制返回数量
curl "http://localhost:38080/my-bucket?list-type=2&max-keys=5" \
-H "Authorization: Bearer $TOKEN"
项目已有 6 个测试类,使用 JUnit 5 + Mockito + MockMvc 进行 Controller 层独立测试:
| 模块 | 测试类 | 用例数 |
|---|---|---|
| object-storage | ObjectControllerTest |
9 (put/get/delete) |
| object-storage | MultipartControllerTest |
11 (init/upload/complete/abort/list) |
| object-metadata | AuthControllerTest |
9 (login/me/logout) |
| object-metadata | BucketControllerTest |
8 (create/list/delete/listObjectsJson) |
| object-metadata | ShareControllerTest |
7 (create/list/revoke/getFiles) |
| object-metadata | RecycleControllerTest |
7 (list/restore/permanentDelete/empty) |
测试使用 MockMvc standalone setup,mock Service 层依赖,不启动完整 Spring 上下文。
后续可补充:
@WebMvcTest — Controller 层集成测试# 运行全部测试(clean 确保不遗漏变更)
mvn clean test
# 运行单个模块测试
mvn clean test -pl object-storage
| 服务 | 内部端口 | 主机映射端口 |
|---|---|---|
| Gateway | 38080 | 38080 |
| object-storage | 38081 | 38081 |
| object-metadata | 38082 | 38082 |
| MySQL (master) | 3306 | 33306 |
| MySQL (slave) | 3306 | 33307 |
| Redis Cluster | 6379 | 37000-37005 |
| RabbitMQ (AMQP) | 5672 | 35672 |
| RabbitMQ (管理) | 15672 | 45672 |
| MinIO (API) | 9000 | 39000 |
| MinIO (Console) | 9001 | 39001 |
| Nacos | 8848 | 38848 |
| Nacos (gRPC) | 9848 | 39848 |
| Sentinel Dashboard | 8858 | 38858 |
| XXL-Job Admin | 39080 | 39080 |
业务端口在原始端口上增加 30000 偏移,避免与本地其他服务冲突。Redis Cluster 使用 37000-37005 端口(6 节点集群)。
# 启动全部服务
docker compose up -d
# 查看运行状态
docker compose ps
# 查看实时日志
docker compose logs -f gateway
# 快速停启(推荐,保留网络和 volumes)
docker compose stop
docker compose start
# 完整停止(删除网络)
docker compose down
# ⚠️ docker compose down 后重新 up 前,务必清除 Redis data volumes
docker volume rm cloud-disk_redis_7000_data cloud-disk_redis_7001_data \
cloud-disk_redis_7002_data cloud-disk_redis_7003_data \
cloud-disk_redis_7004_data cloud-disk_redis_7005_data
# 停止并删除所有数据卷(会丢失全部数据)
docker compose down -v
# 重新构建镜像后启动指定服务
docker compose build gateway
docker compose up -d --no-deps gateway
mvn clean compile # 清理并编译
mvn clean test # 清理并运行测试
mvn clean package -DskipTests # 清理并打包(跳过测试)
mvn clean # 仅清理 target
mvn spring-boot:run # 运行 Spring Boot 应用
# 操作指定模块
mvn <phase> -pl <module> -am
# 示例: mvn compile -pl object-storage -am