EasyPan云盘系统设计与实现
基于Spring Boot + Vue.js构建的现代化云盘存储系统,集成MinIO对象存储、Redis缓存、MySQL数据库,实现文件上传下载、分享、预览等核心功能,支持QQ第三方登录、文件回收站、视频转码等高级特性。
系统架构设计
技术栈选型
后端技术栈:
- Spring Boot 2.6.1 - 微服务框架
- MyBatis Plus 3.4.1 - ORM框架
- MySQL 8.0.30 - 关系型数据库
- Redis - 缓存与会话管理
- MinIO - 对象存储服务
- FFmpeg - 视频转码处理
前端技术栈:
- Vue.js 3.x - 前端框架
- Element Plus - UI组件库
- Axios - HTTP客户端
- Vue Router - 路由管理
系统架构图
graph TB
A[用户浏览器] --> B[Nginx反向代理]
B --> C[Vue.js前端应用]
B --> D[Spring Boot后端服务]
D --> E[MySQL数据库]
D --> F[Redis缓存]
D --> G[MinIO对象存储]
D --> H[FFmpeg转码服务]
subgraph "核心模块"
I[用户管理模块]
J[文件管理模块]
K[分享管理模块]
L[回收站模块]
end
D --> I
D --> J
D --> K
D --> L
核心功能实现
1. 文件存储架构
系统采用MinIO对象存储作为主要存储方案,结合Redis缓存优化性能:
/**
* MinIO服务接口 - 文件存储核心服务
*/
public interface MinioService {
// 上传文件到MinIO
void uploadFile(String objectName, MultipartFile file);
// 分片上传支持大文件
void uploadChunk(String objectName, MultipartFile file, int chunkIndex);
// 合并分片文件
void mergeChunks(String objectName, int chunkCount, String userId, String fileId);
// 获取文件输入流
InputStream getFileInputStream(String objectName);
// 检查文件是否存在
boolean fileExists(String objectName);
}
存储优化策略:
- 分片上传:支持大文件断点续传
- 文件去重:基于MD5哈希值避免重复存储
- 缓存机制:Redis缓存热点文件元数据
- CDN加速:静态资源通过Nginx缓存
2. 文件分享机制
实现安全可控的文件分享功能,支持提取码和过期时间:
/**
* 文件分享控制器 - 核心分享逻辑
*/
@RestController
@RequestMapping("/file")
public class FileInfoController {
/**
* 创建文件分享链接
* 支持自定义提取码和过期时间
*/
@PostMapping("/shareFile")
@LoginValidator
public Map<String, String> shareFile(HttpSession session, @RequestBody ShareDTO shareDTO) {
SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
// 验证文件所有权
FileInfo fileInfo = fileInfoService.getOne(new LambdaQueryWrapper<FileInfo>()
.eq(FileInfo::getId, shareDTO.getFileId())
.eq(FileInfo::getUserId, user.getId()));
if (fileInfo == null) throw new BizException("文件不存在");
// 生成分享令牌和提取码
String token = StringTools.getRandomString(32);
String code = shareDTO.getExtractCode();
long expire = shareDTO.getExpireHours() != null ? shareDTO.getExpireHours() : 24;
// 构建下载信息DTO
DownloadFileDTO dto = new DownloadFileDTO();
dto.setCode(token);
dto.setFilename(fileInfo.getFilename());
dto.setFilePath(fileInfo.getFilePath());
dto.setExtractCode(code);
// 存储到Redis,设置过期时间
redisComponent.saveDownloadCode(token, dto, expire * 3600);
Map<String, String> result = new HashMap<>();
result.put("shareUrl", "/file/shareDownload/" + token);
result.put("extractCode", code);
result.put("expireTime", expire + "小时");
return result;
}
/**
* 分享文件下载 - 支持未登录访问
* 验证提取码和过期时间
*/
@GetMapping("/shareDownload/{token}")
public void shareDownload(HttpServletRequest request, HttpServletResponse response,
@PathVariable String token,
@RequestParam(required = false) String extractCode) {
DownloadFileDTO dto = redisComponent.getDownloadCode(token);
if (dto == null) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"链接已失效\"}");
return;
}
// 验证提取码
if (dto.getExtractCode() != null && !dto.getExtractCode().isEmpty()) {
if (!dto.getExtractCode().equals(extractCode)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"提取码错误\"}");
return;
}
}
// 从MinIO下载文件
String filePath = dto.getFilePath();
String filename = dto.getFilename();
if (minioService.fileExists(filePath)) {
InputStream inputStream = minioService.getFileInputStream(filePath);
response.setContentType("application/x-msdownload; charset=UTF-8");
filename = new String(filename.getBytes("UTF-8"), "ISO8859-1");
response.setHeader("Content-Disposition", "attachment;filename=\"" + filename + "\"");
FileUtil.readFileFromStream(response, inputStream);
}
}
}
3. 用户认证与授权
集成QQ第三方登录,实现多种认证方式:
/**
* 用户控制器 - 认证与授权管理
*/
@RestController
public class UserInfoController {
/**
* QQ第三方登录回调处理
* 实现OAuth2.0标准流程
*/
@PostMapping("/api/qq/callback")
public SessionWebUserVO qqCallback(@RequestBody Map<String, String> param) {
String code = param.get("code");
RestTemplate restTemplate = new RestTemplate();
// 1. 授权码换取访问令牌
String tokenUrl = String.format(
"https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s",
qqAppId, qqAppKey, code, qqRedirectUri);
String tokenResp = restTemplate.getForObject(tokenUrl, String.class);
// 2. 访问令牌换取OpenID
String accessToken = extractAccessToken(tokenResp);
String openIdUrl = String.format("https://graph.qq.com/oauth2.0/me?access_token=%s", accessToken);
String openIdResp = restTemplate.getForObject(openIdUrl, String.class);
String openId = extractOpenId(openIdResp);
// 3. 获取用户信息
String userInfoUrl = String.format(
"https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s",
accessToken, qqAppId, openId);
String userInfoResp = restTemplate.getForObject(userInfoUrl, String.class);
JSONObject userJson = new JSONObject(userInfoResp);
// 4. 查找或创建用户
UserInfo user = userInfoService.getOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getQqOpenId, openId));
if (user == null) {
user = createUserFromQQ(userJson, openId);
userInfoService.save(user);
}
// 5. 构建会话信息
SessionWebUserVO vo = new SessionWebUserVO();
vo.setId(user.getId());
vo.setNickname(user.getNickname());
vo.setAvatar(user.getQqAvatar());
return vo;
}
/**
* 头像上传与更新
* 支持MinIO存储和本地文件兼容
*/
@PostMapping("/updateUserAvatar")
@LoginValidator
public void updateUserAvatar(HttpSession session, MultipartFile avatar) {
SessionWebUserVO userVo = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
String avatarObjectName = Constants.FILE_FOLDER_AVATAR_NAME + userVo.getId() + Constants.AVATAR_SUFFIX;
try {
// 上传到MinIO
minioService.uploadFile(avatarObjectName, avatar);
// 更新数据库
UserInfo userInfo = new UserInfo();
userInfo.setId(userVo.getId());
userInfo.setAvatar(avatarObjectName);
userInfo.setQqAvatar(""); // 清除QQ头像
userInfoService.updateById(userInfo);
// 更新会话
userVo.setAvatar(avatarObjectName);
session.setAttribute(Constants.SESSION_KEY, userVo);
} catch (Exception e) {
throw new BizException("头像更新失败");
}
}
}
4. 文件预览系统
支持多格式文件预览,包括图片、视频、文档等:
/**
* 文件预览控制器 - 多格式预览支持
*/
@GetMapping("/preview/{id}")
public void previewFile(HttpSession session, HttpServletResponse response,
@PathVariable("id") @NotBlank String id) {
SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
FileInfo fileInfo = fileInfoService.getOne(new LambdaQueryWrapper<FileInfo>()
.eq(FileInfo::getId, id)
.eq(FileInfo::getUserId, user.getId()));
if (fileInfo == null) return;
String filePath = fileInfo.getFilePath();
String filename = fileInfo.getFilename();
if (minioService.fileExists(filePath)) {
InputStream inputStream = minioService.getFileInputStream(filePath);
String contentType = getPreviewContentType(filename);
// 检查是否支持预览
if ("application/octet-stream".equals(contentType)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"暂不支持该类型文件预览\"}");
return;
}
response.setContentType(contentType);
// 图片设置缓存
if (contentType.startsWith("image/")) {
response.setHeader("Cache-Control", "max-age=2592000");
}
FileUtil.readFileFromStream(response, inputStream);
}
}
/**
* 根据文件扩展名获取预览Content-Type
*/
private String getPreviewContentType(String filename) {
String suffix = StringTools.getFileSuffix(filename).toLowerCase();
switch (suffix) {
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".png":
return "image/png";
case ".mp4":
return "video/mp4";
case ".pdf":
return "application/pdf";
case ".txt":
return "text/plain";
case ".html":
return "text/html";
default:
return "application/octet-stream";
}
}
5. 回收站机制
实现软删除和恢复机制,保护用户数据:
/**
* 回收站控制器 - 文件恢复与彻底删除
*/
@RestController
@RequestMapping("/recycle")
public class RecycleController {
/**
* 加载回收站文件列表
* 按删除时间倒序排列
*/
@GetMapping("/loadRecycleList")
public IPage<FileInfoVO> loadRecycleList(HttpSession session, FileInfoQuery query) {
Page<FileInfo> pageParam = new Page<>(query.getPage(), query.getLimit());
query.setUserId(((SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY)).getId());
query.setDeleted(FileDelFlagEnums.RECYCLE.getFlag());
query.setOrderBy("recovery_time desc");
return fileInfoService.pageInfo(pageParam, query);
}
/**
* 恢复文件到原位置
* 支持批量恢复
*/
@PutMapping("/recoverFile/{ids}")
public void recoverFile(HttpSession session, @PathVariable("ids") @NotEmpty String ids) {
SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
fileInfoService.recoverFileBatch(user.getId(), ids);
}
/**
* 彻底删除文件
* 从数据库和MinIO中永久删除
*/
@DeleteMapping("/delFile/{ids}")
public void delFile(HttpSession session, @PathVariable("ids") @NotEmpty String ids) {
SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
userFileService.delFileBatch(user.getId(), ids, false);
}
}
系统优化与亮点
1. 性能优化策略
缓存架构设计:
/**
* Redis缓存组件 - 多级缓存策略
*/
@Component("redisComponent")
public class RedisComponent {
// 用户空间使用情况缓存
public void saveUserSpaceUse(String userId, UserSpaceDTO userSpaceDto) {
redisUtils.setex(Constants.REDIS_KEY_USER_SPACE_USE + userId,
userSpaceDto, Constants.REDIS_KEY_EXPIRE_ONE_DAY);
}
// 文件下载码缓存(支持自定义过期时间)
public void saveDownloadCode(String code, DownloadFileDTO fileDTO, long expireSeconds) {
redisUtils.setex(Constants.REDIS_KEY_DOWNLOAD + code, fileDTO, expireSeconds);
}
// 临时文件大小统计
public void saveFileTempSize(String userId, String fileId, Long fileSize) {
Long currentSize = getFileTempSize(userId, fileId);
String key = Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId;
redisUtils.setex(key, currentSize + fileSize, Constants.REDIS_KEY_EXPIRE_ONE_HOUR);
}
}
数据库优化:
- 索引优化:用户ID、文件类型、删除状态等关键字段建立复合索引
- 分页查询:MyBatis Plus分页插件,避免全表扫描
- 连接池配置:HikariCP高性能连接池
2. 安全机制设计
文件访问控制:
/**
* 登录验证切面 - 统一权限控制
*/
@Aspect
@Component
public class LoginAspect {
@Around("@annotation(loginValidator)")
public Object around(ProceedingJoinPoint point, LoginValidator loginValidator) throws Throwable {
if (loginValidator.validated()) {
// 验证用户登录状态
HttpSession session = getHttpSession(point);
SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY);
if (user == null) {
throw new BizException(ResponseCode.CODE_901);
}
}
return point.proceed();
}
}
数据加密存储:
- 密码加密:MD5 + 盐值加密
- 文件路径:UUID生成唯一标识
- 分享链接:32位随机字符串
3. 微服务架构设计
模块化设计:
easypan-server/
├── common/ # 公共模块
│ └── common-util/ # 工具类库
├── server/ # 主服务模块
│ ├── controller/ # 控制器层
│ ├── service/ # 业务逻辑层
│ ├── mapper/ # 数据访问层
│ └── entity/ # 实体类
依赖管理:
<!-- 父POM统一版本管理 -->
<properties>
<springboot.version>2.6.1</springboot.version>
<mysql.version>8.0.30</mysql.version>
<mybatis-plus.version>3.4.1</mybatis-plus.version>
<fastjson.version>2.0.21</fastjson.version>
</properties>
技术亮点总结
1. 架构设计亮点
- 分层架构:Controller-Service-Mapper清晰分层
- 模块化设计:common-util工具库独立打包
- 缓存策略:Redis多级缓存提升性能
- 存储分离:MinIO对象存储 + MySQL元数据
2. 功能实现亮点
- 分片上传:支持大文件断点续传
- 文件分享:提取码 + 过期时间双重保护
- 第三方登录:QQ OAuth2.0标准实现
- 文件预览:多格式预览支持
- 回收站机制:软删除 + 恢复功能
3. 性能优化亮点
- 连接池优化:HikariCP高性能连接池
- 缓存策略:Redis缓存热点数据
- 静态资源:Nginx缓存 + CDN加速
- 数据库优化:复合索引 + 分页查询
4. 安全机制亮点
- 权限控制:AOP切面统一验证
- 数据加密:密码MD5加密存储
- 文件隔离:用户文件路径隔离
- 访问控制:分享链接权限验证
学习收获与技能提升
技术能力提升
- Spring Boot微服务架构:掌握企业级应用开发模式
- 对象存储技术:MinIO分布式存储实践
- 缓存架构设计:Redis多级缓存策略
- 第三方集成:OAuth2.0标准实现
- 文件处理技术:分片上传、转码、预览
系统设计能力
- 架构设计:分层架构、模块化设计
- 性能优化:缓存策略、数据库优化
- 安全设计:权限控制、数据加密
- 用户体验:文件预览、分享机制
工程实践能力
- 代码规范:统一异常处理、响应格式
- 测试驱动:单元测试、集成测试
- 部署运维:Docker容器化、Nginx配置
- 监控告警:日志管理、性能监控
通过EasyPan云盘系统的设计与实现,深入理解了现代Web应用的全栈开发模式,掌握了微服务架构、对象存储、缓存设计等核心技术,为后续大型项目开发奠定了坚实基础。
EasyPan云盘系统设计与实现