From ed1c548c0016065294fb2907499bda4b18faa127 Mon Sep 17 00:00:00 2001 From: hhyykk Date: Thu, 11 Jul 2024 21:28:49 +0800 Subject: [PATCH] =?UTF-8?q?[feat]=20=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/infra/enums/DictTypeConstants.java | 2 + .../infra/enums/ErrorCodeConstants.java | 9 + .../admin/category/CategoryController.java | 94 ++++++++ .../admin/category/vo/CategoryListReqVO.java | 18 ++ .../admin/category/vo/CategoryRespVO.java | 33 +++ .../admin/category/vo/CategorySaveReqVO.java | 28 +++ .../controller/admin/file/FileController.java | 4 +- .../admin/file/vo/file/FileUploadReqVO.java | 6 + .../dal/dataobject/category/CategoryDO.java | 53 +++++ .../infra/dal/dataobject/file/FileDO.java | 5 + .../dal/mysql/category/CategoryMapper.java | 42 ++++ .../service/category/CategoryService.java | 74 ++++++ .../service/category/CategoryServiceImpl.java | 224 ++++++++++++++++++ .../infra/service/file/FileService.java | 8 +- .../infra/service/file/FileServiceImpl.java | 26 +- .../mapper/category/CategoryMapper.xml | 12 + .../category/CategoryServiceImplTest.java | 133 +++++++++++ 17 files changed, 768 insertions(+), 3 deletions(-) create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/CategoryController.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryListReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryRespVO.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategorySaveReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/category/CategoryDO.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/category/CategoryMapper.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryService.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImpl.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/category/CategoryMapper.xml create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImplTest.java diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java index 36ad63d56..4c85a58c3 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java @@ -17,4 +17,6 @@ public interface DictTypeConstants { String OPERATE_TYPE = "infra_operate_type"; // 操作类型 + String CATEGORY_TYPE = "infra_category_type"; // 目录分类类型 + } diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java index e9f39a81f..1f2e3109c 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -68,4 +68,13 @@ public interface ErrorCodeConstants { ErrorCode DEMO03_GRADE_NOT_EXISTS = new ErrorCode(1_001_201_008, "学生班级不存在"); ErrorCode DEMO03_GRADE_EXISTS = new ErrorCode(1_001_201_009, "学生班级已存在"); + // ========= 文件夹 1_001_008_000 + ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_001_008_001, "文件目录不存在"); + ErrorCode CATEGORY_EXITS_CHILDREN = new ErrorCode(1_001_008_002, "存在存在子文件目录,无法删除"); + ErrorCode CATEGORY_PARENT_NOT_EXITS = new ErrorCode(1_001_008_003,"父级文件目录不存在"); + ErrorCode CATEGORY_PARENT_ERROR = new ErrorCode(1_001_008_004, "不能设置自己为父文件目录"); + ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(1_001_008_005, "已经存在该文件夹名称的文件目录"); + ErrorCode CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_001_008_006, "不能设置自己的子Category为父Category"); + ErrorCode CATEGORY_EXISTS_FILES = new ErrorCode(1_001_008_007, "该目录下存在文件"); + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/CategoryController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/CategoryController.java new file mode 100644 index 000000000..3a96c1609 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/CategoryController.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.infra.controller.admin.category; + +import org.springframework.web.bind.annotation.*; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.security.access.prepost.PreAuthorize; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; + +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; +import cn.iocoder.yudao.module.infra.service.category.CategoryService; + +@Tag(name = "管理后台 - 文件目录") +@RestController +@RequestMapping("/infra/category") +@Validated +public class CategoryController { + + @Resource + private CategoryService categoryService; + + @PostMapping("/create") + @Operation(summary = "创建文件目录") + @PreAuthorize("@ss.hasPermission('infra:category:create')") + public CommonResult createCategory(@Valid @RequestBody CategorySaveReqVO createReqVO) { + return success(categoryService.createCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新文件目录") + @PreAuthorize("@ss.hasPermission('infra:category:update')") + public CommonResult updateCategory(@Valid @RequestBody CategorySaveReqVO updateReqVO) { + categoryService.updateCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件目录") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:category:delete')") + public CommonResult deleteCategory(@RequestParam("id") Long id) { + categoryService.deleteCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文件目录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:category:query')") + public CommonResult getCategory(@RequestParam("id") Long id) { + CategoryDO category = categoryService.getCategory(id); + return success(BeanUtils.toBean(category, CategoryRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得文件目录列表") + @PreAuthorize("@ss.hasPermission('infra:category:query')") + public CommonResult> getCategoryList(@Valid CategoryListReqVO listReqVO) { + List list = categoryService.getCategoryList(listReqVO); + return success(BeanUtils.toBean(list, CategoryRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出文件目录 Excel") + @PreAuthorize("@ss.hasPermission('infra:category:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportCategoryExcel(@Valid CategoryListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List list = categoryService.getCategoryList(listReqVO); + // 导出 Excel + ExcelUtils.write(response, "文件目录.xls", "数据", CategoryRespVO.class, + BeanUtils.toBean(list, CategoryRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryListReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryListReqVO.java new file mode 100644 index 000000000..47f66e48e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryListReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.infra.controller.admin.category.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - 文件目录列表 Request VO") +@Data +public class CategoryListReqVO { + + @Schema(description = "编号") + private String code; + + @Schema(description = "文件夹名称", example = "赵六") + private String name; + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryRespVO.java new file mode 100644 index 000000000..8969b6186 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategoryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import com.alibaba.excel.annotation.*; + +@Schema(description = "管理后台 - 文件目录 Response VO") +@Data +@ExcelIgnoreUnannotated +public class CategoryRespVO { + + @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("id") + private Long id; + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("编号") + private String code; + + @Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @ExcelProperty("文件夹名称") + private String name; + + @Schema(description = "父id", example = "3502") + @ExcelProperty("父id") + private Long parentId; + + @Schema(description = "描述", example = "你说的对") + @ExcelProperty("描述") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategorySaveReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategorySaveReqVO.java new file mode 100644 index 000000000..967afd921 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/category/vo/CategorySaveReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - 文件目录新增/修改 Request VO") +@Data +public class CategorySaveReqVO { + + @Schema(description = "主键", example = "19810") + private Long id; + + @Schema(description = "编号") + private String code; + + @Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "文件夹名称不能为空") + private String name; + + @Schema(description = "父id", example = "3502") + private Long parentId; + + @Schema(description = "描述", example = "你说的对") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index 8b983000b..527bccb97 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -50,7 +50,9 @@ public class FileController { public CommonResult uploadFileEx(FileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); String path = uploadReqVO.getPath(); - return success(fileService.createFileEx(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); + Long categoryId = uploadReqVO.getCategoryId(); + String categoryPath = uploadReqVO.getCategoryPath(); + return success(fileService.createFileEx(file.getOriginalFilename(), path, categoryId, categoryPath, IoUtil.readBytes(file.getInputStream()))); } @GetMapping("/presigned-url") diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java index 5d94cc7eb..5ac1ecf59 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -17,4 +17,10 @@ public class FileUploadReqVO { @Schema(description = "文件附件", example = "yudaoyuanma.png") private String path; + @Schema(description = "文件目录id") + private Long categoryId; + + @Schema(description = "文件夹路径") + private String categoryPath; + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/category/CategoryDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/category/CategoryDO.java new file mode 100644 index 000000000..fe93ecf8b --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/category/CategoryDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.category; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; + +/** + * 文件目录 DO + * + * @author 管理员 + */ +@TableName("infra_category") +@KeySequence("infra_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CategoryDO extends BaseDO { + + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 编号 + */ + private String code; + /** + * 文件夹名称 + */ + private String name; + /** + * 父id + */ + private Long parentId; + /** + * 描述 + */ + private String description; + /** + * 排序 + */ + private Integer sort; + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java index c0fb007e1..3ea44ed0a 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java @@ -52,4 +52,9 @@ public class FileDO extends BaseDO { */ private Integer size; + /** + * 目录id + */ + private Long categoryId; + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/category/CategoryMapper.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/category/CategoryMapper.java new file mode 100644 index 000000000..ee770a75d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/category/CategoryMapper.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.category; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.*; + +/** + * 文件目录 Mapper + * + * @author 管理员 + */ +@Mapper +public interface CategoryMapper extends BaseMapperX { + + default List selectList(CategoryListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(CategoryDO::getCode, reqVO.getCode()) + .likeIfPresent(CategoryDO::getName, reqVO.getName()) + .orderByDesc(CategoryDO::getId)); + } + + default CategoryDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(CategoryDO::getParentId, parentId, CategoryDO::getName, name); + } + + default CategoryDO selectByParentIdAndCode(Long parentId, String code) { + return selectOne(CategoryDO::getParentId, parentId, CategoryDO::getCode, code); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(CategoryDO::getParentId, parentId); + } + + default CategoryDO getCategoryByCode(String code) { + return selectOne(CategoryDO::getCode, code); + } +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryService.java new file mode 100644 index 000000000..8e0e08005 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryService.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.infra.service.category; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; + +/** + * 文件目录 Service 接口 + * + * @author 管理员 + */ +public interface CategoryService { + + /** + * 创建文件目录 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCategory(@Valid CategorySaveReqVO createReqVO); + + /** + * 更新文件目录 + * + * @param updateReqVO 更新信息 + */ + void updateCategory(@Valid CategorySaveReqVO updateReqVO); + + /** + * 删除文件目录 + * + * @param id 编号 + */ + void deleteCategory(Long id); + + /** + * 获得文件目录 + * + * @param id 编号 + * @return 文件目录 + */ + CategoryDO getCategory(Long id); + + /** + * 获得文件目录列表 + * + * @param listReqVO 查询条件 + * @return 文件目录列表 + */ + List getCategoryList(CategoryListReqVO listReqVO); + + /** + * 初始化根目录 + * + * @return 根目录 + */ + CategoryDO initRootCategory(); + + /** + * 根据 code 获取文件目录 + * @param code code + * @return 文件目录 + */ + CategoryDO getCategoryByCode(String code); + + /** + * 根据 path 创建文件目录 + * @param path 路径 eg 设计/桩基/钢筋 + * @param isCodeBased 是否基于编码创建, true-基于编码创建,名称从字典获取,false-基于名称创建 编码都为空 + * @return 最后一级目录的id + */ + Long createCategoryWithPath(String path, boolean isCodeBased); +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImpl.java new file mode 100644 index 000000000..56a978556 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImpl.java @@ -0,0 +1,224 @@ +package cn.iocoder.yudao.module.infra.service.category; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.CategoryListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.CategorySaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.dal.mysql.category.CategoryMapper; +import cn.iocoder.yudao.module.infra.service.file.FileService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 文件目录 Service 实现类 + * + * @author 管理员 + */ +@Service +@Validated +public class CategoryServiceImpl implements CategoryService { + + private static final String ROOT_CATEGORY_CODE = "root"; + + private static final String ROOT_CATEGORY_NAME = "根目录"; + + + @Resource + private CategoryMapper categoryMapper; + + @Resource + private FileService fileService; + + @Override + public Long createCategory(CategorySaveReqVO createReqVO) { + // 校验父id的有效性 + validateParentCategory(null, createReqVO.getParentId()); + // 校验文件夹名称的唯一性 + validateCategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); + + if (createReqVO.getParentId() == null) { + createReqVO.setParentId(0L); + } + + // 插入 + CategoryDO category = BeanUtils.toBean(createReqVO, CategoryDO.class); + categoryMapper.insert(category); + // 返回 + return category.getId(); + } + + @Override + public void updateCategory(CategorySaveReqVO updateReqVO) { + // 校验存在 + validateCategoryExists(updateReqVO.getId()); + // 校验父id的有效性 + validateParentCategory(updateReqVO.getId(), updateReqVO.getParentId()); + // 校验文件夹名称的唯一性 + validateCategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); + + // 更新 + CategoryDO updateObj = BeanUtils.toBean(updateReqVO, CategoryDO.class); + categoryMapper.updateById(updateObj); + } + + @Override + public void deleteCategory(Long id) { + // 校验存在 + validateCategoryExists(id); + // 校验是否有子文件目录 + if (categoryMapper.selectCountByParentId(id) > 0) { + throw exception(CATEGORY_EXITS_CHILDREN); + } + // 验证文件夹下是否有问题 + validateCategoryWithFiles(id); + // 删除 + categoryMapper.deleteById(id); + } + + private void validateCategoryExists(Long id) { + if (categoryMapper.selectById(id) == null) { + throw exception(CATEGORY_NOT_EXISTS); + } + } + + private void validateCategoryWithFiles(Long id) { + List fileList = fileService.getFileListWithCategoryId(id); + if (!fileList.isEmpty()) { + throw exception(CATEGORY_EXISTS_FILES); + } + } + + private void validateParentCategory(Long id, Long parentId) { + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + return; + } + // 1. 不能设置自己为父文件目录 + if (Objects.equals(id, parentId)) { + throw exception(CATEGORY_PARENT_ERROR); + } + // 2. 父文件目录不存在 + CategoryDO parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + throw exception(CATEGORY_PARENT_NOT_EXITS); + } + // 3. 递归校验父文件目录,如果父文件目录是自己的子文件目录,则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + parentId = parentCategory.getParentId(); + if (Objects.equals(id, parentId)) { + throw exception(CATEGORY_PARENT_IS_CHILD); + } + // 3.2 继续递归下一级父文件目录 + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + break; + } + } + } + + private void validateCategoryNameUnique(Long id, Long parentId, String name) { + CategoryDO category = categoryMapper.selectByParentIdAndName(parentId, name); + if (category == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的文件目录 + if (id == null) { + throw exception(CATEGORY_NAME_DUPLICATE); + } + if (!Objects.equals(category.getId(), id)) { + throw exception(CATEGORY_NAME_DUPLICATE); + } + } + + @Override + public CategoryDO getCategory(Long id) { + return categoryMapper.selectById(id); + } + + @Override + public List getCategoryList(CategoryListReqVO listReqVO) { + return categoryMapper.selectList(listReqVO); + } + + @Override + public CategoryDO initRootCategory() { + CategoryDO categoryDO = new CategoryDO(); + categoryDO.setCode(ROOT_CATEGORY_CODE); + categoryDO.setName(ROOT_CATEGORY_NAME); + categoryDO.setDescription(ROOT_CATEGORY_NAME); + categoryMapper.insert(categoryDO); + return getCategoryByCode(ROOT_CATEGORY_CODE); + } + + @Override + public CategoryDO getCategoryByCode(String code) { + return categoryMapper.getCategoryByCode(code); + } + + @Override + public Long createCategoryWithPath(String path, boolean isCodeBased) { + // 检查路径有效性 + if (path == null || path.trim().isEmpty() || path.equals("/")) { + throw new IllegalArgumentException("无效的分类路径"); + } + + // hyk: 如果要使用一个作为根目录,释放以下代码 +/* Long parentId = null; + // 校验根路径是否存在,不存在则创建根 + CategoryDO rootCategory = getCategoryByCode(ROOT_CATEGORY_CODE); + if (rootCategory == null) { + rootCategory = initRootCategory(); + parentId = rootCategory.getId(); + }*/ + + // 默认没有根路径第一级parent_id 为0 + Long parentId = 0L; + + // 解析路径 + String[] categoryElements = path.split("/"); + + // 根据路径创建文件夹,并返回最后一个文件夹的id + for (String categoryElement : categoryElements) { + // 获取父级目录 + parentId = createCategoryIfNeeded(parentId, categoryElement, isCodeBased); + } + return parentId; + } + + private Long createCategoryIfNeeded(Long parentId, String element, boolean isCodeBased) { + CategoryDO category; + if (isCodeBased) { + category = categoryMapper.selectByParentIdAndCode(parentId, element); + } else { + category = categoryMapper.selectByParentIdAndName(parentId, element); + } + if (category == null) { + CategoryDO newCategory = new CategoryDO(); + if (isCodeBased) { + newCategory.setCode(element); + } else { + newCategory.setName(element); + } + newCategory.setParentId(parentId); + categoryMapper.insert(newCategory); + return newCategory.getId(); + } else { + return category.getId(); + } + } + +} \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index 3ad7a09d7..cc47a96ab 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import java.util.List; + /** * 文件 Service 接口 * @@ -68,8 +70,12 @@ public interface FileService { * 创建文件ex * @param name 文件名 * @param path 路径 + * @param categoryId 分类id + * @param categoryPath 分类路径 * @param content 文件内容 * @return 文件对象 */ - FileRespVO createFileEx(String name, String path, byte[] content); + FileRespVO createFileEx(String name, String path, Long categoryId ,String categoryPath ,byte[] content); + + List getFileListWithCategoryId(Long categoryId); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 869ea6ef1..ef40de037 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.io.FileUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; @@ -14,10 +15,13 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; +import cn.iocoder.yudao.module.infra.service.category.CategoryService; import jakarta.annotation.Resource; import lombok.SneakyThrows; import org.springframework.stereotype.Service; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; @@ -32,6 +36,9 @@ public class FileServiceImpl implements FileService { @Resource private FileConfigService fileConfigService; + @Resource + private CategoryService categoryService; + @Resource private FileMapper fileMapper; @@ -116,7 +123,18 @@ public class FileServiceImpl implements FileService { @Override @SneakyThrows - public FileRespVO createFileEx(String name, String path, byte[] content) { + public FileRespVO createFileEx(String name, String path, Long categoryId, String categoryPah ,byte[] content) { + Long caId = null; + // 处理目录 + if (categoryId != null) { + CategoryDO category = categoryService.getCategory(categoryId); + if (category != null) { + caId = category.getId(); + } + } else if (categoryPah != null) { + caId = categoryService.createCategoryWithPath(categoryPah, false); + } + // 计算默认的 path 名 String type = FileTypeUtils.getMineType(content, name); if (StrUtil.isEmpty(path)) { @@ -139,6 +157,7 @@ public class FileServiceImpl implements FileService { file.setPath(path); file.setUrl(url); file.setType(type); + file.setCategoryId(caId); file.setSize(content.length); fileMapper.insert(file); FileRespVO vo = new FileRespVO(); @@ -147,4 +166,9 @@ public class FileServiceImpl implements FileService { return vo; } + @Override + public List getFileListWithCategoryId(Long categoryId) { + return fileMapper.selectList(FileDO::getCategoryId, categoryId); + } + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/category/CategoryMapper.xml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/category/CategoryMapper.xml new file mode 100644 index 000000000..662f61034 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/category/CategoryMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImplTest.java b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImplTest.java new file mode 100644 index 000000000..bb87e6824 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/category/CategoryServiceImplTest.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.module.infra.service.category; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +import jakarta.annotation.Resource; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; + +import cn.iocoder.yudao.module.infra.controller.admin.category.vo.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.category.CategoryDO; +import cn.iocoder.yudao.module.infra.dal.mysql.category.CategoryMapper; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link CategoryServiceImpl} 的单元测试类 + * + * @author 管理员 + */ +@Import(CategoryServiceImpl.class) +public class CategoryServiceImplTest extends BaseDbUnitTest { + + @Resource + private CategoryServiceImpl categoryService; + + @Resource + private CategoryMapper categoryMapper; + + @Test + public void testCreateCategory_success() { + // 准备参数 + CategorySaveReqVO createReqVO = randomPojo(CategorySaveReqVO.class).setId(null); + + // 调用 + Long categoryId = categoryService.createCategory(createReqVO); + // 断言 + assertNotNull(categoryId); + // 校验记录的属性是否正确 + CategoryDO category = categoryMapper.selectById(categoryId); + assertPojoEquals(createReqVO, category, "id"); + } + + @Test + public void testUpdateCategory_success() { + // mock 数据 + CategoryDO dbCategory = randomPojo(CategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + CategorySaveReqVO updateReqVO = randomPojo(CategorySaveReqVO.class, o -> { + o.setId(dbCategory.getId()); // 设置更新的 ID + }); + + // 调用 + categoryService.updateCategory(updateReqVO); + // 校验是否更新正确 + CategoryDO category = categoryMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, category); + } + + @Test + public void testUpdateCategory_notExists() { + // 准备参数 + CategorySaveReqVO updateReqVO = randomPojo(CategorySaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.updateCategory(updateReqVO), CATEGORY_NOT_EXISTS); + } + + @Test + public void testDeleteCategory_success() { + // mock 数据 + CategoryDO dbCategory = randomPojo(CategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCategory.getId(); + + // 调用 + categoryService.deleteCategory(id); + // 校验数据不存在了 + assertNull(categoryMapper.selectById(id)); + } + + @Test + public void testDeleteCategory_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetCategoryList() { + // mock 数据 + CategoryDO dbCategory = randomPojo(CategoryDO.class, o -> { // 等会查询到 + o.setCode(null); + o.setName(null); + }); + categoryMapper.insert(dbCategory); + // 测试 code 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCode(null))); + // 测试 name 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null))); + // 准备参数 + CategoryListReqVO reqVO = new CategoryListReqVO(); + reqVO.setCode(null); + reqVO.setName(null); + + // 调用 + List list = categoryService.getCategoryList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbCategory, list.get(0)); + } + +} \ No newline at end of file