目录
- 一. 🦁 前言
- 二. 🦁 主要技术栈
- 三. 🦁 架构搭建
-
- 1. 项目搭建效果
- 2. 各部分作用
- 四. 🦁 主要功能
-
- 1.功能图
- 2. 主要功能
-
- 2.1 分片上传文件
- 2.2 存储分享记录
- 五. 🦁 效果显示
- 🦁 干货分享 🦁
一. 🦁 前言
源码获取地址:https://download.csdn.net/download/m0_58847451/87789468?spm=1001.2014.3001.5503
本系统是一个文件存储和共享平台,提供了强大的功能和可靠的数据保护,方便用户随时随地进行文件的管理和分享。本系统基于Springboot+Vue
实现,操作简便,界面美观,拥有良好的用户体验和稳定的性能。无论是个人用户还是团队合作,都能够在此享受到高效和便捷的文件存储和共享服务。
二. 🦁 主要技术栈
- 后端
SpringBoot
、MySQL
、Mybatis
、SpringMVC
、Redis
、ffmpeg
- 前端
Vue3
、Element-Plus
三. 🦁 架构搭建
tips:
该项目的项目结构改编参考了天罡大佬的《Maven 三层项目结构搭建》一文(天罡大佬人很好,很热心解答狮子疑惑的问题,推荐关注哦
!!!),但是又有写不同的地方,狮子根据自己的理解整理成如下结构:
1. 项目搭建效果
2. 各部分作用
- 表现层: web层,提供controller,处理http请求,提供给前端的API;
- 业务逻辑层: service层,和业务相关,主要处理业务逻辑;
- 数据访问层: dal层,做数据访问,主要使用Mybatis与MySQL交互数据;
- 通用层:common层,主要存放实体类,常量,工具,异常类等。
四. 🦁 主要功能
1.功能图
2. 主要功能
2.1 分片上传文件
这个方法是本系统的核心功能,前端将文件分片后将参数传回,后台接收上传文件的参数,将文件暂存在临时目录中。如果是第一个分片,则判断是否存在相同的文件(根据文件的 MD5 值判断),如果存在相同的文件,则将其直接作为秒传处理,否则继续将文件保存在临时目录中。如果是最后一个分片,则将其记录在数据库中,并异步调用文件合并的方法。在整个上传过程中,还需要进行磁盘空间和用户可用空间的判断。最后返回上传结果。
@Transactional(rollbackFor = Exception.class)
public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName, String filePid, String fileMd5,
Integer chunkIndex,
Integer chunks) {
File tempFileFolder = null;
Boolean uploadSuccess = true;
try {
UploadResultDto resultDto = new UploadResultDto();
if (StringTools.isEmpty(fileId)) {
fileId = StringTools.getRandomString(Constants.LENGTH_10);
}
resultDto.setFileId(fileId);
Date curDate = new Date();
UserSpaceDto spaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
if (chunkIndex == 0) {
FileInfoQuery infoQuery = new FileInfoQuery();
infoQuery.setFileMd5(fileMd5);
infoQuery.setSimplePage(new SimplePage(0, 1));
infoQuery.setStatus(FileStatusEnums.USING.getStatus());
ListFileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);
//秒传
if (!dbFileList.isEmpty()) {
FileInfo dbFile = dbFileList.get(0);
//判断文件状态
if (dbFile.getFileSize() + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
throw new BusinessException(ResponseCodeEnum.CODE_904);
}
dbFile.setFileId(fileId);
dbFile.setFilePid(filePid);
dbFile.setUserId(webUserDto.getUserId());
dbFile.setFileMd5(null);
dbFile.setCreateTime(curDate);
dbFile.setLastUpdateTime(curDate);
dbFile.setStatus(FileStatusEnums.USING.getStatus());
dbFile.setDelFlag(FileDelFlagEnums.USING.getFlag());
dbFile.setFileMd5(fileMd5);
fileName = autoRename(filePid, webUserDto.getUserId(), fileName);
dbFile.setFileName(fileName);
this.fileInfoMapper.insert(dbFile);
resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode());
//更新用户空间使用
updateUserSpace(webUserDto, dbFile.getFileSize());
return resultDto;
}
}
//暂存在临时目录
String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
String currentUserFolderName = webUserDto.getUserId() + fileId;
//创建临时目录
tempFileFolder = new File(tempFolderName + currentUserFolderName);
if (!tempFileFolder.exists()) {
tempFileFolder.mkdirs();
}
//判断磁盘空间
Long currentTempSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
if (file.getSize() + currentTempSize + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
throw new BusinessException(ResponseCodeEnum.CODE_904);
}
File newFile = new File(tempFileFolder.getPath() + "/" + chunkIndex);
file.transferTo(newFile);
//保存临时大小
redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
//不是最后一个分片,直接返回
if (chunkIndex chunks - 1) {
resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
return resultDto;
}
redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
//最后一个分片上传完成,记录数据库,异步合并分片
String month = DateUtil.format(curDate, DateTimePatternEnum.YYYYMM.getPattern());
String fileSuffix = StringTools.getFileSuffix(fileName);
//真实文件名
String realFileName = currentUserFolderName + fileSuffix;
FileTypeEnums fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
//自动重命名
fileName = autoRename(filePid, webUserDto.getUserId(), fileName);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(fileId);
fileInfo.setUserId(webUserDto.getUserId());
fileInfo.setFileMd5(fileMd5);
fileInfo.setFileName(fileName);
fileInfo.setFilePath(month + "/" + realFileName);
fileInfo.setFilePid(filePid);
fileInfo.setCreateTime(curDate);
fileInfo.setLastUpdateTime(curDate);
fileInfo.setFileCategory(fileTypeEnum.getCategory().getCategory());
fileInfo.setFileType(fileTypeEnum.getType());
fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());
fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());
fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
this.fileInfoMapper.insert(fileInfo);
Long totalSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
updateUserSpace(webUserDto, totalSize);
resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());
//事务提交后调用异步方法
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
}
});
return resultDto;
} catch (BusinessException e) {
uploadSuccess = false;
logger.error("文件上传失败", e);
throw e;
} catch (Exception e) {
uploadSuccess = false;
logger.error("文件上传失败", e);
throw new BusinessException("文件上传失败");
} finally {
//如果上传失败,清除临时目录
if (tempFileFolder != null && !uploadSuccess) {
try {
FileUtils.deleteDirectory(tempFileFolder);
} catch (IOException e) {
logger.error("删除临时目录失败");
}
}
}
}
2.2 存储分享记录
这个方法用于存储分享记录,方法签名为public void saveShare(FileShare share)。方法的输入参数为一个FileShare对象。
在方法内部,首先根据share对象中的validType属性获取对应的ShareValidTypeEnums枚举值,如果无法获取该枚举值,则抛出BusinessException异常并返回错误码ResponseCodeEnum.CODE_600。
接着,根据validType属性判断分享是否永久有效,如果不是,则设置该分享的过期时间为当前时间加上对应的天数。
然后,获取当前时间作为分享时间,并为分享记录设置随机的分享ID和提取码(如果提取码为空)。
最后,将分享记录保存至数据库中。
@Override
public void saveShare(FileShare share) {
ShareValidTypeEnums typeEnum = ShareValidTypeEnums.getByType(share.getValidType());
if (null == typeEnum) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
if (typeEnum != ShareValidTypeEnums.FOREVER) {
share.setExpireTime(DateUtil.getAfterDate(typeEnum.getDays()));
}
Date curDate = new Date();
share.setShareTime(curDate);
if (StringTools.isEmpty(share.getCode())) {
share.setCode(StringTools.getRandomString(Constants.LENGTH_5));
}
share.setShareId(StringTools.getRandomString(Constants.LENGTH_20));
this.fileShareMapper.insert(share);
}
五. 🦁 效果显示
🦁 干货分享 🦁
在Spring Boot中,可以使用Spring Security来实现应用程序的安全认证机制。实现步骤如下:
- 添加Spring Security的依赖
在使用Spring Boot创建应用程序时,可以在pom.xml文件中添加Spring Security的依赖:
dependency>
groupId>org.springframework.bootgroupId>
artifactId>spring-boot-starter-securityartifactId>
dependency>
- 配置Spring Security
在Spring Boot中配置Spring Security非常简单,只需要在application.properties或application.yml中添加以下配置:
# 启用Spring Security
spring.security.enabled=true
# 配置用户名和密码
spring.security.user.name=admin
spring.security.user.password=admin
或者使用Java配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("USER");
}
}
- 配置登录页面和注销页面
在Spring Boot中,可以通过配置application.properties或application.yml文件来指定登录页面和注销页面。
# 指定登录页面
spring.security.form-login.login-page=/login
# 指定注销页面
spring.security.logout.success-url=/logout
# 指定登录页面
spring:
security:
form-login:
login-page: /login
# 指定注销页面
logout:
success-url: /logout
- 配置权限控制
在Spring Boot中,可以通过配置@EnableGlobalMethodSecurity和@PreAuthorize注解来实现方法级别的权限控制。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private MethodSecurityService methodSecurityService;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(methodSecurityService));
return expressionHandler;
}
}
@Service
public class MethodSecurityService {
private ListString> roles = Arrays.asList("USER", "ADMIN");
public boolean hasPermission(String permission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
ListGrantedAuthority> authorities = (ListGrantedAuthority>) authentication.getAuthorities();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(permission::equals);
}
@PreAuthorize("hasPermission('user.read')")
public String readUser() {
return "Read user";
}
@PreAuthorize("hasRole('ADMIN')")
public String deleteUser() {
return "Delete user";
}
}