package com.senmesh.sfile.service.impl; import com.senmesh.common.constant.CommonConstants; import com.senmesh.common.exception.util.Assert; import com.senmesh.common.util.ListUtil; import com.senmesh.common.util.UserContextHolder; import com.senmesh.component.file.service.FileService; import com.senmesh.component.file.util.FileUtils; import com.senmesh.sfile.constant.FileStatusEnum; import com.senmesh.sfile.constant.FileTypeEnum; import com.senmesh.sfile.constant.RootTypeEnum; import com.senmesh.sfile.convert.UserFileConvert; import com.senmesh.sfile.dao.UserFileMapper; import com.senmesh.sfile.dto.ExpandUserFileCreateDTO; import com.senmesh.sfile.dto.ExpandUserFileDTO; import com.senmesh.sfile.dto.UserFileCreateDTO; import com.senmesh.sfile.dto.UserFileDTO; import com.senmesh.sfile.dto.UserFileModifyDTO; import com.senmesh.sfile.dto.UserFileModifyStatusDTO; import com.senmesh.sfile.dto.UserFileQueryDTO; import com.senmesh.sfile.dto.UserFileStatisticsDTO; import com.senmesh.sfile.entity.UserFile; import com.senmesh.sfile.service.UserFileService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; /** * 用户文件ServiceImpl */ @Slf4j @Service public class UserFileServiceImpl implements UserFileService { @Autowired private UserFileMapper userFileMapper; @Autowired private FileService fileService; @Override @Transactional public UserFileDTO create(UserFileCreateDTO createDTO) { Long parentId = createDTO.getParentId(); String parentPath = createDTO.getParentPath(); Long userId = createDTO.getUserId(); String rootType = createDTO.getRootType(); // 1. 校验 userId 和 rootType 不能为空 Assert.notNull(userId, "用户ID不能为空"); Assert.notBlank(rootType, "根类型不能为空"); // 2. 校验文件类型时fileCode不能为空 if (FileTypeEnum.isFile(createDTO.getFileType())) { Assert.notBlank(createDTO.getFileCode(), "文件编码不能为空"); } // 3. 处理 parentPath(性能优化:如果提供了 parentPath,直接使用,避免查询) if (StringUtils.isBlank(parentPath) && parentId != null && parentId != 0L) { // 如果没有提供 parentPath,但提供了 parentId,需要查询父节点获取 parentPath UserFileDTO parentFile = this.findOne(UserFileQueryDTO.builder() .id(parentId) .userId(userId) .rootType(rootType) .build()); Assert.isTrue(FileTypeEnum.isFolder(parentFile.getFileType()), "父节点必须是文件夹"); // 获取父路径用于后续拼接 parentPath = parentFile.getPath(); } // 如果提供了 parentPath,直接信任调用方,不做校验 // 4. 插入数据获取ID UserFile userFile = UserFileConvert.INSTANCE.toEntity(createDTO); userFileMapper.saveEntity(userFile); Long newId = userFile.getId(); // 5. 构建path String path = StringUtils.isNotBlank(parentPath) ? parentPath + CommonConstants.COMMA + newId : String.valueOf(newId); // 6. 更新path userFile.setPath(path); userFileMapper.updateEntitySelective(userFile); // 7. 转换为DTO返回 return UserFileConvert.INSTANCE.toDto(userFile); } @Override @Transactional public ExpandUserFileDTO create(ExpandUserFileCreateDTO expandCreateDTO) { Long userFileId = expandCreateDTO.getUserFileId(); Long userId = expandCreateDTO.getUserId(); String rootType = expandCreateDTO.getRootType(); // 1. 查询普通文件(限制rootType为普通) UserFileDTO normalFile = this.findOne(UserFileQueryDTO.builder() .id(userFileId) .userId(userId) .rootType(RootTypeEnum.NORMAL.getType()) .build()); // 2. 创建文件夹,文件名 = 原文件名 + 当前时间 String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); String baseFileName = FileTypeEnum.isFile(normalFile.getFileType()) ? FileUtils.getFileNameNoSuffix(normalFile.getFileName()) : normalFile.getFileName(); String folderName = baseFileName + "_" + timestamp; UserFileCreateDTO folderCreateDTO = UserFileCreateDTO.builder() .userId(userId) .rootType(rootType) .fileStatus(expandCreateDTO.getFileStatus()) .fileType(FileTypeEnum.FOLDER.getType()) .fileName(folderName) .parentId(0L) .endTime(expandCreateDTO.getEndTime()) .accessCode(expandCreateDTO.getAccessCode()) .build(); UserFileDTO folderDTO = this.create(folderCreateDTO); Long folderId = folderDTO.getId(); String folderPath = String.valueOf(folderId); // 3. 查询需要复制的文件列表 UserFileQueryDTO queryDTO = UserFileQueryDTO.builder() .userId(userId) .rootType(RootTypeEnum.NORMAL.getType()) .fileType(FileTypeEnum.FILE.getType()) .pathLike(normalFile.getPath()) .build(); queryDTO.disablePage(); List filesToCopy = this.list(queryDTO); // 4. 在文件夹下创建文件对象 List createdFiles = new java.util.ArrayList<>(); for (UserFileDTO fileDTO : filesToCopy) { UserFileCreateDTO fileCreateDTO = UserFileCreateDTO.builder() .userId(userId) .rootType(rootType) .fileStatus(expandCreateDTO.getFileStatus()) .fileType(FileTypeEnum.FILE.getType()) .fileName(fileDTO.getFileName()) .fileCode(fileDTO.getFileCode()) .parentId(folderId) .parentPath(folderPath) .build(); UserFileDTO createdFile = this.create(fileCreateDTO); createdFiles.add(createdFile); } // 6. 返回结果 return ExpandUserFileDTO.builder() .folder(folderDTO) .files(createdFiles) .build(); } @Override @Transactional public ExpandUserFileDTO create(UserFileDTO folder, List files) { Long userId = folder.getUserId(); String rootType = folder.getRootType(); // 3. 创建文件夹 UserFileCreateDTO folderCreateDTO = UserFileCreateDTO.builder() .userId(userId) .rootType(rootType) .fileType(FileTypeEnum.FOLDER.getType()) .fileName(folder.getFileName()) .parentId(0L) .fileStatus(folder.getFileStatus()) .build(); UserFileDTO folderDTO = this.create(folderCreateDTO); Long folderId = folderDTO.getId(); String folderPath = String.valueOf(folderId); log.info("创建文件夹成功: folderId={}, 文件名={}", folderId, folder.getFileName()); // 4. 创建文件列表 List createdFiles = new ArrayList<>(); for (UserFileDTO fileInfo : files) { // 创建文件记录 UserFileCreateDTO fileCreateDTO = UserFileCreateDTO.builder() .userId(userId) .rootType(rootType) .fileType(FileTypeEnum.FILE.getType()) .fileName(fileInfo.getFileName()) .fileCode(fileInfo.getFileCode()) .parentId(folderId) .parentPath(folderPath) .fileStatus(folder.getFileStatus()) .build(); UserFileDTO createdFile = this.create(fileCreateDTO); createdFiles.add(createdFile); } // 5. 返回结果 return ExpandUserFileDTO.builder() .folder(folderDTO) .files(createdFiles) .build(); } @Override @Transactional public void modify(UserFileModifyDTO modifyDTO) { Long id = modifyDTO.getId(); Long userId = modifyDTO.getUserId(); Long newParentId = modifyDTO.getParentId(); String rootType = modifyDTO.getRootType(); // 1. 根据id+userId+rootType查询文件是否存在 UserFileDTO existFile = this.findOne(UserFileQueryDTO.builder() .id(id) .userId(userId) .rootType(rootType) .build()); // 2. 判断是否需要更新path(parentId有值且与现存的parentId不一致) boolean needUpdatePath = newParentId != null && !newParentId.equals(existFile.getParentId()); if (needUpdatePath) { // 2.1 校验新父节点(如果不是根目录) if (newParentId != 0L) { UserFileDTO newParentFile = this.findOne(UserFileQueryDTO.builder() .id(newParentId) .userId(userId) .rootType(rootType) .build()); Assert.isTrue(FileTypeEnum.isFolder(newParentFile.getFileType()), "父节点必须是文件夹"); // 校验新父节点的rootType必须与当前文件相同 Assert.isTrue(existFile.getRootType().equals(newParentFile.getRootType()), "不能将文件移动到不同类型的文件夹"); } // 2.2 在SQL中一次性更新当前节点和所有子孙节点的path(SQL内部会限制相同userId和rootType) userFileMapper.updatePathByParentIdChange(id, newParentId); } // 3. 更新文件信息 UserFile userFile = UserFileConvert.INSTANCE.toEntity(modifyDTO); userFileMapper.updateEntitySelective(userFile); } @Override @Transactional public void modifyStatus(UserFileModifyStatusDTO modifyStatusDTO) { Long id = modifyStatusDTO.getId(); String oldStatus = modifyStatusDTO.getOldStatus(); String newStatus = modifyStatusDTO.getNewStatus(); Long operateUserId = UserContextHolder.getLoginUserId(); // 更新文件状态及其所有子文件状态(SQL内部会校验oldStatus并限制相同userId和rootType) userFileMapper.updateStatusWithChildren(id, oldStatus, newStatus, operateUserId); } @Override @Transactional public void modifyStatus(UserFileModifyStatusDTO modifyStatusDTO, Long userId) { Assert.notNull(userId, "用户ID不能为空"); Long id = modifyStatusDTO.getId(); // 校验文件是否存在且属于该用户 this.findOne(UserFileQueryDTO.builder() .id(id) .userId(userId) .build()); // 调用原方法修改状态 modifyStatus(modifyStatusDTO); } @Override @Transactional public void remove(Long id) { // 删除文件及其所有子文件(SQL内部会限制相同userId和rootType) userFileMapper.deleteFileWithChildren(id, UserContextHolder.getLoginUserId()); } @Override @Transactional public void remove(Long id, Long userId) { Assert.notNull(id, "文件ID不能为空"); Assert.notNull(userId, "用户ID不能为空"); // 校验文件是否存在且属于该用户 this.findOne(UserFileQueryDTO.builder() .id(id) .userId(userId) .build()); // 删除文件及其所有子文件(SQL内部会限制相同userId和rootType) remove(id); } @Override public List list(UserFileQueryDTO queryDTO) { queryDTO.disablePage(); return userFileMapper.list(queryDTO); } @Override public void page(UserFileQueryDTO queryDTO) { userFileMapper.list(queryDTO); } @Override public UserFileDTO find(UserFileQueryDTO queryDTO) { List userFileDTOs = this.list(queryDTO); return ListUtil.first(userFileDTOs); } @Override public UserFileDTO findOne(UserFileQueryDTO queryDTO) { UserFileDTO userFileDTO = this.find(queryDTO); Assert.notNull(userFileDTO, "文件不存在"); return userFileDTO; } @Override public List statisticsByCategory(UserFileQueryDTO queryDTO) { queryDTO.disablePage(); return userFileMapper.statisticsByCategory(queryDTO); } }