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加密存储
  • 文件隔离:用户文件路径隔离
  • 访问控制:分享链接权限验证

学习收获与技能提升

技术能力提升

  1. Spring Boot微服务架构:掌握企业级应用开发模式
  2. 对象存储技术:MinIO分布式存储实践
  3. 缓存架构设计:Redis多级缓存策略
  4. 第三方集成:OAuth2.0标准实现
  5. 文件处理技术:分片上传、转码、预览

系统设计能力

  1. 架构设计:分层架构、模块化设计
  2. 性能优化:缓存策略、数据库优化
  3. 安全设计:权限控制、数据加密
  4. 用户体验:文件预览、分享机制

工程实践能力

  1. 代码规范:统一异常处理、响应格式
  2. 测试驱动:单元测试、集成测试
  3. 部署运维:Docker容器化、Nginx配置
  4. 监控告警:日志管理、性能监控

通过EasyPan云盘系统的设计与实现,深入理解了现代Web应用的全栈开发模式,掌握了微服务架构、对象存储、缓存设计等核心技术,为后续大型项目开发奠定了坚实基础。

作者

HuangZhongqi

发布于

2024-12-24

更新于

2025-10-03

许可协议

评论