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

核心功能实现
| 模块名称 | 主要功能描述 |
|---|---|
| 用户服务 | 处理用户注册、登录、身份验证(JWT)、资料修改、密码找回等功能 |
| 文件服务 | 实现文件的上传、分片合并、文件删除、下载、重命名、目录管理等 |
| 分享服务 | 用户生成分享链接,设置提取码与有效期,访问权限控制等 |
| 网关服务 | 提供统一访问入口,鉴权校验、服务路由转发、防刷接口限流 |
| 管理后台服务 | 管理员可查看系统统计、用户活跃度、违规行为、后台文件管理 |
| 注册中心 | 管理各服务的注册、发现与健康检查,实现服务间的解耦与动态扩展 |
| 技术 | 用途说明 |
|---|---|
| Spring | 单体服务构建 |
| Spring | 服务治理、网关路由、配置中心 |
| Nacos | 服务注册与发现 |
| MinIO | 分布式对象存储 |
| MySQL | 数据存储 |
| Redis | 缓存热数据与Token |
| Docker | 容器化部署与快速集成 |
| JWT | 鉴权令牌机制 |
| RESTful | 微服务之间通信 |
数据库设计
- email_code(邮箱验证码表)
| 字段名 | 类型 | 长度 | 空 | 默认 | 说明 |
|---|---|---|---|---|---|
| varchar | 150 | 否 | - | 邮箱 | |
| code | varchar | 5 | 否 | - | 验证码 |
| status | tinyint | - | 是 | NULL | 0未用/1已用 |
| create_time | datetime | - | 是 | NULL | 创建时间 |
主键:(email,code)
- 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、父目录等
- 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 | 更新时间 |
- user_info(用户信息表)
| 字段名 | 类型 | 长度 | 空 | 默认 | 说明 |
|---|---|---|---|---|---|
| id | varchar | 15 | 否 | - | 用户ID |
| nickname | varchar | 20 | 是 | NULL | 昵称 |
| 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/ # 实体类
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 奇点智库 SingularityMind!
评论









