基于Spring Boot + Vue.js构建的现代化云盘存储系统,集成MinIO对象存储、Redis缓存、MySQL数据库,实现文件上传下载、分享、预览等核心功能,支持QQ第三方登录、文件回收站、视频转码等高级特性。

系统架构设计

系统架构图

核心功能实现

模块名称 主要功能描述
用户服务 处理用户注册、登录、身份验证(JWT)、资料修改、密码找回等功能
文件服务 实现文件的上传、分片合并、文件删除、下载、重命名、目录管理等
分享服务 用户生成分享链接,设置提取码与有效期,访问权限控制等
网关服务 提供统一访问入口,鉴权校验、服务路由转发、防刷接口限流
管理后台服务 管理员可查看系统统计、用户活跃度、违规行为、后台文件管理
注册中心 管理各服务的注册、发现与健康检查,实现服务间的解耦与动态扩展
技术 用途说明
Spring 单体服务构建
Spring 服务治理、网关路由、配置中心
Nacos 服务注册与发现
MinIO 分布式对象存储
MySQL 数据存储
Redis 缓存热数据与Token
Docker 容器化部署与快速集成
JWT 鉴权令牌机制
RESTful 微服务之间通信

数据库设计

  1. email_code(邮箱验证码表)
字段名 类型 长度 默认 说明
email varchar 150 - 邮箱
code varchar 5 - 验证码
status tinyint - NULL 0未用/1已用
create_time datetime - NULL 创建时间

主键:(email,code)

  1. file_info(文件信息表)
字段名 类型 长度 默认 说明
id varchar 15 - 文件ID
user_id varchar 15 - 用户ID
file_md5 varchar 32 NULL 文件MD5值
file_pid varchar - NULL 父目录ID
file_size bigint 200 NULL 文件大小
filename varchar 100 NULL 文件名
file_cover varchar 100 NULL 文件封面
file_path varchar - NULL 文件路径
folder_type tinyint - NULL 0文件/1目录
file_category tinyint - NULL 文件分类1-5
file_type tinyint - NULL 文件类型1-10
status tinyint - NULL 转码状态0-2
recovery_time datetime - NULL 回收站时间
version int - 1 乐观锁
deleted tinyint - 0 0正常/1回收站
create_time datetime - NULL 创建时间
update_time datetime - NULL 更新时间

主键:(id, user_id) 索引:创建时间、用户ID、MD5、父目录等

  1. file_share(文件分享表)
字段名 类型 长度 默认 说明
id varchar 20 NULL 分享ID
file_id varchar 10 NULL 文件ID
user_id varchar 10 NULL 用户ID
valid_type tinyint - NULL 有效期类型(0-3)
code varchar 5 NULL 提取码
browse_count int - 0 浏览次数
save_count int - 0 保存次数
download_count int - 0 下载次数
deleted tinyint - 0 逻辑删除
version int - 1 乐观锁
expire_time datetime - NULL 过期时间
create_time datetime - NULL 创建时间
update_time datetime - NULL 更新时间
  1. user_info(用户信息表)
字段名 类型 长度 默认 说明
id varchar 15 - 用户ID
nickname varchar 20 NULL 昵称
email varchar 150 NULL 邮箱
qq_open_id varchar 35 NULL QQ openID
qq_avatar varchar 150 NULL QQ头像
password varchar 32 NULL 密码(MD5)
last_login_time datetime - NULL 最后登录时间
use_space bigint - NULL 已用空间(byte)
total_space bigint - NULL 总空间(byte)
status tinyint - 1 0禁用/1启用
version int - 1 乐观锁
deleted tinyint - 0 逻辑删除
create_time datetime - NULL 创建时间
update_time datetime - NULL 更新时间

唯一索引:email、qq_open_id、nickname

系统功能展示

注册登录

文件上传与分片合并功能实现

文件分享与提取码功能实现

获取提取码成功页面

文件管理功能实现

重命名

删除文件夹

管理员后台功能实现

文件存储架构

系统采用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缓存

文件分享机制

实现安全可控的文件分享功能,支持提取码和过期时间:

/**
 * 文件分享控制器 - 核心分享逻辑
 */
@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);
        }
    }
}

用户认证与授权

集成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("头像更新失败");
        }
    }
}

文件预览系统

支持多格式文件预览,包括图片、视频、文档等:

/**
 * 文件预览控制器 - 多格式预览支持
 */
@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";
    }
}

回收站机制

实现软删除恢复机制,保护用户数据:

/**
 * 回收站控制器 - 文件恢复与彻底删除
 */
@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);
    }
}

系统优化与亮点

性能优化策略

缓存架构设计:

/**
 * 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高性能连接池

安全机制设计

文件访问控制:

/**
 * 登录验证切面 - 统一权限控制
 */
@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位随机字符串

微服务架构设计

模块化设计:

easypan-server/
├── common/                 # 公共模块
│   └── common-util/        # 工具类库
├── server/                # 主服务模块
│   ├── controller/        # 控制器层
│   ├── service/          # 业务逻辑层
│   ├── mapper/           # 数据访问层
│   └── entity/           # 实体类