From e7efe7784535a77a21347c0ca142056c16a94902 Mon Sep 17 00:00:00 2001
From: chenhj <1263187585@qq.com>
Date: 星期四, 30 四月 2026 16:52:10 +0800
Subject: [PATCH] 增加定时任务,一个月清除一次无用文件
---
src/main/resources/mapper/basic/StorageBlobMapper.xml | 61 +++++++++---
src/main/resources/mapper/basic/StorageAttachmentMapper.xml | 8 -
src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java | 6 +
src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java | 184 ++++++++++++++++++++++++++++++++++++
4 files changed, 237 insertions(+), 22 deletions(-)
diff --git a/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java b/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
index 756b4b9..5f84cb7 100644
--- a/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
+++ b/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.basic.pojo.StorageBlob;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
/**
* <p>
@@ -15,4 +16,9 @@
@Mapper
public interface StorageBlobMapper extends BaseMapper<StorageBlob> {
+ java.util.List<StorageBlob> selectOrphanBlobsByIdRange(@Param("lastId") long lastId, @Param("limit") int limit);
+
+ int deleteByIdList(@Param("ids") java.util.List<Long> ids);
+
+ java.util.List<String> selectExistingUidFilenames(@Param("fileNames") java.util.List<String> fileNames);
}
diff --git a/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java b/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java
new file mode 100644
index 0000000..d5ac456
--- /dev/null
+++ b/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java
@@ -0,0 +1,184 @@
+package com.ruoyi.basic.task;
+
+import com.ruoyi.basic.mapper.StorageBlobMapper;
+import com.ruoyi.basic.pojo.StorageBlob;
+import com.ruoyi.common.config.FileProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 娓呯悊鏃犳晥鏂囦欢瀹氭椂浠诲姟銆�
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class StorageBlobCleanupTask {
+
+ private static final int DB_BATCH_SIZE = 500;
+ private static final int FILE_NAME_BATCH_SIZE = 1000;
+
+ private final StorageBlobMapper storageBlobMapper;
+ private final FileProperties fileProperties;
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+
+ /**
+ * 姣忔湀 1 鍙峰噷鏅� 2 鐐规墽琛屼竴娆★細
+ * 1. 鍒犻櫎 storage_blob 涓湭琚� storage_attachment 鍏宠仈鐨勮褰曞強鍏舵枃浠�
+ * 2. 鍒犻櫎纾佺洏涓婁笉瀛樺湪浜� storage_blob.uid_filename 鐨勬枃浠�
+ */
+ @Scheduled(cron = "0 0 2 1 * ?")
+ public void cleanupUnusedStorageFiles() {
+ if (!running.compareAndSet(false, true)) {
+ log.warn("鏂囦欢娓呯悊浠诲姟姝e湪鎵ц锛屾湰娆¤烦杩�");
+ return;
+ }
+
+ long start = System.currentTimeMillis();
+ log.info("鏂囦欢娓呯悊浠诲姟寮�濮嬫墽琛岋紝鏍圭洰褰曪細{}", fileProperties.getPath());
+ try {
+ int removedBlobCount = cleanupOrphanStorageBlobs();
+ int removedDiskFileCount = cleanupOrphanDiskFiles();
+ long cost = System.currentTimeMillis() - start;
+ log.info("鏂囦欢娓呯悊浠诲姟鎵ц瀹屾垚锛屽垹闄ゅ鍎� blob 璁板綍锛歿}锛屽垹闄ょ鐩樻棤鏁堟枃浠讹細{}锛岃�楁椂锛歿} ms",
+ removedBlobCount, removedDiskFileCount, cost);
+ } catch (Exception e) {
+ log.error("鏂囦欢娓呯悊浠诲姟鎵ц澶辫触", e);
+ } finally {
+ running.set(false);
+ }
+ }
+
+ private int cleanupOrphanStorageBlobs() {
+ long lastId = 0L;
+ int removedCount = 0;
+
+ while (true) {
+ List<StorageBlob> orphanBlobs = storageBlobMapper.selectOrphanBlobsByIdRange(lastId, DB_BATCH_SIZE);
+ if (CollectionUtils.isEmpty(orphanBlobs)) {
+ break;
+ }
+
+ List<Long> ids = new ArrayList<>(orphanBlobs.size());
+ for (StorageBlob storageBlob : orphanBlobs) {
+ ids.add(storageBlob.getId());
+ deleteBlobFiles(storageBlob);
+ }
+ storageBlobMapper.deleteByIdList(ids);
+ removedCount += ids.size();
+ lastId = orphanBlobs.get(orphanBlobs.size() - 1).getId();
+
+ log.info("宸插垹闄や竴鎵瑰鍎� blob锛宐atchSize={}锛宭astId={}", ids.size(), lastId);
+ }
+
+ return removedCount;
+ }
+
+ private int cleanupOrphanDiskFiles() {
+ File rootDirectory = new File(fileProperties.getPath());
+ if (!rootDirectory.exists() || !rootDirectory.isDirectory()) {
+ log.warn("鏂囦欢鏍圭洰褰曚笉瀛樺湪鎴栦笉鏄洰褰曪紝璺宠繃纾佺洏娓呯悊锛歿}", fileProperties.getPath());
+ return 0;
+ }
+
+ int deletedCount = 0;
+ Deque<File> directories = new ArrayDeque<>();
+ directories.push(rootDirectory);
+
+ while (!directories.isEmpty()) {
+ File currentDirectory = directories.pop();
+ File[] children = currentDirectory.listFiles();
+ if (children == null || children.length == 0) {
+ continue;
+ }
+
+ List<File> filesInDirectory = new ArrayList<>();
+ for (File child : children) {
+ if (child.isDirectory()) {
+ directories.push(child);
+ } else if (child.isFile()) {
+ filesInDirectory.add(child);
+ }
+ }
+
+ deletedCount += cleanupFilesInDirectory(filesInDirectory);
+ }
+
+ return deletedCount;
+ }
+
+ private int cleanupFilesInDirectory(List<File> filesInDirectory) {
+ if (CollectionUtils.isEmpty(filesInDirectory)) {
+ return 0;
+ }
+
+ int deletedCount = 0;
+ for (int start = 0; start < filesInDirectory.size(); start += FILE_NAME_BATCH_SIZE) {
+ int end = Math.min(start + FILE_NAME_BATCH_SIZE, filesInDirectory.size());
+ List<File> batchFiles = filesInDirectory.subList(start, end);
+ List<String> fileNames = new ArrayList<>(batchFiles.size());
+ for (File file : batchFiles) {
+ fileNames.add(file.getName());
+ }
+
+ Set<String> existingFileNames = new HashSet<>(storageBlobMapper.selectExistingUidFilenames(fileNames));
+ for (File file : batchFiles) {
+ if (!existingFileNames.contains(file.getName()) && safeDelete(file)) {
+ deletedCount++;
+ }
+ }
+ }
+ return deletedCount;
+ }
+
+ private void deleteBlobFiles(StorageBlob storageBlob) {
+ File originalFile = resolveBlobFile(storageBlob);
+ safeDelete(originalFile);
+
+ File compressedFile = resolveCompressedFile(originalFile);
+ safeDelete(compressedFile);
+ }
+
+ private File resolveBlobFile(StorageBlob storageBlob) {
+ String basePath = fileProperties.getPath();
+ if (!StringUtils.hasText(storageBlob.getPath())) {
+ return new File(basePath, storageBlob.getUidFilename());
+ }
+ return new File(new File(basePath, storageBlob.getPath()), storageBlob.getUidFilename());
+ }
+
+ private File resolveCompressedFile(File originalFile) {
+ if (originalFile == null) {
+ return null;
+ }
+ File parent = originalFile.getParentFile();
+ if (parent == null) {
+ return null;
+ }
+ return new File(parent, "thumb_" + originalFile.getName());
+ }
+
+ private boolean safeDelete(File file) {
+ if (file == null || !file.exists() || !file.isFile()) {
+ return false;
+ }
+ if (file.delete()) {
+ return true;
+ }
+ log.warn("鍒犻櫎鏂囦欢澶辫触锛歿}", file.getAbsolutePath());
+ return false;
+ }
+}
diff --git a/src/main/resources/mapper/basic/StorageAttachmentMapper.xml b/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
index a2cc6cf..d2b7b92 100644
--- a/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
+++ b/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
@@ -10,13 +10,9 @@
<result column="deleted" property="deleted" />
<result column="record_type" property="recordType" />
<result column="record_id" property="recordId" />
- <result column="name" property="name" />
+ <result column="application" property="application" />
<result column="storage_blob_id" property="storageBlobId" />
</resultMap>
- <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
- <sql id="Base_Column_List">
- id, create_time, update_time, deleted, record_type, record_id, name, storage_blob_id
- </sql>
-</mapper>
\ No newline at end of file
+</mapper>
diff --git a/src/main/resources/mapper/basic/StorageBlobMapper.xml b/src/main/resources/mapper/basic/StorageBlobMapper.xml
index 84e3b00..d8a03fa 100644
--- a/src/main/resources/mapper/basic/StorageBlobMapper.xml
+++ b/src/main/resources/mapper/basic/StorageBlobMapper.xml
@@ -2,21 +2,50 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.basic.mapper.StorageBlobMapper">
- <!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
- <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
- <id column="id" property="id" />
- <result column="create_time" property="createTime" />
- <result column="key" property="key" />
- <result column="content_type" property="contentType" />
- <result column="original_filename" property="originalFilename" />
- <result column="bucket_filename" property="bucketFilename" />
- <result column="bucket_name" property="bucketName" />
- <result column="byte_size" property="byteSize" />
- </resultMap>
+ <!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
+ <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
+ <id column="id" property="id"/>
+ <result column="resource_key" property="resourceKey"/>
+ <result column="content_type" property="contentType"/>
+ <result column="original_filename" property="originalFilename"/>
+ <result column="uid_filename" property="uidFilename"/>
+ <result column="byte_size" property="byteSize"/>
+ <result column="path" property="path"/>
+ </resultMap>
- <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
- <sql id="Base_Column_List">
- id, create_time, key, content_type, original_filename,bucket_filename,bucket_name, byte_size
- </sql>
+ <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
+ <sql id="Base_Column_List">
+ id, resource_key, content_type, original_filename, uid_filename, byte_size, path
+ </sql>
-</mapper>
\ No newline at end of file
+ <select id="selectOrphanBlobsByIdRange" resultMap="BaseResultMap">
+ SELECT
+ <include refid="Base_Column_List"/>
+ FROM storage_blob sb
+ LEFT JOIN storage_attachment sa
+ ON sa.storage_blob_id = sb.id
+ AND sa.deleted = 0
+ WHERE sb.id <![CDATA[>]]> #{lastId}
+ AND sa.id IS NULL
+ ORDER BY sb.id ASC
+ LIMIT #{limit}
+ </select>
+
+ <delete id="deleteByIdList">
+ DELETE FROM storage_blob
+ WHERE id IN
+ <foreach collection="ids" item="id" open="(" separator="," close=")">
+ #{id}
+ </foreach>
+ </delete>
+
+ <select id="selectExistingUidFilenames" resultType="java.lang.String">
+ SELECT uid_filename
+ FROM storage_blob
+ WHERE uid_filename IN
+ <foreach collection="fileNames" item="fileName" open="(" separator="," close=")">
+ #{fileName}
+ </foreach>
+ </select>
+
+</mapper>
--
Gitblit v1.9.3