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.config.FileProperties; import com.senmesh.component.file.service.FileService; import com.senmesh.component.file.util.FileUtils; import com.senmesh.sfile.constant.FileLocationEnum; import com.senmesh.sfile.constant.FileRecordTypeEnum; 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.UserFileRecordDTO; import com.senmesh.sfile.dto.UserFileRecordQueryDTO; import com.senmesh.sfile.dto.UserFileStatisticsDTO; import com.senmesh.sfile.entity.UserFile; import com.senmesh.sfile.service.UserFileService; import com.google.common.collect.Sets; 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.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; /** * 用户文件ServiceImpl */ @Slf4j @Service public class UserFileServiceImpl implements UserFileService { @Autowired private UserFileMapper userFileMapper; @Autowired private FileService fileService; @Autowired private FileProperties fileProperties; @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); List excludeFileIds = expandCreateDTO.getExcludeFileIds(); // 4. 在文件夹下创建文件对象(跳过需排除的文件,如安全检测未通过确认跳过分享) List createdFiles = new java.util.ArrayList<>(); for (UserFileDTO fileDTO : filesToCopy) { if (excludeFileIds != null && excludeFileIds.contains(fileDTO.getId())) { log.info("复制用户文件时跳过排除文件: id={}, fileName={}", fileDTO.getId(), fileDTO.getFileName()); continue; } 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, modifyDTO.getOperateUserId()); } // 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 @Transactional public void removeByIds(List ids, Long userId) { Assert.notEmpty(ids, "文件ID列表不能为空"); Assert.notNull(userId, "用户ID不能为空"); // 循环删除每个文件 for (Long id : ids) { remove(id, userId); } } @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); } @Override public void pageRecordByPhone(UserFileRecordQueryDTO queryDTO) { Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限"); Assert.notBlank(queryDTO.getPhone(), "手机号不能为空"); queryDTO.setPhone(queryDTO.getPhone().trim()); if (queryDTO.getRecordType() != null) { Assert.notNull(FileRecordTypeEnum.getByType(queryDTO.getRecordType()), "记录类型不合法,仅支持1上传/2发送/3接收"); } queryDTO.setRootTypeIn(resolveRootTypes(queryDTO.getRecordType())); List records = userFileMapper.listRecordByPhone(queryDTO); fillRecordType(records); } /** * 记录类型 -> root_type 条件 * 上传记录固定为普通(1);发送/接收方向与本端部署位置有关: * 内网侧发送为内穿外(2)、接收为外传内(3);外网侧相反 * * @param recordType 记录类型,为空查全部 */ private Set resolveRootTypes(Integer recordType) { if (recordType == null) { return Sets.newHashSet(RootTypeEnum.NORMAL.getType(), getSendRootType(), getReceiveRootType()); } switch (FileRecordTypeEnum.getByType(recordType)) { case UPLOAD: return Collections.singleton(RootTypeEnum.NORMAL.getType()); case SEND: return Collections.singleton(getSendRootType()); case RECEIVE: return Collections.singleton(getReceiveRootType()); default: return resolveRootTypes(null); } } /** * 本端发送方向:内网侧为内穿外(2),外网侧为外传内(3),与TransferUserFileServiceImpl保持一致 */ private String getSendRootType() { return FileLocationEnum.isInternal(fileProperties.getTransfer().getLocation()) ? RootTypeEnum.INTERNAL_TO_EXTERNAL.getType() : RootTypeEnum.EXTERNAL_TO_INTERNAL.getType(); } /** * 本端接收方向:内网侧为外传内(3),外网侧为内穿外(2),与TransferFileMonitorServiceImpl保持一致 */ private String getReceiveRootType() { return FileLocationEnum.isInternal(fileProperties.getTransfer().getLocation()) ? RootTypeEnum.EXTERNAL_TO_INTERNAL.getType() : RootTypeEnum.INTERNAL_TO_EXTERNAL.getType(); } /** * 按本端部署位置回填记录类型与描述 */ private void fillRecordType(List records) { if (records == null || records.isEmpty()) { return; } for (UserFileRecordDTO record : records) { String rootType = record.getRootType(); if (RootTypeEnum.NORMAL.getType().equals(rootType)) { record.setRecordType(FileRecordTypeEnum.UPLOAD.getType()); } else if (getSendRootType().equals(rootType)) { record.setRecordType(FileRecordTypeEnum.SEND.getType()); } else if (getReceiveRootType().equals(rootType)) { record.setRecordType(FileRecordTypeEnum.RECEIVE.getType()); } if (record.getRecordType() != null) { record.setRecordTypeDesc(FileRecordTypeEnum.getByType(record.getRecordType()).getDesc()); } else { RootTypeEnum rootTypeEnum = RootTypeEnum.getByType(rootType); record.setRecordTypeDesc(rootTypeEnum == null ? rootType : rootTypeEnum.getDesc()); } } } }