23 changed files with 553 additions and 261 deletions
@ -0,0 +1,67 @@ |
|||||
|
package com.bx.implatform.entity; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.annotation.TableId; |
||||
|
import com.baomidou.mybatisplus.annotation.TableName; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* @author Blue |
||||
|
* @version 1.0 |
||||
|
*/ |
||||
|
@Data |
||||
|
@TableName("im_file_info") |
||||
|
public class FileInfo { |
||||
|
|
||||
|
/** |
||||
|
* 文件ID |
||||
|
*/ |
||||
|
@TableId |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 文件名 |
||||
|
*/ |
||||
|
private String fileName; |
||||
|
|
||||
|
/** |
||||
|
* 原始文件存储路径 |
||||
|
*/ |
||||
|
private String filePath; |
||||
|
|
||||
|
/** |
||||
|
* 压缩文件存储路径 |
||||
|
*/ |
||||
|
private String compressedPath; |
||||
|
|
||||
|
/** |
||||
|
* 封面文件路径 |
||||
|
*/ |
||||
|
private String coverPath; |
||||
|
|
||||
|
/** |
||||
|
* 原始文件大小(字节) |
||||
|
*/ |
||||
|
private Long fileSize; |
||||
|
|
||||
|
/** |
||||
|
* 上传时间 |
||||
|
*/ |
||||
|
private Date uploadTime; |
||||
|
|
||||
|
/** |
||||
|
* 文件类型,枚举: FileType |
||||
|
*/ |
||||
|
private Integer fileType; |
||||
|
|
||||
|
/** |
||||
|
* 是否永久存储 |
||||
|
*/ |
||||
|
private Boolean isPermanent; |
||||
|
|
||||
|
/** |
||||
|
* 文件MD5哈希值 |
||||
|
*/ |
||||
|
private String md5; |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
package com.bx.implatform.mapper; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import com.bx.implatform.entity.FileInfo; |
||||
|
|
||||
|
public interface FileInfoMapper extends BaseMapper<FileInfo> { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
package com.bx.implatform.service; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.extension.service.IService; |
||||
|
import com.bx.implatform.entity.FileInfo; |
||||
|
import com.bx.implatform.vo.UploadImageVO; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
public interface FileService extends IService<FileInfo> { |
||||
|
|
||||
|
String uploadFile(MultipartFile file); |
||||
|
|
||||
|
UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent); |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,191 @@ |
|||||
|
package com.bx.implatform.service; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
||||
|
import com.bx.implatform.config.props.MinioProperties; |
||||
|
import com.bx.implatform.contant.Constant; |
||||
|
import com.bx.implatform.entity.FileInfo; |
||||
|
import com.bx.implatform.enums.FileType; |
||||
|
import com.bx.implatform.enums.ResultCode; |
||||
|
import com.bx.implatform.exception.GlobalException; |
||||
|
import com.bx.implatform.mapper.FileInfoMapper; |
||||
|
import com.bx.implatform.session.SessionContext; |
||||
|
import com.bx.implatform.thirdparty.MinioService; |
||||
|
import com.bx.implatform.util.FileUtil; |
||||
|
import com.bx.implatform.util.ImageUtil; |
||||
|
import com.bx.implatform.vo.UploadImageVO; |
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
import org.springframework.util.DigestUtils; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
import java.io.IOException; |
||||
|
import java.util.Date; |
||||
|
import java.util.Objects; |
||||
|
|
||||
|
/** |
||||
|
* 文件上传服务 |
||||
|
* |
||||
|
* author: Blue date: 2024-09-28 version: 1.0 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class FileServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileService { |
||||
|
|
||||
|
private final MinioService minioSerivce; |
||||
|
|
||||
|
private final MinioProperties minioProps; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
if (!minioSerivce.bucketExists(minioProps.getBucketName())) { |
||||
|
// 创建bucket
|
||||
|
minioSerivce.makeBucket(minioProps.getBucketName()); |
||||
|
// 公开bucket
|
||||
|
minioSerivce.setBucketPublic(minioProps.getBucketName()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String uploadFile(MultipartFile file) { |
||||
|
try { |
||||
|
Long userId = SessionContext.getSession().getUserId(); |
||||
|
// 大小校验
|
||||
|
if (file.getSize() > Constant.MAX_FILE_SIZE) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M"); |
||||
|
} |
||||
|
// 如果文件已存在,直接复用
|
||||
|
String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); |
||||
|
FileInfo fileInfo = findByMd5(md5); |
||||
|
if (!Objects.isNull(fileInfo)) { |
||||
|
// 更新上传时间
|
||||
|
fileInfo.setUploadTime(new Date()); |
||||
|
this.updateById(fileInfo); |
||||
|
// 返回
|
||||
|
return fileInfo.getFilePath(); |
||||
|
} |
||||
|
// 上传
|
||||
|
String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getFilePath(), file); |
||||
|
if (StringUtils.isEmpty(fileName)) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败"); |
||||
|
} |
||||
|
String url = generUrl(FileType.FILE, fileName); |
||||
|
// 保存文件
|
||||
|
saveFileInfo(file, md5, url); |
||||
|
log.info("文件文件成功,用户id:{},url:{}", userId, url); |
||||
|
return url; |
||||
|
} catch (IOException e) { |
||||
|
log.error("上传图片失败,{}", e.getMessage(), e); |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "上传图片失败"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Transactional |
||||
|
@Override |
||||
|
public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent) { |
||||
|
try { |
||||
|
Long userId = SessionContext.getSession().getUserId(); |
||||
|
// 大小校验
|
||||
|
if (file.getSize() > Constant.MAX_IMAGE_SIZE) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片大小不能超过20M"); |
||||
|
} |
||||
|
// 图片格式校验
|
||||
|
if (!FileUtil.isImage(file.getOriginalFilename())) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片格式不合法"); |
||||
|
} |
||||
|
UploadImageVO vo = new UploadImageVO(); |
||||
|
// 如果文件已存在,直接复用
|
||||
|
String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); |
||||
|
FileInfo fileInfo = findByMd5(md5); |
||||
|
if (!Objects.isNull(fileInfo)) { |
||||
|
// 更新上传时间和持久化标记
|
||||
|
fileInfo.setIsPermanent(isPermanent || fileInfo.getIsPermanent()); |
||||
|
fileInfo.setUploadTime(new Date()); |
||||
|
this.updateById(fileInfo); |
||||
|
// 返回
|
||||
|
vo.setOriginUrl(fileInfo.getFilePath()); |
||||
|
vo.setThumbUrl(fileInfo.getCompressedPath()); |
||||
|
return vo; |
||||
|
} |
||||
|
// 上传原图
|
||||
|
String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), file); |
||||
|
if (StringUtils.isEmpty(fileName)) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
||||
|
} |
||||
|
vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); |
||||
|
if (file.getSize() > 50 * 1024) { |
||||
|
// 大于50K的文件需上传缩略图
|
||||
|
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); |
||||
|
String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), |
||||
|
file.getOriginalFilename(), imageByte, file.getContentType()); |
||||
|
if (StringUtils.isEmpty(thumbFileName)) { |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
||||
|
} |
||||
|
vo.setThumbUrl(generUrl(FileType.IMAGE, thumbFileName)); |
||||
|
// 保存文件信息
|
||||
|
saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), isPermanent); |
||||
|
}else{ |
||||
|
// 小于50k,用原图充当缩略图
|
||||
|
vo.setThumbUrl(generUrl(FileType.IMAGE, fileName)); |
||||
|
// 保存文件信息,由于缩略图不允许删除,此时原图也不允许删除
|
||||
|
saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), true); |
||||
|
} |
||||
|
log.info("文件图片成功,用户id:{},url:{}", userId, vo.getOriginUrl()); |
||||
|
return vo; |
||||
|
} catch (IOException e) { |
||||
|
log.error("上传图片失败,{}", e.getMessage(), e); |
||||
|
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private String generUrl(FileType fileType, String fileName) { |
||||
|
return StrUtil.join("/", minioProps.getDomain(), minioProps.getBucketName(), getBucketPath(fileType), fileName); |
||||
|
} |
||||
|
|
||||
|
private String getBucketPath(FileType fileType) { |
||||
|
return switch (fileType) { |
||||
|
case FILE -> minioProps.getFilePath(); |
||||
|
case IMAGE -> minioProps.getImagePath(); |
||||
|
case VIDEO -> minioProps.getVideoPath(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private FileInfo findByMd5(String md5) { |
||||
|
LambdaQueryWrapper<FileInfo> wrapper = Wrappers.lambdaQuery(); |
||||
|
wrapper.eq(FileInfo::getMd5, md5); |
||||
|
return getOne(wrapper); |
||||
|
} |
||||
|
|
||||
|
private void saveImageFileInfo(MultipartFile file, String md5, String filePath, String compressedPath, |
||||
|
Boolean isPermanent) throws IOException { |
||||
|
FileInfo fileInfo = new FileInfo(); |
||||
|
fileInfo.setFileName(file.getOriginalFilename()); |
||||
|
fileInfo.setFileSize(file.getSize()); |
||||
|
fileInfo.setFileType(FileType.IMAGE.code()); |
||||
|
fileInfo.setFilePath(filePath); |
||||
|
fileInfo.setCompressedPath(compressedPath); |
||||
|
fileInfo.setMd5(md5); |
||||
|
fileInfo.setIsPermanent(isPermanent); |
||||
|
fileInfo.setUploadTime(new Date()); |
||||
|
this.save(fileInfo); |
||||
|
} |
||||
|
|
||||
|
private void saveFileInfo(MultipartFile file, String md5, String filePath) throws IOException { |
||||
|
FileInfo fileInfo = new FileInfo(); |
||||
|
fileInfo.setFileName(file.getOriginalFilename()); |
||||
|
fileInfo.setFileSize(file.getSize()); |
||||
|
fileInfo.setFileType(FileType.FILE.code()); |
||||
|
fileInfo.setFilePath(filePath); |
||||
|
fileInfo.setMd5(md5); |
||||
|
fileInfo.setIsPermanent(false); |
||||
|
fileInfo.setUploadTime(new Date()); |
||||
|
this.save(fileInfo); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,121 +0,0 @@ |
|||||
package com.bx.implatform.service.thirdparty; |
|
||||
|
|
||||
import com.bx.implatform.config.props.MinioProperties; |
|
||||
import com.bx.implatform.contant.Constant; |
|
||||
import com.bx.implatform.enums.FileType; |
|
||||
import com.bx.implatform.enums.ResultCode; |
|
||||
import com.bx.implatform.exception.GlobalException; |
|
||||
import com.bx.implatform.session.SessionContext; |
|
||||
import com.bx.implatform.util.FileUtil; |
|
||||
import com.bx.implatform.util.ImageUtil; |
|
||||
import com.bx.implatform.util.MinioUtil; |
|
||||
import com.bx.implatform.vo.UploadImageVO; |
|
||||
import jakarta.annotation.PostConstruct; |
|
||||
import lombok.RequiredArgsConstructor; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.apache.commons.lang3.StringUtils; |
|
||||
import org.springframework.stereotype.Service; |
|
||||
import org.springframework.web.multipart.MultipartFile; |
|
||||
|
|
||||
import java.io.IOException; |
|
||||
import java.util.Objects; |
|
||||
|
|
||||
/** |
|
||||
* todo 通过校验文件MD5实现重复文件秒传 |
|
||||
* 文件上传服务 |
|
||||
* |
|
||||
* @author Blue |
|
||||
* @date 2022/10/28 |
|
||||
*/ |
|
||||
@Slf4j |
|
||||
@Service |
|
||||
@RequiredArgsConstructor |
|
||||
public class FileService { |
|
||||
private final MinioUtil minioUtil; |
|
||||
|
|
||||
private final MinioProperties minioProps; |
|
||||
|
|
||||
|
|
||||
@PostConstruct |
|
||||
public void init() { |
|
||||
if (!minioUtil.bucketExists(minioProps.getBucketName())) { |
|
||||
// 创建bucket
|
|
||||
minioUtil.makeBucket(minioProps.getBucketName()); |
|
||||
// 公开bucket
|
|
||||
minioUtil.setBucketPublic(minioProps.getBucketName()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
public String uploadFile(MultipartFile file) { |
|
||||
Long userId = SessionContext.getSession().getUserId(); |
|
||||
// 大小校验
|
|
||||
if (file.getSize() > Constant.MAX_FILE_SIZE) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M"); |
|
||||
} |
|
||||
// 上传
|
|
||||
String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getFilePath(), file); |
|
||||
if (StringUtils.isEmpty(fileName)) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败"); |
|
||||
} |
|
||||
String url = generUrl(FileType.FILE, fileName); |
|
||||
log.info("文件文件成功,用户id:{},url:{}", userId, url); |
|
||||
return url; |
|
||||
} |
|
||||
|
|
||||
public UploadImageVO uploadImage(MultipartFile file) { |
|
||||
try { |
|
||||
Long userId = SessionContext.getSession().getUserId(); |
|
||||
// 大小校验
|
|
||||
if (file.getSize() > Constant.MAX_IMAGE_SIZE) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片大小不能超过20M"); |
|
||||
} |
|
||||
// 图片格式校验
|
|
||||
if (!FileUtil.isImage(file.getOriginalFilename())) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片格式不合法"); |
|
||||
} |
|
||||
// 上传原图
|
|
||||
UploadImageVO vo = new UploadImageVO(); |
|
||||
String fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), file); |
|
||||
if (StringUtils.isEmpty(fileName)) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
|
||||
} |
|
||||
vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); |
|
||||
// 大于30K的文件需上传缩略图
|
|
||||
if (file.getSize() > 30 * 1024) { |
|
||||
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); |
|
||||
fileName = minioUtil.upload(minioProps.getBucketName(), minioProps.getImagePath(), Objects.requireNonNull(file.getOriginalFilename()), imageByte, file.getContentType()); |
|
||||
if (StringUtils.isEmpty(fileName)) { |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
|
||||
} |
|
||||
} |
|
||||
vo.setThumbUrl(generUrl(FileType.IMAGE, fileName)); |
|
||||
log.info("文件图片成功,用户id:{},url:{}", userId, vo.getOriginUrl()); |
|
||||
return vo; |
|
||||
} catch (IOException e) { |
|
||||
log.error("上传图片失败,{}", e.getMessage(), e); |
|
||||
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
public String generUrl(FileType fileTypeEnum, String fileName) { |
|
||||
String url = minioProps.getDomain() + "/" + minioProps.getBucketName(); |
|
||||
switch (fileTypeEnum) { |
|
||||
case FILE: |
|
||||
url += "/" + minioProps.getFilePath() + "/"; |
|
||||
break; |
|
||||
case IMAGE: |
|
||||
url += "/" + minioProps.getImagePath() + "/"; |
|
||||
break; |
|
||||
case VIDEO: |
|
||||
url += "/" + minioProps.getVideoPath() + "/"; |
|
||||
break; |
|
||||
default: |
|
||||
break; |
|
||||
} |
|
||||
url += fileName; |
|
||||
return url; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,78 @@ |
|||||
|
package com.bx.implatform.task.schedule; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
||||
|
import com.bx.implatform.annotation.RedisLock; |
||||
|
import com.bx.implatform.config.props.MinioProperties; |
||||
|
import com.bx.implatform.contant.RedisKey; |
||||
|
import com.bx.implatform.entity.FileInfo; |
||||
|
import com.bx.implatform.service.FileService; |
||||
|
import com.bx.implatform.thirdparty.MinioService; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.time.DateUtils; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 过期文件清理任务 |
||||
|
* |
||||
|
* @author Blue |
||||
|
* @version 1.0 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class FileExpireTask { |
||||
|
|
||||
|
private final FileService fileService; |
||||
|
private final MinioService minioService; |
||||
|
private final MinioProperties minioProps; |
||||
|
|
||||
|
@RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK) |
||||
|
@Scheduled(cron = "0 * * * * ?") |
||||
|
public void run() { |
||||
|
log.info("【定时任务】过期文件处理..."); |
||||
|
int batchSize = 100; |
||||
|
List<FileInfo> files = loadBatch(batchSize); |
||||
|
while (true) { |
||||
|
for (FileInfo fileInfo : files) { |
||||
|
String url = fileInfo.getFilePath(); |
||||
|
String relativePath = url.substring(fileInfo.getFilePath().indexOf(minioProps.getBucketName())); |
||||
|
String[] arr = relativePath.split("/"); |
||||
|
String bucket = minioProps.getBucketName(); |
||||
|
String path = arr[1]; |
||||
|
String fileNme = StrUtil.join("/", arr[2], arr[3]); |
||||
|
if (minioService.isExist(bucket, path, fileNme)) { |
||||
|
if (!minioService.remove(bucket, path, fileNme)) { |
||||
|
// 删除失败,不再往下执行
|
||||
|
log.error("删除过期文件异常, id:{},文件名:{}", fileInfo.getId(), fileInfo.getFileName()); |
||||
|
return; |
||||
|
} |
||||
|
// 删除文件信息
|
||||
|
fileService.removeById(fileInfo.getId()); |
||||
|
} |
||||
|
} |
||||
|
if (files.size() < batchSize) { |
||||
|
break; |
||||
|
} |
||||
|
// 下一批
|
||||
|
files = loadBatch(batchSize); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
List<FileInfo> loadBatch(int size) { |
||||
|
Date minDate = DateUtils.addDays(new Date(), -minioProps.getExpireIn()); |
||||
|
LambdaQueryWrapper<FileInfo> wrapper = Wrappers.lambdaQuery(); |
||||
|
wrapper.eq(FileInfo::getIsPermanent, false); |
||||
|
wrapper.le(FileInfo::getUploadTime, minDate); |
||||
|
wrapper.orderByAsc(FileInfo::getId); |
||||
|
wrapper.last("limit " + size); |
||||
|
return fileService.list(wrapper); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 22 KiB |
Loading…
Reference in new issue