diff --git a/db/im-platform.sql b/db/im-platform.sql index e1ca294..887006b 100644 --- a/db/im-platform.sql +++ b/db/im-platform.sql @@ -91,4 +91,18 @@ create table `im_sensitive_word`( `enabled` tinyint DEFAULT 0 COMMENT '是否启用 0:未启用 1:启用', `creator` bigint DEFAULT NULL COMMENT '创建者', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' -)ENGINE=InnoDB CHARSET=utf8mb4 comment '敏感词'; \ No newline at end of file +)ENGINE=InnoDB CHARSET=utf8mb4 comment '敏感词'; + +CREATE TABLE `im_file_info` ( + `id` BIGINT NOT NULL auto_increment PRIMARY key comment 'id', + `file_name` VARCHAR(255) NOT NULL comment '文件名', + `file_path` VARCHAR(255) NOT NULL comment '文件地址', + `file_size` INTEGER NOT NULL comment '文件大小', + `file_type` tinyint NOT NULL comment '0:普通文件 1:图片 2:视频', + `compressed_path` VARCHAR(255) DEFAULT NULL comment '压缩文件路径', + `cover_path` VARCHAR(255) DEFAULT NULL comment '封面文件路径,仅视频文件有效', + `upload_time` datetime DEFAULT CURRENT_TIMESTAMP comment '上传时间', + `is_permanent` tinyint DEFAULT 0 comment '是否永久文件', + `md5` VARCHAR(64) NOT NULL comment '文件md5', + UNIQUE KEY `idx_md5` (md5), +) ENGINE = InnoDB CHARSET = utf8mb4 comment '文件'; \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java index 0421931..fca9a98 100644 --- a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java +++ b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java @@ -21,7 +21,7 @@ public @interface RedisLock { /** * spel 表达式 */ - String key(); + String key() default ""; /** * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1 diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java index dc5c6b5..2c0d26a 100644 --- a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java +++ b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import com.bx.implatform.annotation.RedisLock; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -61,18 +62,22 @@ public class RedisLockAspect { private String parseKey(ProceedingJoinPoint joinPoint){ Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); RedisLock annotation = method.getAnnotation(RedisLock.class); + String key = annotation.key(); + if(StrUtil.isEmpty(key)){ + return Strings.EMPTY; + } // el解析需要的上下文对象 EvaluationContext context = new StandardEvaluationContext(); // 参数名 String[] params = parameterNameDiscoverer.getParameterNames(method); if(Objects.isNull(params)){ - return annotation.key(); + return key; } Object[] args = joinPoint.getArgs(); for (int i = 0; i < params.length; i++) { context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 } - Expression expression = parser.parseExpression(annotation.key()); + Expression expression = parser.parseExpression(key); return expression.getValue(context, String.class); } diff --git a/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java index 2bfbf7f..5a64db3 100644 --- a/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java +++ b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java @@ -29,4 +29,6 @@ public class MinioProperties { private String filePath; private String videoPath; + + private Integer expireIn; } diff --git a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java index 3162ea4..7793598 100644 --- a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java +++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java @@ -53,4 +53,9 @@ public final class RedisKey { */ public static final String IM_REPEAT_SUBMIT = "im:repeat:submit"; + /** + * 分布式锁-清理过期文件 + */ + public static final String IM_LOCK_FILE_TASK = "im:lock:task:file"; + } diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java index ddf9236..7f8295e 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java @@ -2,7 +2,7 @@ package com.bx.implatform.controller; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; -import com.bx.implatform.service.thirdparty.FileService; +import com.bx.implatform.service.FileService; import com.bx.implatform.vo.UploadImageVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -24,15 +24,16 @@ public class FileController { @Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url") @PostMapping("/image/upload") - public Result uploadImage(@RequestParam("file") MultipartFile file) { - return ResultUtils.success(fileService.uploadImage(file)); + public Result uploadImage(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "true") Boolean isPermanent) { + return ResultUtils.success(fileService.uploadImage(file,isPermanent)); } @CrossOrigin @Operation(summary = "上传文件", description = "上传文件,上传后返回文件url") @PostMapping("/file/upload") public Result uploadFile(@RequestParam("file") MultipartFile file) { - return ResultUtils.success(fileService.uploadFile(file), ""); + return ResultUtils.success(fileService.uploadFile(file)); } } \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java new file mode 100644 index 0000000..9a847f5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java @@ -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; +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/FileType.java b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java index f7aea02..2237f48 100644 --- a/im-platform/src/main/java/com/bx/implatform/enums/FileType.java +++ b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java @@ -16,11 +16,7 @@ public enum FileType { /** * 视频 */ - VIDEO(2, "视频"), - /** - * 声音 - */ - AUDIO(3, "声音"); + VIDEO(2, "视频"); private final Integer code; diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java new file mode 100644 index 0000000..37ebf8c --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java @@ -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 { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/FileService.java new file mode 100644 index 0000000..bc32e19 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FileService.java @@ -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 { + + String uploadFile(MultipartFile file); + + UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java new file mode 100644 index 0000000..44b51e8 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FileServiceImpl.java @@ -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 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 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); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java deleted file mode 100644 index 189bfbc..0000000 --- a/im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java +++ /dev/null @@ -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; - } - -} diff --git a/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java new file mode 100644 index 0000000..9dddcb0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java @@ -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 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 loadBatch(int size) { + Date minDate = DateUtils.addDays(new Date(), -minioProps.getExpireIn()); + LambdaQueryWrapper 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); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java similarity index 88% rename from im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java rename to im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java index bd0da21..254a8e2 100644 --- a/im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java +++ b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java @@ -1,5 +1,6 @@ -package com.bx.implatform.util; +package com.bx.implatform.thirdparty; +import com.bx.implatform.util.DateTimeUtils; import io.minio.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -14,7 +15,7 @@ import java.util.Date; @Slf4j @Component @RequiredArgsConstructor -public class MinioUtil { +public class MinioService { private final MinioClient minioClient; @@ -137,11 +138,29 @@ public class MinioUtil { */ public boolean remove(String bucketName, String path, String fileName) { try { - minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(path + fileName).build()); + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); } catch (Exception e) { log.error("删除文件失败,", e); return false; } return true; } + + /** + * 判断文件是否存在 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param fileName 文件名 + * @return + */ + public Boolean isExist(String bucketName, String path, String fileName) { + try { + minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); + } catch (Exception e) { + return false; + } + return true; + } + } diff --git a/im-platform/src/main/resources/application-dev.yml b/im-platform/src/main/resources/application-dev.yml index aff630b..a18048c 100644 --- a/im-platform/src/main/resources/application-dev.yml +++ b/im-platform/src/main/resources/application-dev.yml @@ -18,6 +18,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/application-prod.yml b/im-platform/src/main/resources/application-prod.yml index dc8e1b6..4b2047d 100644 --- a/im-platform/src/main/resources/application-prod.yml +++ b/im-platform/src/main/resources/application-prod.yml @@ -19,6 +19,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/application-test.yml b/im-platform/src/main/resources/application-test.yml index b9b852d..8be6393 100644 --- a/im-platform/src/main/resources/application-test.yml +++ b/im-platform/src/main/resources/application-test.yml @@ -19,6 +19,7 @@ minio: imagePath: image filePath: file videoPath: video + expireIn: 180 # 文件过期时间,单位:天 webrtc: max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 diff --git a/im-platform/src/main/resources/static/favicon.ico b/im-platform/src/main/resources/static/favicon.ico index 8cd39d4..d31f507 100644 Binary files a/im-platform/src/main/resources/static/favicon.ico and b/im-platform/src/main/resources/static/favicon.ico differ diff --git a/im-uniapp/components/image-upload/image-upload.vue b/im-uniapp/components/image-upload/image-upload.vue index 6cfe9e5..7346bf0 100644 --- a/im-uniapp/components/image-upload/image-upload.vue +++ b/im-uniapp/components/image-upload/image-upload.vue @@ -29,6 +29,10 @@ export default { type: String, default: 'album' }, + isPermanent: { + type: Boolean, + default: false + }, onBefore: { type: Function, default: null @@ -61,7 +65,7 @@ export default { }, uploadImage(file) { uni.uploadFile({ - url: UNI_APP.BASE_URL + '/image/upload', + url: UNI_APP.BASE_URL + '/image/upload?isPermanent=' + this.isPermanent, header: { accessToken: uni.getStorageSync("loginInfo").accessToken }, diff --git a/im-uniapp/pages/group/group-edit.vue b/im-uniapp/pages/group/group-edit.vue index c1fd83f..eaae034 100644 --- a/im-uniapp/pages/group/group-edit.vue +++ b/im-uniapp/pages/group/group-edit.vue @@ -5,7 +5,7 @@ 群聊头像 - + 头像 - + diff --git a/im-web/src/components/common/FileUpload.vue b/im-web/src/components/common/FileUpload.vue index 1193ac2..f38284a 100644 --- a/im-web/src/components/common/FileUpload.vue +++ b/im-web/src/components/common/FileUpload.vue @@ -33,6 +33,10 @@ export default { type: Boolean, default: false }, + isPermanent: { + type: Boolean, + default: false + }, disabled: { type: Boolean, default: false @@ -52,7 +56,7 @@ export default { let formData = new FormData() formData.append('file', file.file) this.$http({ - url: this.action, + url: this.action + '?isPermanent=' + this.isPermanent, data: formData, method: 'post', headers: { diff --git a/im-web/src/components/setting/Setting.vue b/im-web/src/components/setting/Setting.vue index 6002227..8147be0 100644 --- a/im-web/src/components/setting/Setting.vue +++ b/im-web/src/components/setting/Setting.vue @@ -1,140 +1,141 @@ + \ No newline at end of file