cloud-disk

cloud-disk 开发指南

S3 兼容对象存储服务 —— 基于 Spring Cloud Alibaba 微服务架构


目录

  1. 开发环境准备
  2. 项目结构
  3. 本地开发流程
  4. 添加新功能的步骤
  5. 调试技巧
  6. 代码规范
  7. 测试指南

1. 开发环境准备

1.1 必需的软件和版本

软件 版本要求 说明
JDK 21+ 项目使用 Java 21,包含虚拟线程等新特性
Maven 3.8+ 项目构建工具,推荐 3.9.x
Docker Desktop 最新稳定版 运行基础设施(MySQL、Redis、MinIO 等)
Git 最新稳定版 版本控制

1.2 IDE 推荐

推荐使用 IntelliJ IDEA Ultimate(2024.1 或更新版本),社区版亦可但缺少 Spring 相关辅助功能。

必备插件:

建议安装:

1.3 克隆仓库和导入项目

# 克隆仓库
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。


2. 项目结构

2.1 目录树

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(统一版本管理)

2.2 各模块职责

模块 角色 技术栈 端口 职责
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/分享/回收站管理、中英文国际化

2.3 包结构说明

common 模块

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      # 对象不存在

gateway 模块

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

object-storage 模块

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     # 启动类

object-metadata 模块

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         # 启动类

3. 本地开发流程

3.1 启动基础设施

在项目根目录下,使用 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 包而失败。

3.2 初始化数据库

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

3.3 编译

# 编译所有模块(推荐使用 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 表示同时编译依赖的模块

3.4 启动单个服务

在开发阶段,推荐以 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 主类:

启动顺序:MySQL 主从复制、Nacos、Redis Cluster 必须先于业务服务启动。object-storage 和 object-metadata 无先后依赖,可以并行启动。mysql-replication-initrestart: "no" 的一次性容器,只在首次 docker compose up -d 时自动运行。

3.5 打包

# 全量打包(推荐使用 make,自动加载 .env)
make package-fast

# 打包单个模块及其依赖
mvn clean package -pl object-storage -am -DskipTests

# 全量打包并运行测试
make package

打包产物在各模块的 target/ 目录下,例如 gateway/target/gateway-1.0.0-SNAPSHOT.jar

3.6 以 Docker Compose 启动全部服务

完成打包后,可以通过以下命令启动全部服务(含业务服务):

# 启动所有服务
docker compose up -d

# 仅启动业务服务(基础设施已在运行中时)
docker compose up -d gateway object-storage object-metadata

3.7 启动 Admin UI(管理后台前端)

Admin UI 在开发模式下通过 Vite dev server 运行,直接请求 Gateway (38080) 的 API。

环境配置

# 安装依赖
cd admin-ui
pnpm install

# 启动开发服务器(默认 http://localhost:5173)
pnpm dev

# 生产构建
pnpm build

Admin UI 启动后,浏览器直接向 http://localhost:38080 发起 API 请求(跨域)。Gateway 的 CorsConfig 已配置允许所有来源,无需额外处理。


4. 添加新功能的步骤

以下以”添加一个列出所有活跃用户的接口”为例,说明完整开发流程。

4.1 在 common 模块添加 DTO 或异常枚举

如果新功能需要新的响应数据结构或错误码,先在 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

4.2 在对应模块添加 Controller / Service

根据功能所属领域选择模块:

// 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));
    }
}

4.3 在 gateway 添加路由

如果新增的接口需要对外暴露,必须在 gatewayapplication.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 限流规则(如果需要)。

4.4 编译验证

# 编译受影响的模块及其依赖
mvn compile -pl object-metadata,gateway -am

# 确认无编译错误

4.5 单模块验证

# 启动所依赖的基础设施(如果尚未启动)
docker compose up -d

# 或以 spring-boot:run 启动修改后的服务
mvn spring-boot:run -pl object-metadata

5. 调试技巧

5.1 Nacos 控制台 — 查看服务注册

常见问题:启动业务服务后 Nacos 控制台看不到服务 → 检查 Nacos 是否已启动并可用,检查服务日志中报错的 Nacos 连接地址。

5.2 Sentinel Dashboard — 查看限流监控

Sentinel 控制台采用懒加载机制,仅当有请求经过时,服务才会在控制台中显示。首次启动后需要先发起一次 API 调用。

5.3 MinIO Console — 查看文件

MinIO API 端口为 39000,SDK 连接时使用此端口。

5.4 RabbitMQ 管理界面

5.5 数据库直连调试

使用任意 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;

5.6 日志级别调整

各服务的 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 消息收发

5.7 常用 curl 调试命令

# 验证服务是否启动成功 — 登录接口
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

6. 代码规范

6.1 Lombok 使用规范

项目广泛使用 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")
}

禁止

6.2 Result 统一响应体

所有 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"
}

6.3 异常体系

项目采用三层异常处理架构:

  1. BaseException — 业务异常基类,持有 S3ErrorCode
  2. 子异常类 — 如 BucketNotFoundExceptionObjectNotFoundException,继承 BaseException
  3. GlobalExceptionHandler@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 中只捕获并处理框架级异常(如参数校验失败)。

6.4 常量定义

所有常量集中在 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;
}

原则

6.5 注释规范

// 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: 极端并发下可能存在竞态条件

原则

6.6 Nacos 配置规范

各业务服务的 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.ymlenvironment 字段传入容器内地址:

environment:
  NACOS_SERVER: nacos:8848
  MINIO_ENDPOINT: http://minio:39000
  STORAGE_LOCAL_PATH: /data/local-storage

本地开发时默认使用 localhost 加偏移端口,无需额外配置。


7. 测试指南

7.1 前置条件

在进行 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

7.2 获取 Token

# 请求登录接口获取 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"

7.3 Bucket 管理 API

# 设置 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"

7.4 对象操作 API

# 上传对象
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"

7.5 分片上传 API

# 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"

7.6 列举对象 API

# 列举 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"

7.7 单元测试

项目已有 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 上下文。

后续可补充:

# 运行全部测试(clean 确保不遗漏变更)
mvn clean test

# 运行单个模块测试
mvn clean test -pl object-storage

附录

A. 服务端口总览

服务 内部端口 主机映射端口
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 节点集群)。

B. Docker Compose 常用命令

# 启动全部服务
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

C. Maven 常用命令速查

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