diff --git a/continew-server/src/main/resources/db/changelog/mysql/main_data.sql b/continew-server/src/main/resources/db/changelog/mysql/main_data.sql index 70ad9d5c..47d73545 100644 --- a/continew-server/src/main/resources/db/changelog/mysql/main_data.sql +++ b/continew-server/src/main/resources/db/changelog/mysql/main_data.sql @@ -61,6 +61,10 @@ VALUES (1116, '下载', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()), (1117, '创建文件夹', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:createDir', 7, 1, 1, NOW()), (1118, '计算文件夹大小', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:calcDirSize', 8, 1, 1, NOW()), +(1119, '回收站文件列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:list', 9, 1, 1, NOW()), +(1120, '还原回收站文件', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:restore', 10, 1, 1, NOW()), +(1121, '删除回收站文件', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:delete', 11, 1, 1, NOW()), +(1122, '清空回收站', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:clean', 12, 1, 1, NOW()), (1130, '字典管理', 1000, 2, '/system/dict', 'SystemDict', 'system/dict/index', NULL, 'bookmark', b'0', b'0', b'0', NULL, 7, 1, 1, NOW()), (1131, '列表', 1130, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:dict:list', 1, 1, 1, NOW()), @@ -278,10 +282,10 @@ INSERT INTO `sys_role_dept` (`role_id`, `dept_id`) VALUES (547888897925840927, 5 -- 初始化默认存储 INSERT INTO `sys_storage` -(`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`) +(`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `recycle_bin_enabled`, `recycle_bin_path`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`) VALUES -(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', '本地存储', b'1', 1, 1, 1, NOW()), -(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', '本地存储', b'0', 2, 2, 1, NOW()); +(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', b'1', '.RECYCLE.BIN/', '本地存储', b'1', 1, 1, 1, NOW()), +(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', b'1', '.RECYCLE.BIN/', '本地存储', b'0', 2, 2, 1, NOW()); -- 初始化客户端数据 INSERT INTO `sys_client` diff --git a/continew-server/src/main/resources/db/changelog/mysql/main_table.sql b/continew-server/src/main/resources/db/changelog/mysql/main_table.sql index c18b7bcc..4e6df59e 100644 --- a/continew-server/src/main/resources/db/changelog/mysql/main_table.sql +++ b/continew-server/src/main/resources/db/changelog/mysql/main_table.sql @@ -283,24 +283,26 @@ CREATE TABLE IF NOT EXISTS `sys_notice_log` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告日志表'; CREATE TABLE IF NOT EXISTS `sys_storage` ( - `id` bigint(20) AUTO_INCREMENT COMMENT 'ID', - `name` varchar(100) NOT NULL COMMENT '名称', - `code` varchar(30) NOT NULL COMMENT '编码', - `type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:本地存储;2:对象存储)', - `access_key` varchar(255) DEFAULT NULL COMMENT 'Access Key', - `secret_key` varchar(255) DEFAULT NULL COMMENT 'Secret Key', - `endpoint` varchar(255) DEFAULT NULL COMMENT 'Endpoint', - `bucket_name` varchar(255) NOT NULL COMMENT 'Bucket', - `domain` varchar(255) DEFAULT NULL COMMENT '域名', - `description` varchar(200) DEFAULT NULL COMMENT '描述', - `is_default` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为默认存储', - `sort` int NOT NULL DEFAULT 999 COMMENT '排序', - `status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态(1:启用;2:禁用)', - `create_user` bigint(20) NOT NULL COMMENT '创建人', - `create_time` datetime NOT NULL COMMENT '创建时间', - `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', - `update_time` datetime DEFAULT NULL COMMENT '修改时间', - `deleted` bigint(20) NOT NULL DEFAULT 0 COMMENT '是否已删除(0:否;id:是)', + `id` bigint(20) AUTO_INCREMENT COMMENT 'ID', + `name` varchar(100) NOT NULL COMMENT '名称', + `code` varchar(30) NOT NULL COMMENT '编码', + `type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:本地存储;2:对象存储)', + `access_key` varchar(255) DEFAULT NULL COMMENT 'Access Key', + `secret_key` varchar(255) DEFAULT NULL COMMENT 'Secret Key', + `endpoint` varchar(255) DEFAULT NULL COMMENT 'Endpoint', + `bucket_name` varchar(255) NOT NULL COMMENT 'Bucket', + `domain` varchar(255) DEFAULT NULL COMMENT '域名', + `recycle_bin_enabled` bit(1) NOT NULL DEFAULT b'1' COMMENT '启用回收站', + `recycle_bin_path` varchar(255) DEFAULT NULL COMMENT '回收站路径', + `description` varchar(200) DEFAULT NULL COMMENT '描述', + `is_default` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为默认存储', + `sort` int NOT NULL DEFAULT 999 COMMENT '排序', + `status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态(1:启用;2:禁用)', + `create_user` bigint(20) NOT NULL COMMENT '创建人', + `create_time` datetime NOT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `deleted` bigint(20) NOT NULL DEFAULT 0 COMMENT '是否已删除(0:否;id:是)', PRIMARY KEY (`id`), UNIQUE INDEX `uk_code`(`code`, `deleted`), INDEX `idx_create_user`(`create_user`), diff --git a/continew-server/src/main/resources/db/changelog/postgresql/main_data.sql b/continew-server/src/main/resources/db/changelog/postgresql/main_data.sql index d83635f3..c76a64ad 100644 --- a/continew-server/src/main/resources/db/changelog/postgresql/main_data.sql +++ b/continew-server/src/main/resources/db/changelog/postgresql/main_data.sql @@ -61,6 +61,10 @@ VALUES (1116, '下载', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()), (1117, '创建文件夹', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:createDir', 7, 1, 1, NOW()), (1118, '计算文件夹大小', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:calcDirSize', 8, 1, 1, NOW()), +(1119, '回收站文件列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:list', 9, 1, 1, NOW()), +(1120, '还原回收站文件', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:restore', 10, 1, 1, NOW()), +(1121, '删除回收站文件', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:delete', 11, 1, 1, NOW()), +(1122, '清空回收站', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:fileRecycle:clean', 12, 1, 1, NOW()), (1130, '字典管理', 1000, 2, '/system/dict', 'SystemDict', 'system/dict/index', NULL, 'bookmark', false, false, false, NULL, 7, 1, 1, NOW()), (1131, '列表', 1130, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:dict:list', 1, 1, 1, NOW()), @@ -278,10 +282,10 @@ INSERT INTO "sys_role_dept" ("role_id", "dept_id") VALUES (547888897925840927, 5 -- 初始化默认存储 INSERT INTO "sys_storage" -("id", "name", "code", "type", "access_key", "secret_key", "endpoint", "bucket_name", "domain", "description", "is_default", "sort", "status", "create_user", "create_time") +("id", "name", "code", "type", "access_key", "secret_key", "endpoint", "bucket_name", "domain", "recycle_bin_enabled", "recycle_bin_path", "description", "is_default", "sort", "status", "create_user", "create_time") VALUES -(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', '本地存储', true, 1, 1, 1, NOW()), -(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', '本地存储', false, 2, 2, 1, NOW()); +(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', true, '.RECYCLE.BIN/', '本地存储', true, 1, 1, 1, NOW()), +(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', true, '.RECYCLE.BIN/', '本地存储', false, 2, 2, 1, NOW()); -- 初始化客户端数据 INSERT INTO "sys_client" diff --git a/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql b/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql index 3666da51..7ecc9e2f 100644 --- a/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql +++ b/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql @@ -466,49 +466,53 @@ COMMENT ON COLUMN "sys_notice_log"."read_time" IS '读取时间'; COMMENT ON TABLE "sys_notice_log" IS '公告日志表'; CREATE TABLE IF NOT EXISTS "sys_storage" ( - "id" int8 NOT NULL, - "name" varchar(100) NOT NULL, - "code" varchar(30) NOT NULL, - "type" int2 NOT NULL DEFAULT 1, - "access_key" varchar(255) DEFAULT NULL, - "secret_key" varchar(255) DEFAULT NULL, - "endpoint" varchar(255) DEFAULT NULL, - "bucket_name" varchar(255) NOT NULL, - "domain" varchar(255) DEFAULT NULL, - "description" varchar(200) DEFAULT NULL, - "is_default" bool NOT NULL DEFAULT false, - "sort" int4 NOT NULL DEFAULT 999, - "status" int2 NOT NULL DEFAULT 1, - "create_user" int8 NOT NULL, - "create_time" timestamp NOT NULL, - "update_user" int8 DEFAULT NULL, - "update_time" timestamp DEFAULT NULL, - "deleted" int8 NOT NULL DEFAULT 0, + "id" int8 NOT NULL, + "name" varchar(100) NOT NULL, + "code" varchar(30) NOT NULL, + "type" int2 NOT NULL DEFAULT 1, + "access_key" varchar(255) DEFAULT NULL, + "secret_key" varchar(255) DEFAULT NULL, + "endpoint" varchar(255) DEFAULT NULL, + "bucket_name" varchar(255) NOT NULL, + "domain" varchar(255) DEFAULT NULL, + "recycle_bin_enabled" bool NOT NULL DEFAULT true, + "recycle_bin_path" varchar(255) DEFAULT NULL, + "description" varchar(200) DEFAULT NULL, + "is_default" bool NOT NULL DEFAULT false, + "sort" int4 NOT NULL DEFAULT 999, + "status" int2 NOT NULL DEFAULT 1, + "create_user" int8 NOT NULL, + "create_time" timestamp NOT NULL, + "update_user" int8 DEFAULT NULL, + "update_time" timestamp DEFAULT NULL, + "deleted" int8 NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); CREATE UNIQUE INDEX "uk_storage_code" ON "sys_storage" ("code", "deleted"); CREATE INDEX "idx_storage_create_user" ON "sys_storage" ("create_user"); CREATE INDEX "idx_storage_update_user" ON "sys_storage" ("update_user"); CREATE INDEX "idx_storage_deleted" ON "sys_storage" ("deleted"); -COMMENT ON COLUMN "sys_storage"."id" IS 'ID'; -COMMENT ON COLUMN "sys_storage"."name" IS '名称'; -COMMENT ON COLUMN "sys_storage"."code" IS '编码'; -COMMENT ON COLUMN "sys_storage"."type" IS '类型(1:本地存储;2:对象存储)'; -COMMENT ON COLUMN "sys_storage"."access_key" IS 'Access Key'; -COMMENT ON COLUMN "sys_storage"."secret_key" IS 'Secret Key'; -COMMENT ON COLUMN "sys_storage"."endpoint" IS 'Endpoint'; -COMMENT ON COLUMN "sys_storage"."bucket_name" IS 'Bucket'; -COMMENT ON COLUMN "sys_storage"."domain" IS '域名'; -COMMENT ON COLUMN "sys_storage"."description" IS '描述'; -COMMENT ON COLUMN "sys_storage"."is_default" IS '是否为默认存储'; -COMMENT ON COLUMN "sys_storage"."sort" IS '排序'; -COMMENT ON COLUMN "sys_storage"."status" IS '状态(1:启用;2:禁用)'; -COMMENT ON COLUMN "sys_storage"."create_user" IS '创建人'; -COMMENT ON COLUMN "sys_storage"."create_time" IS '创建时间'; -COMMENT ON COLUMN "sys_storage"."update_user" IS '修改人'; -COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间'; -COMMENT ON COLUMN "sys_storage"."deleted" IS '是否已删除(0:否;id:是)'; -COMMENT ON TABLE "sys_storage" IS '存储表'; +COMMENT ON COLUMN "sys_storage"."id" IS 'ID'; +COMMENT ON COLUMN "sys_storage"."name" IS '名称'; +COMMENT ON COLUMN "sys_storage"."code" IS '编码'; +COMMENT ON COLUMN "sys_storage"."type" IS '类型(1:本地存储;2:对象存储)'; +COMMENT ON COLUMN "sys_storage"."access_key" IS 'Access Key'; +COMMENT ON COLUMN "sys_storage"."secret_key" IS 'Secret Key'; +COMMENT ON COLUMN "sys_storage"."endpoint" IS 'Endpoint'; +COMMENT ON COLUMN "sys_storage"."bucket_name" IS 'Bucket'; +COMMENT ON COLUMN "sys_storage"."domain" IS '域名'; +COMMENT ON COLUMN "sys_storage"."recycle_bin_enabled" IS '启用回收站'; +COMMENT ON COLUMN "sys_storage"."recycle_bin_path" IS '回收站路径'; +COMMENT ON COLUMN "sys_storage"."description" IS '描述'; +COMMENT ON COLUMN "sys_storage"."is_default" IS '是否为默认存储'; +COMMENT ON COLUMN "sys_storage"."sort" IS '排序'; +COMMENT ON COLUMN "sys_storage"."status" IS '状态(1:启用;2:禁用)'; +COMMENT ON COLUMN "sys_storage"."create_user" IS '创建人'; +COMMENT ON COLUMN "sys_storage"."create_time" IS '创建时间'; +COMMENT ON COLUMN "sys_storage"."update_user" IS '修改人'; +COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间'; +COMMENT ON COLUMN "sys_storage"."deleted" IS '是否已删除(0:否;id:是)'; +COMMENT ON TABLE "sys_storage" IS '存储表'; CREATE TABLE IF NOT EXISTS "sys_file" ( "id" int8 NOT NULL, diff --git a/continew-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java b/continew-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java index fb6727f5..4da0c27b 100644 --- a/continew-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java +++ b/continew-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java @@ -37,6 +37,7 @@ import top.continew.starter.core.util.URLUtils; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -55,6 +56,9 @@ public class FileRecorderImpl implements FileRecorder { @Override public boolean save(FileInfo fileInfo) { + if (fileInfo.getAttr() == null) { + return true; + } // 保存文件信息 FileDO file = new FileDO(fileInfo); StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false)); @@ -87,7 +91,7 @@ public class FileRecorderImpl implements FileRecorder { public boolean delete(String url) { FileDO file = this.getFileByUrl(url); if (file == null) { - return false; + return true; } return fileMapper.lambdaUpdate().eq(FileDO::getId, file.getId()).remove(); } @@ -131,7 +135,7 @@ public class FileRecorderImpl implements FileRecorder { // 结合存储配置进行匹配 List storageList = storageMapper.selectByIds(CollUtils.mapToList(list, FileDO::getStorageId)); Map storageMap = storageList.stream() - .collect(Collectors.toMap(StorageDO::getId, storage -> storage)); + .collect(Collectors.toMap(StorageDO::getId, Function.identity(), (existing, replacement) -> existing)); return list.stream().filter(file -> { // http://localhost:8000/file/user/avatar/6825e687db4174e7a297a5f8.png => http://localhost:8000/file/user/avatar String urlPrefix = StrUtil.subBefore(url, StringConstants.SLASH, true); diff --git a/continew-system/src/main/java/top/continew/admin/system/controller/FileRecycleController.java b/continew-system/src/main/java/top/continew/admin/system/controller/FileRecycleController.java new file mode 100644 index 00000000..74648205 --- /dev/null +++ b/continew-system/src/main/java/top/continew/admin/system/controller/FileRecycleController.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.admin.system.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import top.continew.admin.system.model.query.FileQuery; +import top.continew.admin.system.model.resp.file.FileResp; +import top.continew.admin.system.service.FileRecycleService; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +/** + * 文件回收站管理 API + * + * @author Charles7c + * @since 2025/11/11 21:28 + */ +@Tag(name = "文件回收站管理 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/file/recycle") +public class FileRecycleController { + + private final FileRecycleService baseService; + + @Operation(summary = "分页查询列表", description = "分页查询列表") + @SaCheckPermission("system:fileRecycle:list") + @GetMapping + public PageResp page(@Valid FileQuery query, @Valid PageQuery pageQuery) { + return baseService.page(query, pageQuery); + } + + @Operation(summary = "还原文件", description = "还原文件") + @SaCheckPermission("system:fileRecycle:restore") + @PutMapping("/restore/{id}") + public void restore(@PathVariable Long id) { + baseService.restore(id); + } + + @Operation(summary = "删除文件", description = "删除文件") + @SaCheckPermission("system:fileRecycle:delete") + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + baseService.delete(id); + } + + @Operation(summary = "清空回收站", description = "清空回收站") + @SaCheckPermission("system:fileRecycle:clean") + @DeleteMapping("/clean") + public void clean() { + baseService.clean(); + } +} diff --git a/continew-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java b/continew-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java index e5bd6420..064e500d 100644 --- a/continew-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java +++ b/continew-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java @@ -89,5 +89,10 @@ public enum StorageTypeEnum implements BaseEnum { if (StrUtil.isNotBlank(req.getDomain())) { req.setDomain(StrUtil.appendIfMissing(req.getDomain(), StringConstants.SLASH)); } + // 回收站路径需要以 “/” 结尾 + if (Boolean.TRUE.equals(req.getRecycleBinEnabled())) { + req.setRecycleBinPath(StrUtil.appendIfMissing(StrUtil.removePrefix(req + .getRecycleBinPath(), StringConstants.SLASH), StringConstants.SLASH)); + } } } diff --git a/continew-system/src/main/java/top/continew/admin/system/mapper/FileMapper.java b/continew-system/src/main/java/top/continew/admin/system/mapper/FileMapper.java index 8b8b454c..f9651942 100644 --- a/continew-system/src/main/java/top/continew/admin/system/mapper/FileMapper.java +++ b/continew-system/src/main/java/top/continew/admin/system/mapper/FileMapper.java @@ -16,8 +16,14 @@ package top.continew.admin.system.mapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import top.continew.admin.system.model.entity.FileDO; import top.continew.admin.system.model.resp.file.FileStatisticsResp; import top.continew.starter.data.mapper.BaseMapper; @@ -40,4 +46,57 @@ public interface FileMapper extends BaseMapper { */ @Select("SELECT type, COUNT(1) number, SUM(size) size FROM sys_file WHERE deleted = 0 AND type != 0 GROUP BY type") List statistics(); + + /** + * 分页查询回收站列表 + * + * @param page 分页条件 + * @param queryWrapper 查询条件 + * @return 回收站分页列表信息 + */ + @Select("SELECT * FROM sys_file ${ew.customSqlSegment}") + Page selectPageInRecycleBin(@Param("page") IPage page, + @Param(Constants.WRAPPER) LambdaQueryWrapper queryWrapper); + + /** + * 根据 ID 查询(文件已进入回收站) + * + * @param id ID + * @return 文件信息 + */ + @Select("SELECT * FROM sys_file WHERE id = #{id} AND deleted = 1") + FileDO selectByIdInRecycleBin(@Param("id") Long id); + + /** + * 查询回收站文件列表 + * + * @return 回收站文件列表 + */ + @Select("SELECT * FROM sys_file WHERE deleted = 1") + List selectListInRecycleBin(); + + /** + * 从回收站恢复文件 + * + * @param id ID + * @param userId 用户 ID + */ + @Update("UPDATE sys_file SET deleted = 0, update_user = #{userId}, update_time = NOW() WHERE id = #{id}") + void restoreInRecycleBin(@Param("id") Long id, @Param("userId") Long userId); + + /** + * 删除文件(不进入回收站) + * + * @param ids ID 列表 + * @param userId 用户 ID + */ + void deleteWithoutRecycleBin(@Param("ids") List ids, @Param("userId") Long userId); + + /** + * 清空文件回收站 + * + * @param userId 用户 ID + */ + @Update("UPDATE sys_file SET deleted = id, update_user = #{userId}, update_time = NOW() WHERE deleted = 1") + void cleanRecycleBin(@Param("userId") Long userId); } \ No newline at end of file diff --git a/continew-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java b/continew-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java index b94e39fe..c60814c7 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java @@ -19,6 +19,7 @@ package top.continew.admin.system.model.entity; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.NoArgsConstructor; @@ -114,6 +115,12 @@ public class FileDO extends BaseDO { */ private Long storageId; + /** + * 是否已删除(0:否;1:回收站) + */ + @TableLogic(value = "0", delval = "1") + private Long deleted; + /** * 基于 {@link FileInfo} 构建文件信息对象 * @@ -143,7 +150,7 @@ public class FileDO extends BaseDO { /** * 转换为 {@link FileInfo} 文件信息对象 * - * @param storage 存储配置信息 + * @param storage 存储配置 * @return {@link FileInfo} 文件信息对象 */ public FileInfo toFileInfo(StorageDO storage) { diff --git a/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java b/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java index 370a1a82..ef950984 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java @@ -22,8 +22,8 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.base.model.entity.BaseDO; +import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.system.enums.StorageTypeEnum; import top.continew.starter.core.constant.StringConstants; import top.continew.starter.encrypt.field.annotation.FieldEncrypt; @@ -86,6 +86,16 @@ public class StorageDO extends BaseDO { */ private String domain; + /** + * 启用回收站 + */ + private Boolean recycleBinEnabled; + + /** + * 回收站路径 + */ + private String recycleBinPath; + /** * 描述 */ diff --git a/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java b/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java index 702db61b..56a42830 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java @@ -16,6 +16,8 @@ package top.continew.admin.system.model.req; +import cn.sticki.spel.validator.constrain.SpelNotBlank; +import cn.sticki.spel.validator.jakarta.SpelValid; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -38,6 +40,7 @@ import java.io.Serializable; * @since 2023/12/26 22:09 */ @Data +@SpelValid @Schema(description = "存储创建或修改请求参数") public class StorageReq implements Serializable { @@ -109,6 +112,20 @@ public class StorageReq implements Serializable { @NotBlank(message = "访问路径不能为空", groups = ValidationGroup.Storage.Local.class) private String domain; + /** + * 启用回收站 + */ + @Schema(description = "启用回收站", example = "true") + @NotNull(message = "启用回收站无效") + private Boolean recycleBinEnabled; + + /** + * 回收站路径 + */ + @Schema(description = "回收站路径", example = ".RECYCLE.BIN/") + @SpelNotBlank(condition = "#this.recycleBinEnabled == true", message = "回收站路径不能为空") + private String recycleBinPath; + /** * 排序 */ diff --git a/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java b/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java index 6bce6139..0cfa93a3 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java @@ -85,6 +85,18 @@ public class StorageResp extends BaseDetailResp { @Schema(description = "域名", example = "http://localhost:8000/file") private String domain; + /** + * 启用回收站 + */ + @Schema(description = "启用回收站", example = "true") + private Boolean recycleBinEnabled; + + /** + * 回收站路径 + */ + @Schema(description = "回收站路径", example = ".RECYCLE.BIN/") + private String recycleBinPath; + /** * 描述 */ diff --git a/continew-system/src/main/java/top/continew/admin/system/service/FileRecycleService.java b/continew-system/src/main/java/top/continew/admin/system/service/FileRecycleService.java new file mode 100644 index 00000000..79ea7872 --- /dev/null +++ b/continew-system/src/main/java/top/continew/admin/system/service/FileRecycleService.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.admin.system.service; + +import jakarta.validation.Valid; +import top.continew.admin.system.model.query.FileQuery; +import top.continew.admin.system.model.resp.file.FileResp; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +/** + * 文件回收站业务接口 + * + * @author Charles7c + * @since 2025/11/11 21:28 + */ +public interface FileRecycleService { + + /** + * 分页查询列表 + * + * @param query 查询参数 + * @param pageQuery 分页参数 + * @return 文件列表 + */ + PageResp page(@Valid FileQuery query, @Valid PageQuery pageQuery); + + /** + * 还原文件 + * + * @param id ID + */ + void restore(Long id); + + /** + * 删除文件 + * + * @param id ID + */ + void delete(Long id); + + /** + * 清空回收站 + */ + void clean(); +} diff --git a/continew-system/src/main/java/top/continew/admin/system/service/impl/FileRecycleServiceImpl.java b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileRecycleServiceImpl.java new file mode 100644 index 00000000..1cbe8049 --- /dev/null +++ b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileRecycleServiceImpl.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.admin.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.continew.admin.common.context.UserContextHolder; +import top.continew.admin.system.mapper.FileMapper; +import top.continew.admin.system.model.entity.FileDO; +import top.continew.admin.system.model.entity.StorageDO; +import top.continew.admin.system.model.query.FileQuery; +import top.continew.admin.system.model.resp.file.FileResp; +import top.continew.admin.system.service.FileRecycleService; +import top.continew.admin.system.service.StorageService; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.data.util.QueryWrapperHelper; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 文件回收站业务实现 + * + * @author Charles7c + * @since 2025/11/11 21:28 + */ +@Service +@RequiredArgsConstructor +public class FileRecycleServiceImpl implements FileRecycleService { + + private final FileMapper fileMapper; + private final StorageService storageService; + private final FileStorageService fileStorageService; + + @Override + public PageResp page(FileQuery query, PageQuery pageQuery) { + QueryWrapper queryWrapper = QueryWrapperHelper.build(query, pageQuery.getSort()); + Page page = fileMapper.selectPageInRecycleBin(new Page<>(pageQuery.getPage(), pageQuery + .getSize()), queryWrapper.lambda().eq(FileDO::getDeleted, 1L)); + return PageResp.build(page, FileResp.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restore(Long id) { + FileDO file = this.getById(id); + // 恢复记录 + fileMapper.restoreInRecycleBin(id, UserContextHolder.getUserId()); + // 还原文件 + StorageDO storage = storageService.getById(file.getStorageId()); + FileInfo fileInfo = file.toFileInfo(storage); + fileInfo.setPath(storage.getRecycleBinPath() + fileInfo.getPath()); + String newPath = fileInfo.getPath().replace(storage.getRecycleBinPath(), StringConstants.EMPTY); + fileStorageService.move(fileInfo).setPath(newPath).move(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + FileDO file = this.getById(id); + // 删除记录 + fileMapper.deleteWithoutRecycleBin(List.of(id), UserContextHolder.getUserId()); + // 删除文件 + StorageDO storage = storageService.getById(file.getStorageId()); + FileInfo fileInfo = file.toFileInfo(storage); + fileInfo.setPath(storage.getRecycleBinPath() + fileInfo.getPath()); + fileStorageService.delete(fileInfo); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void clean() { + // 查询文件 + List list = fileMapper.selectListInRecycleBin(); + // 删除记录 + fileMapper.cleanRecycleBin(UserContextHolder.getUserId()); + // 删除文件 + // 批量获取存储配置 + Map> fileListGroup = list.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); + List storageList = storageService.listByIds(fileListGroup.keySet()); + Map storageGroup = storageList.stream() + .collect(Collectors.toMap(StorageDO::getId, Function.identity(), (existing, replacement) -> existing)); + // 删除文件 + for (Map.Entry> entry : fileListGroup.entrySet()) { + StorageDO storage = storageGroup.get(entry.getKey()); + // 清空回收站 + FileInfo fileInfo = new FileInfo(); + fileInfo.setPlatform(storage.getCode()); + fileInfo.setBasePath(StringConstants.EMPTY); + fileInfo.setPath(storage.getRecycleBinPath()); + fileStorageService.delete(fileInfo); + } + } + + /** + * 根据 ID 查询 + * + * @param id ID + * @return 文件信息 + */ + private FileDO getById(Long id) { + FileDO file = fileMapper.selectByIdInRecycleBin(id); + if (file == null) { + throw new BusinessException("ID 为 [%s] 的文件已不存在".formatted(id)); + } + return file; + } +} diff --git a/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java index 9c9a15da..510e336b 100644 --- a/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java +++ b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java @@ -30,8 +30,10 @@ import org.dromara.x.file.storage.core.ProgressListener; import org.dromara.x.file.storage.core.upload.UploadPretreatment; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import top.continew.admin.common.base.service.BaseServiceImpl; +import top.continew.admin.common.context.UserContextHolder; import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.system.enums.FileTypeEnum; import top.continew.admin.system.mapper.FileMapper; @@ -45,6 +47,7 @@ import top.continew.admin.system.service.FileService; import top.continew.admin.system.service.StorageService; import top.continew.starter.cache.redisson.util.RedisLockUtils; import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.CollUtils; import top.continew.starter.core.util.StrUtils; import top.continew.starter.core.util.validation.CheckUtils; import top.continew.starter.core.util.validation.ValidationUtils; @@ -52,6 +55,7 @@ import top.continew.starter.core.util.validation.ValidationUtils; import java.io.File; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -71,25 +75,32 @@ public class FileServiceImpl extends BaseServiceImpl ids) { + @Transactional(rollbackFor = Exception.class) + public void delete(List ids) { List fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); + if (CollUtil.isEmpty(fileList)) { + return; + } + // 批量获取存储配置 Map> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); + List storageList = storageService.listByIds(fileListGroup.keySet()); + Map storageGroup = storageList.stream() + .collect(Collectors.toMap(StorageDO::getId, Function.identity(), (existing, replacement) -> existing)); + // 删除记录 for (Map.Entry> entry : fileListGroup.entrySet()) { - StorageDO storage = storageService.getById(entry.getKey()); - for (FileDO file : entry.getValue()) { - if (!FileTypeEnum.DIR.equals(file.getType())) { - FileInfo fileInfo = file.toFileInfo(storage); - fileStorageService.delete(fileInfo); - } else { - // 不允许删除非空文件夹 - boolean exists = baseMapper.lambdaQuery() - .eq(FileDO::getParentPath, file.getPath()) - .eq(FileDO::getStorageId, entry.getKey()) - .exists(); - CheckUtils.throwIf(exists, "文件夹 [{}] 不为空,请先删除文件夹下的内容", file.getName()); - } + StorageDO storage = storageGroup.get(entry.getKey()); + List idList = CollUtils.mapToList(entry.getValue(), FileDO::getId); + if (Boolean.TRUE.equals(storage.getRecycleBinEnabled())) { + baseMapper.deleteByIds(idList); + } else { + baseMapper.deleteWithoutRecycleBin(idList, UserContextHolder.getUserId()); } } + // 删除实际文件 + for (Map.Entry> entry : fileListGroup.entrySet()) { + StorageDO storage = storageGroup.get(entry.getKey()); + entry.getValue().forEach(file -> this.deleteFile(file, storage)); + } } @Override @@ -322,4 +333,32 @@ public class FileServiceImpl extends BaseServiceImpl + + + + UPDATE sys_file + SET deleted = id, update_user = #{userId}, update_time = NOW() + WHERE id IN + + #{item} + + + \ No newline at end of file