Files
senmesh-zc/src/main/java/com/senmesh/sfile/service/impl/UserFileServiceImpl.java

447 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<UserFileDTO> filesToCopy = this.list(queryDTO);
List<Long> excludeFileIds = expandCreateDTO.getExcludeFileIds();
// 4. 在文件夹下创建文件对象(跳过需排除的文件,如安全检测未通过确认跳过分享)
List<UserFileDTO> 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<UserFileDTO> 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<UserFileDTO> 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<Long> ids, Long userId) {
Assert.notEmpty(ids, "文件ID列表不能为空");
Assert.notNull(userId, "用户ID不能为空");
// 循环删除每个文件
for (Long id : ids) {
remove(id, userId);
}
}
@Override
public List<UserFileDTO> 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<UserFileDTO> 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<UserFileStatisticsDTO> 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<UserFileRecordDTO> records = userFileMapper.listRecordByPhone(queryDTO);
fillRecordType(records);
}
/**
* 记录类型 -> root_type 条件
* 上传记录固定为普通(1);发送/接收方向与本端部署位置有关:
* 内网侧发送为内穿外(2)、接收为外传内(3);外网侧相反
*
* @param recordType 记录类型,为空查全部
*/
private Set<String> 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<UserFileRecordDTO> 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());
}
}
}
}