|
@@ -0,0 +1,168 @@
|
|
|
|
|
+package com.lingyue.parse.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.lingyue.common.exception.ServiceException;
|
|
|
|
|
+import com.lingyue.parse.config.FileStorageProperties;
|
|
|
|
|
+import com.lingyue.parse.dto.FileUploadResponse;
|
|
|
|
|
+import com.lingyue.parse.enums.FileType;
|
|
|
|
|
+import com.lingyue.parse.enums.ParseStatus;
|
|
|
|
|
+import com.lingyue.parse.util.FileTypeDetector;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.nio.file.Paths;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 文件上传服务
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author lingyue
|
|
|
|
|
+ * @since 2026-01-14
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class FileUploadService {
|
|
|
|
|
+
|
|
|
|
|
+ private final FileStorageProperties fileStorageProperties;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param file 上传的文件
|
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
|
+ * @return 上传响应
|
|
|
|
|
+ */
|
|
|
|
|
+ public FileUploadResponse uploadFile(MultipartFile file, String userId) {
|
|
|
|
|
+ // 1. 验证文件
|
|
|
|
|
+ validateFile(file);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 检测文件类型
|
|
|
|
|
+ FileType fileType = FileTypeDetector.detectFileType(file);
|
|
|
|
|
+ log.info("检测到文件类型: {}", fileType);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 生成文档ID
|
|
|
|
|
+ String documentId = UUID.randomUUID().toString();
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 构建存储路径
|
|
|
|
|
+ String filePath = buildFilePath(userId, documentId, file.getOriginalFilename());
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 保存文件
|
|
|
|
|
+ try {
|
|
|
|
|
+ saveFile(file, filePath);
|
|
|
|
|
+ log.info("文件保存成功: {}", filePath);
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ log.error("文件保存失败", e);
|
|
|
|
|
+ throw new ServiceException("文件保存失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 构建响应
|
|
|
|
|
+ return FileUploadResponse.builder()
|
|
|
|
|
+ .documentId(documentId)
|
|
|
|
|
+ .fileName(file.getOriginalFilename())
|
|
|
|
|
+ .fileType(fileType.name())
|
|
|
|
|
+ .fileSize(file.getSize())
|
|
|
|
|
+ .filePath(filePath)
|
|
|
|
|
+ .parseStatus(ParseStatus.PENDING.getCode())
|
|
|
|
|
+ .uploadTime(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证文件
|
|
|
|
|
+ */
|
|
|
|
|
+ private void validateFile(MultipartFile file) {
|
|
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
|
|
+ throw new ServiceException("文件不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
|
|
+ if (originalFilename == null || originalFilename.isEmpty()) {
|
|
|
|
|
+ throw new ServiceException("文件名不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证文件扩展名
|
|
|
|
|
+ if (!FileTypeDetector.isAllowedExtension(originalFilename,
|
|
|
|
|
+ fileStorageProperties.getAllowedExtensions())) {
|
|
|
|
|
+ throw new ServiceException("不支持的文件类型,允许的类型: " +
|
|
|
|
|
+ fileStorageProperties.getAllowedExtensions());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证文件大小(500MB)
|
|
|
|
|
+ long maxSize = 500 * 1024 * 1024L;
|
|
|
|
|
+ if (file.getSize() > maxSize) {
|
|
|
|
|
+ throw new ServiceException("文件大小超过限制(最大500MB)");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建文件存储路径
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
|
+ * @param documentId 文档ID
|
|
|
|
|
+ * @param originalFilename 原始文件名
|
|
|
|
|
+ * @return 文件路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildFilePath(String userId, String documentId, String originalFilename) {
|
|
|
|
|
+ // 获取文件扩展名
|
|
|
|
|
+ String extension = "";
|
|
|
|
|
+ if (originalFilename.contains(".")) {
|
|
|
|
|
+ extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建路径: /data/lingyue/files/{userId}/{documentId}/original{extension}
|
|
|
|
|
+ return Paths.get(
|
|
|
|
|
+ fileStorageProperties.getBasePath(),
|
|
|
|
|
+ userId,
|
|
|
|
|
+ documentId,
|
|
|
|
|
+ "original" + extension
|
|
|
|
|
+ ).toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存文件到磁盘
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveFile(MultipartFile file, String filePath) throws IOException {
|
|
|
|
|
+ Path path = Paths.get(filePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建目录
|
|
|
|
|
+ Files.createDirectories(path.getParent());
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件
|
|
|
|
|
+ file.transferTo(path.toFile());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param filePath 文件路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public void deleteFile(String filePath) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ if (file.exists()) {
|
|
|
|
|
+ if (file.delete()) {
|
|
|
|
|
+ log.info("文件删除成功: {}", filePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("文件删除失败: {}", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("删除文件异常", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查文件是否存在
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean fileExists(String filePath) {
|
|
|
|
|
+ return new File(filePath).exists();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|