[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-spring-boot-shixianwenjianmiaochuangongnengcongyuanlidaoshijian":3},{"id":4,"title":5,"slug":6,"summary":7,"content":8,"contentHtml":9,"wordCount":10,"readingTime":11,"categoryId":12,"tags":13,"coverImage":13,"thumbnail":13,"status":14,"isTop":15,"isRecommended":15,"allowComments":15,"password":13,"viewCount":16,"likeCount":15,"commentCount":15,"seoTitle":5,"seoKeywords":17,"seoDescription":18,"source":13,"sourceUrl":13,"publishTime":19},"2002405842165493761","Spring Boot 实现文件秒传功能：从原理到实践","spring-boot-shixianwenjianmiaochuangongnengcongyuanlidaoshijian","## 1. 文件秒传技术原理 ### 1.1 什么是文件秒传 文件秒传是指当用户上传一个文件时，如果服务器上已经存在相同的文件，则无需再次上传文件内容，直接返回上传成功。这种技术可以显著减少网络传输量，提升用户体验。 ### 1.2 实现原理 秒传的核心原理是**文件内容指纹比对**： 1. **客户端计算文件哈希值**：在上传前，客户端计算文件的哈希值（如MD5、SHA-1等） 2. **查询服务器文件状态**：将哈希值发送到服务器查询是否已存在 3. **服务器验证**：服务器检查哈希值对应的文件是否存在 4. **决定上传策略**： - 文件已存在：直接返回成功（秒传） - 文件不存在：执行正常上传流程 ### 1.3 哈希算法选择 | 算法 | 安全性 | 速度 | 适用场景 | |------|--------|------|----------| | MD5 | 较低 | 快....","## 1. 文件秒传技术原理\n\n### 1.1 什么是文件秒传\n文件秒传是指当用户上传一个文件时，如果服务器上已经存在相同的文件，则无需再次上传文件内容，直接返回上传成功。这种技术可以显著减少网络传输量，提升用户体验。\n\n### 1.2 实现原理\n秒传的核心原理是**文件内容指纹比对**：\n\n1. **客户端计算文件哈希值**：在上传前，客户端计算文件的哈希值（如MD5、SHA-1等）\n2. **查询服务器文件状态**：将哈希值发送到服务器查询是否已存在\n3. **服务器验证**：服务器检查哈希值对应的文件是否存在\n4. **决定上传策略**：\n   - 文件已存在：直接返回成功（秒传）\n   - 文件不存在：执行正常上传流程\n\n### 1.3 哈希算法选择\n| 算法 | 安全性 | 速度 | 适用场景 |\n|------|--------|------|----------|\n| MD5 | 较低 | 快 | 一般文件校验 |\n| SHA-1 | 中等 | 较快 | 普通安全需求 |\n| SHA-256 | 高 | 较慢 | 高安全需求 |\n\n## 2. 系统设计与实现\n\n### 2.1 数据库设计\n```sql\nCREATE TABLE file_metadata (\n    id BIGINT AUTO_INCREMENT PRIMARY KEY,\n    file_hash VARCHAR(64) NOT NULL UNIQUE COMMENT '文件哈希值',\n    file_name VARCHAR(255) NOT NULL COMMENT '原始文件名',\n    file_size BIGINT NOT NULL COMMENT '文件大小',\n    file_path VARCHAR(500) NOT NULL COMMENT '存储路径',\n    upload_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',\n    upload_count INT DEFAULT 1 COMMENT '上传次数',\n    INDEX idx_hash (file_hash)\n);\n```\n\n### 2.2 项目结构\n```\nsrc\u002Fmain\u002Fjava\u002Fcom\u002Fexample\u002Fupload\u002F\n├── config\u002F\n│   └── FileStorageConfig.java\n├── controller\u002F\n│   └── FileUploadController.java\n├── entity\u002F\n│   └── FileMetadata.java\n├── service\u002F\n│   ├── FileStorageService.java\n│   └── HashService.java\n├── repository\u002F\n│   └── FileMetadataRepository.java\n└── dto\u002F\n    ├── UploadRequest.java\n    └── UploadResponse.java\n```\n\n## 3. 核心代码实现\n\n### 3.1 配置文件存储\n```java\n@Configuration\npublic class FileStorageConfig {\n    \n    @Value(\"${file.upload-dir:.\u002Fuploads}\")\n    private String uploadDir;\n    \n    @Bean\n    public Path fileStorageLocation() {\n        Path path = Paths.get(uploadDir).toAbsolutePath().normalize();\n        try {\n            Files.createDirectories(path);\n            return path;\n        } catch (IOException e) {\n            throw new RuntimeException(\"无法创建文件存储目录\", e);\n        }\n    }\n}\n```\n\n### 3.2 文件元数据实体\n```java\n@Entity\n@Table(name = \"file_metadata\")\n@Data\npublic class FileMetadata {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n    \n    @Column(nullable = false, unique = true, length = 64)\n    private String fileHash;\n    \n    @Column(nullable = false)\n    private String fileName;\n    \n    @Column(nullable = false)\n    private Long fileSize;\n    \n    @Column(nullable = false, length = 500)\n    private String filePath;\n    \n    @Column(updatable = false)\n    private LocalDateTime uploadTime = LocalDateTime.now();\n    \n    private Integer uploadCount = 1;\n}\n```\n\n### 3.3 哈希服务\n```java\n@Service\n@Slf4j\npublic class HashService {\n    \n    private static final int BUFFER_SIZE = 8192;\n    \n    \u002F**\n     * 计算文件的MD5哈希值\n     *\u002F\n    public String calculateFileHash(File file) throws IOException {\n        try (FileInputStream fis = new FileInputStream(file);\n             DigestInputStream dis = new DigestInputStream(fis, \n                 MessageDigest.getInstance(\"MD5\"))) {\n            \n            byte[] buffer = new byte[BUFFER_SIZE];\n            while (dis.read(buffer) != -1) {\n                \u002F\u002F 读取文件内容，自动更新哈希\n            }\n            \n            byte[] hashBytes = dis.getMessageDigest().digest();\n            return bytesToHex(hashBytes);\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"MD5算法不可用\", e);\n        }\n    }\n    \n    \u002F**\n     * 计算输入流的哈希值（适用于大文件分片计算）\n     *\u002F\n    public String calculateStreamHash(InputStream inputStream) throws IOException {\n        try {\n            MessageDigest digest = MessageDigest.getInstance(\"MD5\");\n            byte[] buffer = new byte[BUFFER_SIZE];\n            int bytesRead;\n            \n            while ((bytesRead = inputStream.read(buffer)) != -1) {\n                digest.update(buffer, 0, bytesRead);\n            }\n            \n            byte[] hashBytes = digest.digest();\n            return bytesToHex(hashBytes);\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"MD5算法不可用\", e);\n        }\n    }\n    \n    private String bytesToHex(byte[] bytes) {\n        StringBuilder hexString = new StringBuilder();\n        for (byte b : bytes) {\n            String hex = Integer.toHexString(0xff & b);\n            if (hex.length() == 1) {\n                hexString.append('0');\n            }\n            hexString.append(hex);\n        }\n        return hexString.toString();\n    }\n}\n```\n\n### 3.4 文件存储服务\n```java\n@Service\n@Slf4j\npublic class FileStorageService {\n    \n    private final Path fileStorageLocation;\n    private final FileMetadataRepository metadataRepository;\n    private final HashService hashService;\n    \n    public FileStorageService(Path fileStorageLocation, \n                            FileMetadataRepository metadataRepository,\n                            HashService hashService) {\n        this.fileStorageLocation = fileStorageLocation;\n        this.metadataRepository = metadataRepository;\n        this.hashService = hashService;\n    }\n    \n    \u002F**\n     * 检查文件是否已存在（秒传检查）\n     *\u002F\n    public UploadResponse checkFileExists(String fileHash, String fileName, long fileSize) {\n        Optional\u003CFileMetadata> existingFile = metadataRepository.findByFileHash(fileHash);\n        \n        if (existingFile.isPresent()) {\n            FileMetadata metadata = existingFile.get();\n            \u002F\u002F 增加上传计数\n            metadata.setUploadCount(metadata.getUploadCount() + 1);\n            metadataRepository.save(metadata);\n            \n            log.info(\"文件秒传成功: {} -> {}\", fileName, metadata.getFilePath());\n            return UploadResponse.success(metadata, true);\n        }\n        \n        return UploadResponse.notExists();\n    }\n    \n    \u002F**\n     * 存储文件\n     *\u002F\n    public UploadResponse storeFile(MultipartFile file, String clientFileHash) throws IOException {\n        String fileName = StringUtils.cleanPath(file.getOriginalFilename());\n        long fileSize = file.getSize();\n        \n        \u002F\u002F 验证文件名\n        if (fileName.contains(\"..\")) {\n            throw new RuntimeException(\"文件名包含非法路径序列: \" + fileName);\n        }\n        \n        \u002F\u002F 计算服务器端文件哈希（可选，用于双重验证）\n        String serverFileHash = hashService.calculateStreamHash(file.getInputStream());\n        \n        \u002F\u002F 如果提供了客户端哈希，进行验证\n        if (clientFileHash != null && !clientFileHash.equals(serverFileHash)) {\n            log.warn(\"客户端与服务端哈希不一致: {} != {}\", clientFileHash, serverFileHash);\n            \u002F\u002F 可以根据业务需求决定是否继续上传\n        }\n        \n        \u002F\u002F 再次检查是否已存在（防止并发上传）\n        Optional\u003CFileMetadata> existingFile = metadataRepository.findByFileHash(\n            clientFileHash != null ? clientFileHash : serverFileHash);\n        \n        if (existingFile.isPresent()) {\n            return checkFileExists(clientFileHash != null ? clientFileHash : serverFileHash, fileName, fileSize);\n        }\n        \n        \u002F\u002F 生成存储文件名\n        String storageFileName = generateStorageFileName(fileName, serverFileHash);\n        Path targetLocation = fileStorageLocation.resolve(storageFileName);\n        \n        \u002F\u002F 确保目录存在\n        Files.createDirectories(targetLocation.getParent());\n        \n        \u002F\u002F 存储文件\n        Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);\n        \n        \u002F\u002F 保存元数据\n        FileMetadata metadata = new FileMetadata();\n        metadata.setFileHash(serverFileHash);\n        metadata.setFileName(fileName);\n        metadata.setFileSize(fileSize);\n        metadata.setFilePath(targetLocation.toString());\n        metadata.setUploadTime(LocalDateTime.now());\n        \n        FileMetadata savedMetadata = metadataRepository.save(metadata);\n        log.info(\"文件上传成功: {} -> {}\", fileName, targetLocation);\n        \n        return UploadResponse.success(savedMetadata, false);\n    }\n    \n    \u002F**\n     * 生成存储文件名：哈希值_原始文件名\n     *\u002F\n    private String generateStorageFileName(String originalFileName, String fileHash) {\n        String fileExtension = \"\";\n        int dotIndex = originalFileName.lastIndexOf(\".\");\n        if (dotIndex > 0) {\n            fileExtension = originalFileName.substring(dotIndex);\n        }\n        return fileHash + fileExtension;\n    }\n    \n    \u002F**\n     * 根据哈希值加载文件\n     *\u002F\n    public Resource loadFileAsResource(String fileHash) {\n        try {\n            FileMetadata metadata = metadataRepository.findByFileHash(fileHash)\n                    .orElseThrow(() -> new RuntimeException(\"文件不存在: \" + fileHash));\n            \n            Path filePath = Paths.get(metadata.getFilePath()).normalize();\n            Resource resource = new UrlResource(filePath.toUri());\n            \n            if (resource.exists()) {\n                return resource;\n            } else {\n                throw new RuntimeException(\"文件不存在: \" + fileHash);\n            }\n        } catch (MalformedURLException e) {\n            throw new RuntimeException(\"文件路径错误: \" + fileHash, e);\n        }\n    }\n}\n```\n\n### 3.5 控制器实现\n```java\n@RestController\n@RequestMapping(\"\u002Fapi\u002Ffile\")\n@Slf4j\npublic class FileUploadController {\n    \n    private final FileStorageService fileStorageService;\n    \n    public FileUploadController(FileStorageService fileStorageService) {\n        this.fileStorageService = fileStorageService;\n    }\n    \n    \u002F**\n     * 秒传检查接口\n     *\u002F\n    @PostMapping(\"\u002Fcheck\")\n    public UploadResponse checkFile(@RequestBody UploadRequest request) {\n        log.info(\"秒传检查: {}, 大小: {}\", request.getFileName(), request.getFileSize());\n        return fileStorageService.checkFileExists(\n            request.getFileHash(), \n            request.getFileName(), \n            request.getFileSize()\n        );\n    }\n    \n    \u002F**\n     * 文件上传接口\n     *\u002F\n    @PostMapping(\"\u002Fupload\")\n    public UploadResponse uploadFile(\n            @RequestParam(\"file\") MultipartFile file,\n            @RequestParam(value = \"fileHash\", required = false) String fileHash) {\n        \n        try {\n            if (file.isEmpty()) {\n                return UploadResponse.failure(\"文件为空\");\n            }\n            \n            \u002F\u002F 先进行秒传检查\n            if (fileHash != null) {\n                UploadResponse checkResult = fileStorageService.checkFileExists(\n                    fileHash, file.getOriginalFilename(), file.getSize());\n                \n                if (checkResult.isExists()) {\n                    return checkResult;\n                }\n            }\n            \n            \u002F\u002F 执行实际上传\n            return fileStorageService.storeFile(file, fileHash);\n            \n        } catch (IOException e) {\n            log.error(\"文件上传失败: {}\", e.getMessage());\n            return UploadResponse.failure(\"文件上传失败: \" + e.getMessage());\n        } catch (Exception e) {\n            log.error(\"系统错误: {}\", e.getMessage());\n            return UploadResponse.failure(\"系统错误: \" + e.getMessage());\n        }\n    }\n    \n    \u002F**\n     * 文件下载接口\n     *\u002F\n    @GetMapping(\"\u002Fdownload\u002F{fileHash}\")\n    public ResponseEntity\u003CResource> downloadFile(@PathVariable String fileHash) {\n        try {\n            Resource resource = fileStorageService.loadFileAsResource(fileHash);\n            FileMetadata metadata = fileStorageService.getFileMetadata(fileHash)\n                    .orElseThrow(() -> new RuntimeException(\"文件不存在\"));\n            \n            return ResponseEntity.ok()\n                    .header(HttpHeaders.CONTENT_DISPOSITION, \n                           \"attachment; filename=\\\"\" + metadata.getFileName() + \"\\\"\")\n                    .contentType(MediaType.APPLICATION_OCTET_STREAM)\n                    .body(resource);\n            \n        } catch (RuntimeException e) {\n            return ResponseEntity.notFound().build();\n        }\n    }\n}\n```\n\n### 3.6 DTO对象\n```java\n@Data\npublic class UploadRequest {\n    private String fileHash;\n    private String fileName;\n    private Long fileSize;\n}\n\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class UploadResponse {\n    private boolean success;\n    private String message;\n    private boolean exists; \u002F\u002F 是否已存在（秒传）\n    private FileMetadata fileMetadata;\n    \n    public static UploadResponse success(FileMetadata metadata, boolean isInstant) {\n        String message = isInstant ? \"文件秒传成功\" : \"文件上传成功\";\n        return new UploadResponse(true, message, isInstant, metadata);\n    }\n    \n    public static UploadResponse notExists() {\n        return new UploadResponse(true, \"文件不存在，需要上传\", false, null);\n    }\n    \n    public static UploadResponse failure(String message) {\n        return new UploadResponse(false, message, false, null);\n    }\n}\n```\n\n## 4. 前端实现示例\n\n### 4.1 HTML页面\n```html\n\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n    \u003Ctitle>文件秒传演示\u003C\u002Ftitle>\n    \u003Cscript src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fspark-md5@3.0.2\u002Fspark-md5.min.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv>\n        \u003Cinput type=\"file\" id=\"fileInput\">\n        \u003Cbutton onclick=\"uploadFile()\">上传文件\u003C\u002Fbutton>\n        \u003Cdiv id=\"progress\">\u003C\u002Fdiv>\n        \u003Cdiv id=\"result\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cscript>\n        async function uploadFile() {\n            const fileInput = document.getElementById('fileInput');\n            const file = fileInput.files[0];\n            \n            if (!file) {\n                alert('请选择文件');\n                return;\n            }\n            \n            const progressDiv = document.getElementById('progress');\n            const resultDiv = document.getElementById('result');\n            \n            try {\n                \u002F\u002F 计算文件哈希\n                progressDiv.innerHTML = '计算文件哈希中...';\n                const fileHash = await calculateFileHash(file);\n                \n                \u002F\u002F 秒传检查\n                progressDiv.innerHTML = '检查文件是否已存在...';\n                const checkResponse = await fetch('\u002Fapi\u002Ffile\u002Fcheck', {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application\u002Fjson' },\n                    body: JSON.stringify({\n                        fileHash: fileHash,\n                        fileName: file.name,\n                        fileSize: file.size\n                    })\n                });\n                \n                const checkResult = await checkResponse.json();\n                \n                if (checkResult.exists) {\n                    resultDiv.innerHTML = `✅ 秒传成功！${checkResult.message}`;\n                    return;\n                }\n                \n                \u002F\u002F 执行上传\n                progressDiv.innerHTML = '上传文件中...';\n                const formData = new FormData();\n                formData.append('file', file);\n                formData.append('fileHash', fileHash);\n                \n                const uploadResponse = await fetch('\u002Fapi\u002Ffile\u002Fupload', {\n                    method: 'POST',\n                    body: formData\n                });\n                \n                const uploadResult = await uploadResponse.json();\n                \n                if (uploadResult.success) {\n                    resultDiv.innerHTML = `✅ ${uploadResult.message}`;\n                } else {\n                    resultDiv.innerHTML = `❌ 上传失败: ${uploadResult.message}`;\n                }\n                \n            } catch (error) {\n                resultDiv.innerHTML = `❌ 错误: ${error.message}`;\n            } finally {\n                progressDiv.innerHTML = '';\n            }\n        }\n        \n        function calculateFileHash(file) {\n            return new Promise((resolve, reject) => {\n                const chunkSize = 2 * 1024 * 1024; \u002F\u002F 2MB分片\n                const chunks = Math.ceil(file.size \u002F chunkSize);\n                const spark = new SparkMD5.ArrayBuffer();\n                const fileReader = new FileReader();\n                let currentChunk = 0;\n                \n                function loadNext() {\n                    const start = currentChunk * chunkSize;\n                    const end = Math.min(start + chunkSize, file.size);\n                    const chunk = file.slice(start, end);\n                    \n                    fileReader.readAsArrayBuffer(chunk);\n                }\n                \n                fileReader.onload = e => {\n                    spark.append(e.target.result);\n                    currentChunk++;\n                    \n                    if (currentChunk \u003C chunks) {\n                        loadNext();\n                    } else {\n                        resolve(spark.end());\n                    }\n                };\n                \n                fileReader.onerror = reject;\n                loadNext();\n            });\n        }\n    \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n```\n\n## 5. 高级特性与优化\n\n### 5.1 分片上传与秒传结合\n```java\n@Service\npublic class ChunkedUploadService {\n    \n    \u002F**\n     * 分片上传检查\n     *\u002F\n    public ChunkedUploadResponse checkChunkedUpload(String fileHash, String fileName, \n                                                   long fileSize, int chunkSize) {\n        Optional\u003CFileMetadata> existingFile = metadataRepository.findByFileHash(fileHash);\n        \n        if (existingFile.isPresent()) {\n            return ChunkedUploadResponse.instant(existingFile.get());\n        }\n        \n        \u002F\u002F 检查已上传的分片\n        List\u003CInteger> uploadedChunks = findUploadedChunks(fileHash);\n        return ChunkedUploadResponse.continueUpload(uploadedChunks);\n    }\n    \n    \u002F**\n     * 上传分片\n     *\u002F\n    public void uploadChunk(String fileHash, int chunkIndex, \n                           MultipartFile chunkFile) throws IOException {\n        \u002F\u002F 存储分片文件\n        String chunkFileName = String.format(\"%s_%d\", fileHash, chunkIndex);\n        Path chunkPath = chunkStorageLocation.resolve(chunkFileName);\n        Files.copy(chunkFile.getInputStream(), chunkPath, \n                  StandardCopyOption.REPLACE_EXISTING);\n        \n        \u002F\u002F 记录分片上传状态\n        recordChunkUpload(fileHash, chunkIndex);\n    }\n}\n```\n\n### 5.2 分布式环境下的秒传\n在分布式环境中，需要使用分布式缓存来存储文件哈希映射：\n\n```java\n@Service\npublic class DistributedFileService {\n    \n    @Autowired\n    private RedisTemplate\u003CString, String> redisTemplate;\n    \n    private static final String FILE_HASH_KEY = \"file:hash:\";\n    private static final long CACHE_EXPIRE_HOURS = 24;\n    \n    \u002F**\n     * 分布式秒传检查\n     *\u002F\n    public boolean checkFileExistsDistributed(String fileHash) {\n        \u002F\u002F 先查缓存\n        String cachedPath = redisTemplate.opsForValue().get(FILE_HASH_KEY + fileHash);\n        if (cachedPath != null) {\n            return true;\n        }\n        \n        \u002F\u002F 查数据库\n        Optional\u003CFileMetadata> metadata = metadataRepository.findByFileHash(fileHash);\n        if (metadata.isPresent()) {\n            \u002F\u002F 写入缓存\n            redisTemplate.opsForValue().set(FILE_HASH_KEY + fileHash, \n                metadata.get().getFilePath(), \n                Duration.ofHours(CACHE_EXPIRE_HOURS));\n            return true;\n        }\n        \n        return false;\n    }\n}\n```\n\n## 6. 测试与验证\n\n### 6.1 单元测试\n```java\n@SpringBootTest\nclass FileUploadServiceTest {\n    \n    @Autowired\n    private FileStorageService fileStorageService;\n    \n    @Test\n    void testInstantUpload() throws IOException {\n        \u002F\u002F 准备测试文件\n        MultipartFile file = createTestFile(\"test.txt\", \"Hello World\");\n        \n        \u002F\u002F 第一次上传\n        UploadResponse response1 = fileStorageService.storeFile(file, null);\n        assertTrue(response1.isSuccess());\n        assertFalse(response1.isExists());\n        \n        \u002F\u002F 第二次上传（应该秒传）\n        String fileHash = response1.getFileMetadata().getFileHash();\n        UploadResponse response2 = fileStorageService.checkFileExists(\n            fileHash, \"test.txt\", file.getSize());\n        \n        assertTrue(response2.isSuccess());\n        assertTrue(response2.isExists());\n    }\n}\n```\n\n## 7. 总结\n\n文件秒传功能通过文件内容哈希比对，有效避免了重复文件的传输，具有以下优势：\n\n1. **节省带宽**：减少重复文件上传的网络消耗\n2. **提升用户体验**：大幅缩短上传时间\n3. **节省存储空间**：相同文件只存储一份\n\n在实际应用中，还需要考虑：\n- 哈希碰撞的概率（虽然极低）\n- 大文件哈希计算的性能优化\n- 分布式环境下的并发处理\n- 安全性和权限控制\n\n通过本文的实现，你可以快速为Spring Boot应用添加文件秒传功能，并根据实际需求进行扩展和优化。","\u003Ch2 data-line=\"0\" id=\"1. 文件秒传技术原理\">1. 文件秒传技术原理\u003C\u002Fh2>\n\u003Ch3 data-line=\"2\" id=\"1.1 什么是文件秒传\">1.1 什么是文件秒传\u003C\u002Fh3>\n\u003Cp data-line=\"3\">文件秒传是指当用户上传一个文件时，如果服务器上已经存在相同的文件，则无需再次上传文件内容，直接返回上传成功。这种技术可以显著减少网络传输量，提升用户体验。\u003C\u002Fp>\n\u003Ch3 data-line=\"5\" id=\"1.2 实现原理\">1.2 实现原理\u003C\u002Fh3>\n\u003Cp data-line=\"6\">秒传的核心原理是\u003Cstrong>文件内容指纹比对\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col data-line=\"8\">\n\u003Cli data-line=\"8\">\u003Cstrong>客户端计算文件哈希值\u003C\u002Fstrong>：在上传前，客户端计算文件的哈希值（如MD5、SHA-1等）\u003C\u002Fli>\n\u003Cli data-line=\"9\">\u003Cstrong>查询服务器文件状态\u003C\u002Fstrong>：将哈希值发送到服务器查询是否已存在\u003C\u002Fli>\n\u003Cli data-line=\"10\">\u003Cstrong>服务器验证\u003C\u002Fstrong>：服务器检查哈希值对应的文件是否存在\u003C\u002Fli>\n\u003Cli data-line=\"11\">\u003Cstrong>决定上传策略\u003C\u002Fstrong>：\n\u003Cul data-line=\"12\">\n\u003Cli data-line=\"12\">文件已存在：直接返回成功（秒传）\u003C\u002Fli>\n\u003Cli data-line=\"13\">文件不存在：执行正常上传流程\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 data-line=\"15\" id=\"1.3 哈希算法选择\">1.3 哈希算法选择\u003C\u002Fh3>\n\u003Ctable data-line=\"16\">\n\u003Cthead data-line=\"16\">\n\u003Ctr data-line=\"16\">\n\u003Cth>算法\u003C\u002Fth>\n\u003Cth>安全性\u003C\u002Fth>\n\u003Cth>速度\u003C\u002Fth>\n\u003Cth>适用场景\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody data-line=\"18\">\n\u003Ctr data-line=\"18\">\n\u003Ctd>MD5\u003C\u002Ftd>\n\u003Ctd>较低\u003C\u002Ftd>\n\u003Ctd>快\u003C\u002Ftd>\n\u003Ctd>一般文件校验\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr data-line=\"19\">\n\u003Ctd>SHA-1\u003C\u002Ftd>\n\u003Ctd>中等\u003C\u002Ftd>\n\u003Ctd>较快\u003C\u002Ftd>\n\u003Ctd>普通安全需求\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr data-line=\"20\">\n\u003Ctd>SHA-256\u003C\u002Ftd>\n\u003Ctd>高\u003C\u002Ftd>\n\u003Ctd>较慢\u003C\u002Ftd>\n\u003Ctd>高安全需求\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2 data-line=\"22\" id=\"2. 系统设计与实现\">2. 系统设计与实现\u003C\u002Fh2>\n\u003Ch3 data-line=\"24\" id=\"2.1 数据库设计\">2.1 数据库设计\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"25\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">sql\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-sql\" language=sql>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-keyword\">CREATE\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">TABLE\u003C\u002Fspan> file_metadata (\n    id \u003Cspan class=\"hljs-type\">BIGINT\u003C\u002Fspan> AUTO_INCREMENT \u003Cspan class=\"hljs-keyword\">PRIMARY\u003C\u002Fspan> KEY,\n    file_hash \u003Cspan class=\"hljs-type\">VARCHAR\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">64\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">NOT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">NULL\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">UNIQUE\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;文件哈希值&#x27;\u003C\u002Fspan>,\n    file_name \u003Cspan class=\"hljs-type\">VARCHAR\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">255\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">NOT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">NULL\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;原始文件名&#x27;\u003C\u002Fspan>,\n    file_size \u003Cspan class=\"hljs-type\">BIGINT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">NOT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">NULL\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;文件大小&#x27;\u003C\u002Fspan>,\n    file_path \u003Cspan class=\"hljs-type\">VARCHAR\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">500\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">NOT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">NULL\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;存储路径&#x27;\u003C\u002Fspan>,\n    upload_time DATETIME \u003Cspan class=\"hljs-keyword\">DEFAULT\u003C\u002Fspan> \u003Cspan class=\"hljs-built_in\">CURRENT_TIMESTAMP\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;上传时间&#x27;\u003C\u002Fspan>,\n    upload_count \u003Cspan class=\"hljs-type\">INT\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">DEFAULT\u003C\u002Fspan> \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan> COMMENT \u003Cspan class=\"hljs-string\">&#x27;上传次数&#x27;\u003C\u002Fspan>,\n    INDEX idx_hash (file_hash)\n);\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"38\" id=\"2.2 项目结构\">2.2 项目结构\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"39\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-\" language=>\u003Cspan class=\"md-editor-code-block\">src\u003Cspan class=\"hljs-regexp\">\u002Fmain\u002F\u003C\u002Fspan>java\u003Cspan class=\"hljs-regexp\">\u002Fcom\u002F\u003C\u002Fspan>example\u003Cspan class=\"hljs-regexp\">\u002Fupload\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> config\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">FileStorageConfig\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> controller\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">FileUploadController\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> entity\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> service\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">FileStorageService\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">HashService\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> repository\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n\u003Cspan class=\"hljs-operator\">│\u003C\u002Fspan>   \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">FileMetadataRepository\u003C\u002Fspan>.java\n\u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> dto\u003Cspan class=\"hljs-operator\">\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-operator\">├──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">UploadRequest\u003C\u002Fspan>.java\n    \u003Cspan class=\"hljs-operator\">└──\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">UploadResponse\u003C\u002Fspan>.java\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch2 data-line=\"57\" id=\"3. 核心代码实现\">3. 核心代码实现\u003C\u002Fh2>\n\u003Ch3 data-line=\"59\" id=\"3.1 配置文件存储\">3.1 配置文件存储\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"60\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Configuration\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileStorageConfig\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-meta\">@Value(&quot;${file.upload-dir:.\u002Fuploads}&quot;)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String uploadDir;\n    \n    \u003Cspan class=\"hljs-meta\">@Bean\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> Path \u003Cspan class=\"hljs-title function_\">fileStorageLocation\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">()\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-type\">Path\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">path\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> Paths.get(uploadDir).toAbsolutePath().normalize();\n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n            Files.createDirectories(path);\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> path;\n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (IOException e) {\n            \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;无法创建文件存储目录&quot;\u003C\u002Fspan>, e);\n        }\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"80\" id=\"3.2 文件元数据实体\">3.2 文件元数据实体\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"81\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Entity\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@Table(name = &quot;file_metadata&quot;)\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@Data\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileMetadata\u003C\u002Fspan> {\n    \u003Cspan class=\"hljs-meta\">@Id\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-meta\">@GeneratedValue(strategy = GenerationType.IDENTITY)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> Long id;\n    \n    \u003Cspan class=\"hljs-meta\">@Column(nullable = false, unique = true, length = 64)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String fileHash;\n    \n    \u003Cspan class=\"hljs-meta\">@Column(nullable = false)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String fileName;\n    \n    \u003Cspan class=\"hljs-meta\">@Column(nullable = false)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> Long fileSize;\n    \n    \u003Cspan class=\"hljs-meta\">@Column(nullable = false, length = 500)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String filePath;\n    \n    \u003Cspan class=\"hljs-meta\">@Column(updatable = false)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">LocalDateTime\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">uploadTime\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> LocalDateTime.now();\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">Integer\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">uploadCount\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>;\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"109\" id=\"3.3 哈希服务\">3.3 哈希服务\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"110\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Service\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@Slf4j\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">HashService\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">int\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">BUFFER_SIZE\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-number\">8192\u003C\u002Fspan>;\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 计算文件的MD5哈希值\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> String \u003Cspan class=\"hljs-title function_\">calculateFileHash\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(File file)\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">throws\u003C\u002Fspan> IOException {\n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> (\u003Cspan class=\"hljs-type\">FileInputStream\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">fis\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileInputStream\u003C\u002Fspan>(file);\n             \u003Cspan class=\"hljs-type\">DigestInputStream\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">dis\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">DigestInputStream\u003C\u002Fspan>(fis, \n                 MessageDigest.getInstance(\u003Cspan class=\"hljs-string\">&quot;MD5&quot;\u003C\u002Fspan>))) {\n            \n            \u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan>[] buffer = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">byte\u003C\u002Fspan>[BUFFER_SIZE];\n            \u003Cspan class=\"hljs-keyword\">while\u003C\u002Fspan> (dis.read(buffer) != -\u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>) {\n                \u003Cspan class=\"hljs-comment\">\u002F\u002F 读取文件内容，自动更新哈希\u003C\u002Fspan>\n            }\n            \n            \u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan>[] hashBytes = dis.getMessageDigest().digest();\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> bytesToHex(hashBytes);\n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (NoSuchAlgorithmException e) {\n            \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;MD5算法不可用&quot;\u003C\u002Fspan>, e);\n        }\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 计算输入流的哈希值（适用于大文件分片计算）\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> String \u003Cspan class=\"hljs-title function_\">calculateStreamHash\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(InputStream inputStream)\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">throws\u003C\u002Fspan> IOException {\n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n            \u003Cspan class=\"hljs-type\">MessageDigest\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">digest\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> MessageDigest.getInstance(\u003Cspan class=\"hljs-string\">&quot;MD5&quot;\u003C\u002Fspan>);\n            \u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan>[] buffer = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">byte\u003C\u002Fspan>[BUFFER_SIZE];\n            \u003Cspan class=\"hljs-type\">int\u003C\u002Fspan> bytesRead;\n            \n            \u003Cspan class=\"hljs-keyword\">while\u003C\u002Fspan> ((bytesRead = inputStream.read(buffer)) != -\u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>) {\n                digest.update(buffer, \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>, bytesRead);\n            }\n            \n            \u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan>[] hashBytes = digest.digest();\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> bytesToHex(hashBytes);\n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (NoSuchAlgorithmException e) {\n            \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;MD5算法不可用&quot;\u003C\u002Fspan>, e);\n        }\n    }\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String \u003Cspan class=\"hljs-title function_\">bytesToHex\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(\u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan>[] bytes)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-type\">StringBuilder\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">hexString\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">StringBuilder\u003C\u002Fspan>();\n        \u003Cspan class=\"hljs-keyword\">for\u003C\u002Fspan> (\u003Cspan class=\"hljs-type\">byte\u003C\u002Fspan> b : bytes) {\n            \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">hex\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> Integer.toHexString(\u003Cspan class=\"hljs-number\">0xff\u003C\u002Fspan> &amp; b);\n            \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (hex.length() == \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>) {\n                hexString.append(\u003Cspan class=\"hljs-string\">&#x27;0&#x27;\u003C\u002Fspan>);\n            }\n            hexString.append(hex);\n        }\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> hexString.toString();\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"171\" id=\"3.4 文件存储服务\">3.4 文件存储服务\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"172\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Service\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@Slf4j\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileStorageService\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> Path fileStorageLocation;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> FileMetadataRepository metadataRepository;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> HashService hashService;\n    \n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">FileStorageService\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(Path fileStorageLocation, \n                            FileMetadataRepository metadataRepository,\n                            HashService hashService)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-built_in\">this\u003C\u002Fspan>.fileStorageLocation = fileStorageLocation;\n        \u003Cspan class=\"hljs-built_in\">this\u003C\u002Fspan>.metadataRepository = metadataRepository;\n        \u003Cspan class=\"hljs-built_in\">this\u003C\u002Fspan>.hashService = hashService;\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 检查文件是否已存在（秒传检查）\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">checkFileExists\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String fileHash, String fileName, \u003Cspan class=\"hljs-type\">long\u003C\u002Fspan> fileSize)\u003C\u002Fspan> {\n        Optional&lt;FileMetadata&gt; existingFile = metadataRepository.findByFileHash(fileHash);\n        \n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (existingFile.isPresent()) {\n            \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">metadata\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> existingFile.get();\n            \u003Cspan class=\"hljs-comment\">\u002F\u002F 增加上传计数\u003C\u002Fspan>\n            metadata.setUploadCount(metadata.getUploadCount() + \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>);\n            metadataRepository.save(metadata);\n            \n            log.info(\u003Cspan class=\"hljs-string\">&quot;文件秒传成功: {} -&gt; {}&quot;\u003C\u002Fspan>, fileName, metadata.getFilePath());\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.success(metadata, \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>);\n        }\n        \n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.notExists();\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 存储文件\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">storeFile\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(MultipartFile file, String clientFileHash)\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">throws\u003C\u002Fspan> IOException {\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">fileName\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> StringUtils.cleanPath(file.getOriginalFilename());\n        \u003Cspan class=\"hljs-type\">long\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">fileSize\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> file.getSize();\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 验证文件名\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (fileName.contains(\u003Cspan class=\"hljs-string\">&quot;..&quot;\u003C\u002Fspan>)) {\n            \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;文件名包含非法路径序列: &quot;\u003C\u002Fspan> + fileName);\n        }\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 计算服务器端文件哈希（可选，用于双重验证）\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">serverFileHash\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> hashService.calculateStreamHash(file.getInputStream());\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 如果提供了客户端哈希，进行验证\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (clientFileHash != \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan> &amp;&amp; !clientFileHash.equals(serverFileHash)) {\n            log.warn(\u003Cspan class=\"hljs-string\">&quot;客户端与服务端哈希不一致: {} != {}&quot;\u003C\u002Fspan>, clientFileHash, serverFileHash);\n            \u003Cspan class=\"hljs-comment\">\u002F\u002F 可以根据业务需求决定是否继续上传\u003C\u002Fspan>\n        }\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 再次检查是否已存在（防止并发上传）\u003C\u002Fspan>\n        Optional&lt;FileMetadata&gt; existingFile = metadataRepository.findByFileHash(\n            clientFileHash != \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan> ? clientFileHash : serverFileHash);\n        \n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (existingFile.isPresent()) {\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> checkFileExists(clientFileHash != \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan> ? clientFileHash : serverFileHash, fileName, fileSize);\n        }\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 生成存储文件名\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">storageFileName\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> generateStorageFileName(fileName, serverFileHash);\n        \u003Cspan class=\"hljs-type\">Path\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">targetLocation\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageLocation.resolve(storageFileName);\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 确保目录存在\u003C\u002Fspan>\n        Files.createDirectories(targetLocation.getParent());\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 存储文件\u003C\u002Fspan>\n        Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 保存元数据\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">metadata\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileMetadata\u003C\u002Fspan>();\n        metadata.setFileHash(serverFileHash);\n        metadata.setFileName(fileName);\n        metadata.setFileSize(fileSize);\n        metadata.setFilePath(targetLocation.toString());\n        metadata.setUploadTime(LocalDateTime.now());\n        \n        \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">savedMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> metadataRepository.save(metadata);\n        log.info(\u003Cspan class=\"hljs-string\">&quot;文件上传成功: {} -&gt; {}&quot;\u003C\u002Fspan>, fileName, targetLocation);\n        \n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.success(savedMetadata, \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>);\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 生成存储文件名：哈希值_原始文件名\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String \u003Cspan class=\"hljs-title function_\">generateStorageFileName\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String originalFileName, String fileHash)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">fileExtension\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;&quot;\u003C\u002Fspan>;\n        \u003Cspan class=\"hljs-type\">int\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">dotIndex\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> originalFileName.lastIndexOf(\u003Cspan class=\"hljs-string\">&quot;.&quot;\u003C\u002Fspan>);\n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (dotIndex &gt; \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>) {\n            fileExtension = originalFileName.substring(dotIndex);\n        }\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> fileHash + fileExtension;\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 根据哈希值加载文件\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> Resource \u003Cspan class=\"hljs-title function_\">loadFileAsResource\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String fileHash)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n            \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">metadata\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> metadataRepository.findByFileHash(fileHash)\n                    .orElseThrow(() -&gt; \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;文件不存在: &quot;\u003C\u002Fspan> + fileHash));\n            \n            \u003Cspan class=\"hljs-type\">Path\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">filePath\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> Paths.get(metadata.getFilePath()).normalize();\n            \u003Cspan class=\"hljs-type\">Resource\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">resource\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UrlResource\u003C\u002Fspan>(filePath.toUri());\n            \n            \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (resource.exists()) {\n                \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> resource;\n            } \u003Cspan class=\"hljs-keyword\">else\u003C\u002Fspan> {\n                \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;文件不存在: &quot;\u003C\u002Fspan> + fileHash);\n            }\n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (MalformedURLException e) {\n            \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;文件路径错误: &quot;\u003C\u002Fspan> + fileHash, e);\n        }\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"296\" id=\"3.5 控制器实现\">3.5 控制器实现\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"297\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@RestController\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@RequestMapping(&quot;\u002Fapi\u002Ffile&quot;)\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@Slf4j\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileUploadController\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> FileStorageService fileStorageService;\n    \n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">FileUploadController\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(FileStorageService fileStorageService)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-built_in\">this\u003C\u002Fspan>.fileStorageService = fileStorageService;\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 秒传检查接口\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-meta\">@PostMapping(&quot;\u002Fcheck&quot;)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">checkFile\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(\u003Cspan class=\"hljs-meta\">@RequestBody\u003C\u002Fspan> UploadRequest request)\u003C\u002Fspan> {\n        log.info(\u003Cspan class=\"hljs-string\">&quot;秒传检查: {}, 大小: {}&quot;\u003C\u002Fspan>, request.getFileName(), request.getFileSize());\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> fileStorageService.checkFileExists(\n            request.getFileHash(), \n            request.getFileName(), \n            request.getFileSize()\n        );\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 文件上传接口\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-meta\">@PostMapping(&quot;\u002Fupload&quot;)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">uploadFile\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(\n            \u003Cspan class=\"hljs-meta\">@RequestParam(&quot;file&quot;)\u003C\u002Fspan> MultipartFile file,\n            \u003Cspan class=\"hljs-meta\">@RequestParam(value = &quot;fileHash&quot;, required = false)\u003C\u002Fspan> String fileHash)\u003C\u002Fspan> {\n        \n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n            \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (file.isEmpty()) {\n                \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.failure(\u003Cspan class=\"hljs-string\">&quot;文件为空&quot;\u003C\u002Fspan>);\n            }\n            \n            \u003Cspan class=\"hljs-comment\">\u002F\u002F 先进行秒传检查\u003C\u002Fspan>\n            \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (fileHash != \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>) {\n                \u003Cspan class=\"hljs-type\">UploadResponse\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">checkResult\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageService.checkFileExists(\n                    fileHash, file.getOriginalFilename(), file.getSize());\n                \n                \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (checkResult.isExists()) {\n                    \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> checkResult;\n                }\n            }\n            \n            \u003Cspan class=\"hljs-comment\">\u002F\u002F 执行实际上传\u003C\u002Fspan>\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> fileStorageService.storeFile(file, fileHash);\n            \n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (IOException e) {\n            log.error(\u003Cspan class=\"hljs-string\">&quot;文件上传失败: {}&quot;\u003C\u002Fspan>, e.getMessage());\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.failure(\u003Cspan class=\"hljs-string\">&quot;文件上传失败: &quot;\u003C\u002Fspan> + e.getMessage());\n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (Exception e) {\n            log.error(\u003Cspan class=\"hljs-string\">&quot;系统错误: {}&quot;\u003C\u002Fspan>, e.getMessage());\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> UploadResponse.failure(\u003Cspan class=\"hljs-string\">&quot;系统错误: &quot;\u003C\u002Fspan> + e.getMessage());\n        }\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 文件下载接口\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-meta\">@GetMapping(&quot;\u002Fdownload\u002F{fileHash}&quot;)\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> ResponseEntity&lt;Resource&gt; \u003Cspan class=\"hljs-title function_\">downloadFile\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(\u003Cspan class=\"hljs-meta\">@PathVariable\u003C\u002Fspan> String fileHash)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n            \u003Cspan class=\"hljs-type\">Resource\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">resource\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageService.loadFileAsResource(fileHash);\n            \u003Cspan class=\"hljs-type\">FileMetadata\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">metadata\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageService.getFileMetadata(fileHash)\n                    .orElseThrow(() -&gt; \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">RuntimeException\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;文件不存在&quot;\u003C\u002Fspan>));\n            \n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> ResponseEntity.ok()\n                    .header(HttpHeaders.CONTENT_DISPOSITION, \n                           \u003Cspan class=\"hljs-string\">&quot;attachment; filename=\\&quot;&quot;\u003C\u002Fspan> + metadata.getFileName() + \u003Cspan class=\"hljs-string\">&quot;\\&quot;&quot;\u003C\u002Fspan>)\n                    .contentType(MediaType.APPLICATION_OCTET_STREAM)\n                    .body(resource);\n            \n        } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (RuntimeException e) {\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> ResponseEntity.notFound().build();\n        }\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"380\" id=\"3.6 DTO对象\">3.6 DTO对象\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"381\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Data\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UploadRequest\u003C\u002Fspan> {\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String fileHash;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String fileName;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> Long fileSize;\n}\n\n\u003Cspan class=\"hljs-meta\">@Data\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@AllArgsConstructor\u003C\u002Fspan>\n\u003Cspan class=\"hljs-meta\">@NoArgsConstructor\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UploadResponse\u003C\u002Fspan> {\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">boolean\u003C\u002Fspan> success;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> String message;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">boolean\u003C\u002Fspan> exists; \u003Cspan class=\"hljs-comment\">\u002F\u002F 是否已存在（秒传）\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> FileMetadata fileMetadata;\n    \n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">success\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(FileMetadata metadata, \u003Cspan class=\"hljs-type\">boolean\u003C\u002Fspan> isInstant)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">message\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> isInstant ? \u003Cspan class=\"hljs-string\">&quot;文件秒传成功&quot;\u003C\u002Fspan> : \u003Cspan class=\"hljs-string\">&quot;文件上传成功&quot;\u003C\u002Fspan>;\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UploadResponse\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>, message, isInstant, metadata);\n    }\n    \n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">notExists\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">()\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UploadResponse\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>, \u003Cspan class=\"hljs-string\">&quot;文件不存在，需要上传&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>, \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>);\n    }\n    \n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> UploadResponse \u003Cspan class=\"hljs-title function_\">failure\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String message)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">UploadResponse\u003C\u002Fspan>(\u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>, message, \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>, \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>);\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch2 data-line=\"413\" id=\"4. 前端实现示例\">4. 前端实现示例\u003C\u002Fh2>\n\u003Ch3 data-line=\"415\" id=\"4.1 HTML页面\">4.1 HTML页面\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"416\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">html\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-html\" language=html>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">&lt;!DOCTYPE \u003Cspan class=\"hljs-keyword\">html\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">html\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">head\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">title\u003C\u002Fspan>&gt;\u003C\u002Fspan>文件秒传演示\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">title\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">src\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fspark-md5@3.0.2\u002Fspark-md5.min.js&quot;\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">head\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">body\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">input\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">type\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;file&quot;\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">id\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;fileInput&quot;\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">onclick\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;uploadFile()&quot;\u003C\u002Fspan>&gt;\u003C\u002Fspan>上传文件\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">button\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">id\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;progress&quot;\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">id\u003C\u002Fspan>=\u003Cspan class=\"hljs-string\">&quot;result&quot;\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">div\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\n    \u003Cspan class=\"hljs-tag\">&lt;\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003Cspan class=\"language-javascript\">\n        \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">uploadFile\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003C\u002Fspan>) {\n            \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> fileInput = \u003Cspan class=\"hljs-variable language_\">document\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">getElementById\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;fileInput&#x27;\u003C\u002Fspan>);\n            \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> file = fileInput.\u003Cspan class=\"hljs-property\">files\u003C\u002Fspan>[\u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>];\n            \n            \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (!file) {\n                \u003Cspan class=\"hljs-title function_\">alert\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;请选择文件&#x27;\u003C\u002Fspan>);\n                \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>;\n            }\n            \n            \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> progressDiv = \u003Cspan class=\"hljs-variable language_\">document\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">getElementById\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;progress&#x27;\u003C\u002Fspan>);\n            \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> resultDiv = \u003Cspan class=\"hljs-variable language_\">document\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">getElementById\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;result&#x27;\u003C\u002Fspan>);\n            \n            \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n                \u003Cspan class=\"hljs-comment\">\u002F\u002F 计算文件哈希\u003C\u002Fspan>\n                progressDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">&#x27;计算文件哈希中...&#x27;\u003C\u002Fspan>;\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> fileHash = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">calculateFileHash\u003C\u002Fspan>(file);\n                \n                \u003Cspan class=\"hljs-comment\">\u002F\u002F 秒传检查\u003C\u002Fspan>\n                progressDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">&#x27;检查文件是否已存在...&#x27;\u003C\u002Fspan>;\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> checkResponse = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">fetch\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;\u002Fapi\u002Ffile\u002Fcheck&#x27;\u003C\u002Fspan>, {\n                    \u003Cspan class=\"hljs-attr\">method\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">&#x27;POST&#x27;\u003C\u002Fspan>,\n                    \u003Cspan class=\"hljs-attr\">headers\u003C\u002Fspan>: { \u003Cspan class=\"hljs-string\">&#x27;Content-Type&#x27;\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">&#x27;application\u002Fjson&#x27;\u003C\u002Fspan> },\n                    \u003Cspan class=\"hljs-attr\">body\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">JSON\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">stringify\u003C\u002Fspan>({\n                        \u003Cspan class=\"hljs-attr\">fileHash\u003C\u002Fspan>: fileHash,\n                        \u003Cspan class=\"hljs-attr\">fileName\u003C\u002Fspan>: file.\u003Cspan class=\"hljs-property\">name\u003C\u002Fspan>,\n                        \u003Cspan class=\"hljs-attr\">fileSize\u003C\u002Fspan>: file.\u003Cspan class=\"hljs-property\">size\u003C\u002Fspan>\n                    })\n                });\n                \n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> checkResult = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> checkResponse.\u003Cspan class=\"hljs-title function_\">json\u003C\u002Fspan>();\n                \n                \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (checkResult.\u003Cspan class=\"hljs-property\">exists\u003C\u002Fspan>) {\n                    resultDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">`✅ 秒传成功！\u003Cspan class=\"hljs-subst\">${checkResult.message}\u003C\u002Fspan>`\u003C\u002Fspan>;\n                    \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>;\n                }\n                \n                \u003Cspan class=\"hljs-comment\">\u002F\u002F 执行上传\u003C\u002Fspan>\n                progressDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">&#x27;上传文件中...&#x27;\u003C\u002Fspan>;\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> formData = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FormData\u003C\u002Fspan>();\n                formData.\u003Cspan class=\"hljs-title function_\">append\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;file&#x27;\u003C\u002Fspan>, file);\n                formData.\u003Cspan class=\"hljs-title function_\">append\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;fileHash&#x27;\u003C\u002Fspan>, fileHash);\n                \n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> uploadResponse = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">fetch\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&#x27;\u002Fapi\u002Ffile\u002Fupload&#x27;\u003C\u002Fspan>, {\n                    \u003Cspan class=\"hljs-attr\">method\u003C\u002Fspan>: \u003Cspan class=\"hljs-string\">&#x27;POST&#x27;\u003C\u002Fspan>,\n                    \u003Cspan class=\"hljs-attr\">body\u003C\u002Fspan>: formData\n                });\n                \n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> uploadResult = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> uploadResponse.\u003Cspan class=\"hljs-title function_\">json\u003C\u002Fspan>();\n                \n                \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (uploadResult.\u003Cspan class=\"hljs-property\">success\u003C\u002Fspan>) {\n                    resultDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">`✅ \u003Cspan class=\"hljs-subst\">${uploadResult.message}\u003C\u002Fspan>`\u003C\u002Fspan>;\n                } \u003Cspan class=\"hljs-keyword\">else\u003C\u002Fspan> {\n                    resultDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">`❌ 上传失败: \u003Cspan class=\"hljs-subst\">${uploadResult.message}\u003C\u002Fspan>`\u003C\u002Fspan>;\n                }\n                \n            } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (error) {\n                resultDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">`❌ 错误: \u003Cspan class=\"hljs-subst\">${error.message}\u003C\u002Fspan>`\u003C\u002Fspan>;\n            } \u003Cspan class=\"hljs-keyword\">finally\u003C\u002Fspan> {\n                progressDiv.\u003Cspan class=\"hljs-property\">innerHTML\u003C\u002Fspan> = \u003Cspan class=\"hljs-string\">&#x27;&#x27;\u003C\u002Fspan>;\n            }\n        }\n        \n        \u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">calculateFileHash\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">file\u003C\u002Fspan>) {\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Promise\u003C\u002Fspan>(\u003Cspan class=\"hljs-function\">(\u003Cspan class=\"hljs-params\">resolve, reject\u003C\u002Fspan>) =&gt;\u003C\u002Fspan> {\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> chunkSize = \u003Cspan class=\"hljs-number\">2\u003C\u002Fspan> * \u003Cspan class=\"hljs-number\">1024\u003C\u002Fspan> * \u003Cspan class=\"hljs-number\">1024\u003C\u002Fspan>; \u003Cspan class=\"hljs-comment\">\u002F\u002F 2MB分片\u003C\u002Fspan>\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> chunks = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">ceil\u003C\u002Fspan>(file.\u003Cspan class=\"hljs-property\">size\u003C\u002Fspan> \u002F chunkSize);\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> spark = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">SparkMD5\u003C\u002Fspan>.\u003Cspan class=\"hljs-title class_\">ArrayBuffer\u003C\u002Fspan>();\n                \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> fileReader = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileReader\u003C\u002Fspan>();\n                \u003Cspan class=\"hljs-keyword\">let\u003C\u002Fspan> currentChunk = \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>;\n                \n                \u003Cspan class=\"hljs-keyword\">function\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">loadNext\u003C\u002Fspan>(\u003Cspan class=\"hljs-params\">\u003C\u002Fspan>) {\n                    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> start = currentChunk * chunkSize;\n                    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> end = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">min\u003C\u002Fspan>(start + chunkSize, file.\u003Cspan class=\"hljs-property\">size\u003C\u002Fspan>);\n                    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> chunk = file.\u003Cspan class=\"hljs-title function_\">slice\u003C\u002Fspan>(start, end);\n                    \n                    fileReader.\u003Cspan class=\"hljs-title function_\">readAsArrayBuffer\u003C\u002Fspan>(chunk);\n                }\n                \n                fileReader.\u003Cspan class=\"hljs-property\">onload\u003C\u002Fspan> = \u003Cspan class=\"hljs-function\">\u003Cspan class=\"hljs-params\">e\u003C\u002Fspan> =&gt;\u003C\u002Fspan> {\n                    spark.\u003Cspan class=\"hljs-title function_\">append\u003C\u002Fspan>(e.\u003Cspan class=\"hljs-property\">target\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">result\u003C\u002Fspan>);\n                    currentChunk++;\n                    \n                    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (currentChunk &lt; chunks) {\n                        \u003Cspan class=\"hljs-title function_\">loadNext\u003C\u002Fspan>();\n                    } \u003Cspan class=\"hljs-keyword\">else\u003C\u002Fspan> {\n                        \u003Cspan class=\"hljs-title function_\">resolve\u003C\u002Fspan>(spark.\u003Cspan class=\"hljs-title function_\">end\u003C\u002Fspan>());\n                    }\n                };\n                \n                fileReader.\u003Cspan class=\"hljs-property\">onerror\u003C\u002Fspan> = reject;\n                \u003Cspan class=\"hljs-title function_\">loadNext\u003C\u002Fspan>();\n            });\n        }\n    \u003C\u002Fspan>\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">script\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">body\u003C\u002Fspan>&gt;\u003C\u002Fspan>\n\u003Cspan class=\"hljs-tag\">&lt;\u002F\u003Cspan class=\"hljs-name\">html\u003C\u002Fspan>&gt;\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch2 data-line=\"530\" id=\"5. 高级特性与优化\">5. 高级特性与优化\u003C\u002Fh2>\n\u003Ch3 data-line=\"532\" id=\"5.1 分片上传与秒传结合\">5.1 分片上传与秒传结合\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"533\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Service\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">ChunkedUploadService\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 分片上传检查\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> ChunkedUploadResponse \u003Cspan class=\"hljs-title function_\">checkChunkedUpload\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String fileHash, String fileName, \n                                                   \u003Cspan class=\"hljs-type\">long\u003C\u002Fspan> fileSize, \u003Cspan class=\"hljs-type\">int\u003C\u002Fspan> chunkSize)\u003C\u002Fspan> {\n        Optional&lt;FileMetadata&gt; existingFile = metadataRepository.findByFileHash(fileHash);\n        \n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (existingFile.isPresent()) {\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> ChunkedUploadResponse.instant(existingFile.get());\n        }\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 检查已上传的分片\u003C\u002Fspan>\n        List&lt;Integer&gt; uploadedChunks = findUploadedChunks(fileHash);\n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> ChunkedUploadResponse.continueUpload(uploadedChunks);\n    }\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 上传分片\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">void\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">uploadChunk\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String fileHash, \u003Cspan class=\"hljs-type\">int\u003C\u002Fspan> chunkIndex, \n                           MultipartFile chunkFile)\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">throws\u003C\u002Fspan> IOException {\n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 存储分片文件\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">chunkFileName\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> String.format(\u003Cspan class=\"hljs-string\">&quot;%s_%d&quot;\u003C\u002Fspan>, fileHash, chunkIndex);\n        \u003Cspan class=\"hljs-type\">Path\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">chunkPath\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> chunkStorageLocation.resolve(chunkFileName);\n        Files.copy(chunkFile.getInputStream(), chunkPath, \n                  StandardCopyOption.REPLACE_EXISTING);\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 记录分片上传状态\u003C\u002Fspan>\n        recordChunkUpload(fileHash, chunkIndex);\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch3 data-line=\"570\" id=\"5.2 分布式环境下的秒传\">5.2 分布式环境下的秒传\u003C\u002Fh3>\n\u003Cp data-line=\"571\">在分布式环境中，需要使用分布式缓存来存储文件哈希映射：\u003C\u002Fp>\n\n        \u003Cdetails  data-line=\"573\" class=\"md-editor-code\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@Service\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">DistributedFileService\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-meta\">@Autowired\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> RedisTemplate&lt;String, String&gt; redisTemplate;\n    \n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">FILE_HASH_KEY\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;file:hash:&quot;\u003C\u002Fspan>;\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">static\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">final\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">long\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">CACHE_EXPIRE_HOURS\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> \u003Cspan class=\"hljs-number\">24\u003C\u002Fspan>;\n    \n    \u003Cspan class=\"hljs-comment\">\u002F**\n     * 分布式秒传检查\n     *\u002F\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">public\u003C\u002Fspan> \u003Cspan class=\"hljs-type\">boolean\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">checkFileExistsDistributed\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">(String fileHash)\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 先查缓存\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">cachedPath\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> redisTemplate.opsForValue().get(FILE_HASH_KEY + fileHash);\n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (cachedPath != \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>) {\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>;\n        }\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 查数据库\u003C\u002Fspan>\n        Optional&lt;FileMetadata&gt; metadata = metadataRepository.findByFileHash(fileHash);\n        \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (metadata.isPresent()) {\n            \u003Cspan class=\"hljs-comment\">\u002F\u002F 写入缓存\u003C\u002Fspan>\n            redisTemplate.opsForValue().set(FILE_HASH_KEY + fileHash, \n                metadata.get().getFilePath(), \n                Duration.ofHours(CACHE_EXPIRE_HOURS));\n            \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-literal\">true\u003C\u002Fspan>;\n        }\n        \n        \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-literal\">false\u003C\u002Fspan>;\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch2 data-line=\"608\" id=\"6. 测试与验证\">6. 测试与验证\u003C\u002Fh2>\n\u003Ch3 data-line=\"610\" id=\"6.1 单元测试\">6.1 单元测试\u003C\u002Fh3>\n\n        \u003Cdetails  data-line=\"611\" class=\"md-editor-code\" open=\"\">\n          \u003Csummary class=\"md-editor-code-head\">\n            \u003Cdiv class=\"md-editor-code-flag\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fdiv>\n            \u003Cdiv class=\"md-editor-code-action\">\n              \u003Cspan class=\"md-editor-code-lang\">java\u003C\u002Fspan>\n              \u003Cspan class=\"md-editor-copy-button\" data-tips=\"复制代码\">复制代码\u003C\u002Fspan>\n              \n              \u003Cspan class=\"md-editor-collapse-tips\">\u003Csvg xmlns=\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-chevron-left md-editor-icon\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m14 16-4-4 4-4\"\u002F>\u003C\u002Fsvg>\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          \u003C\u002Fsummary>\n          \u003Cpre>\u003Ccode class=\"language-java\" language=java>\u003Cspan class=\"md-editor-code-block\">\u003Cspan class=\"hljs-meta\">@SpringBootTest\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">FileUploadServiceTest\u003C\u002Fspan> {\n    \n    \u003Cspan class=\"hljs-meta\">@Autowired\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> FileStorageService fileStorageService;\n    \n    \u003Cspan class=\"hljs-meta\">@Test\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">void\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">testInstantUpload\u003C\u002Fspan>\u003Cspan class=\"hljs-params\">()\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">throws\u003C\u002Fspan> IOException {\n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 准备测试文件\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">MultipartFile\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">file\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> createTestFile(\u003Cspan class=\"hljs-string\">&quot;test.txt&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-string\">&quot;Hello World&quot;\u003C\u002Fspan>);\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 第一次上传\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">UploadResponse\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">response1\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageService.storeFile(file, \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>);\n        assertTrue(response1.isSuccess());\n        assertFalse(response1.isExists());\n        \n        \u003Cspan class=\"hljs-comment\">\u002F\u002F 第二次上传（应该秒传）\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-type\">String\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">fileHash\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> response1.getFileMetadata().getFileHash();\n        \u003Cspan class=\"hljs-type\">UploadResponse\u003C\u002Fspan> \u003Cspan class=\"hljs-variable\">response2\u003C\u002Fspan> \u003Cspan class=\"hljs-operator\">=\u003C\u002Fspan> fileStorageService.checkFileExists(\n            fileHash, \u003Cspan class=\"hljs-string\">&quot;test.txt&quot;\u003C\u002Fspan>, file.getSize());\n        \n        assertTrue(response2.isSuccess());\n        assertTrue(response2.isExists());\n    }\n}\u003C\u002Fspan>\u003Cspan rn-wrapper aria-hidden=\"true\">\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\n        \u003C\u002Fdetails>\n      \u003Ch2 data-line=\"639\" id=\"7. 总结\">7. 总结\u003C\u002Fh2>\n\u003Cp data-line=\"641\">文件秒传功能通过文件内容哈希比对，有效避免了重复文件的传输，具有以下优势：\u003C\u002Fp>\n\u003Col data-line=\"643\">\n\u003Cli data-line=\"643\">\u003Cstrong>节省带宽\u003C\u002Fstrong>：减少重复文件上传的网络消耗\u003C\u002Fli>\n\u003Cli data-line=\"644\">\u003Cstrong>提升用户体验\u003C\u002Fstrong>：大幅缩短上传时间\u003C\u002Fli>\n\u003Cli data-line=\"645\">\u003Cstrong>节省存储空间\u003C\u002Fstrong>：相同文件只存储一份\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp data-line=\"647\">在实际应用中，还需要考虑：\u003C\u002Fp>\n\u003Cul data-line=\"648\">\n\u003Cli data-line=\"648\">哈希碰撞的概率（虽然极低）\u003C\u002Fli>\n\u003Cli data-line=\"649\">大文件哈希计算的性能优化\u003C\u002Fli>\n\u003Cli data-line=\"650\">分布式环境下的并发处理\u003C\u002Fli>\n\u003Cli data-line=\"651\">安全性和权限控制\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp data-line=\"653\">通过本文的实现，你可以快速为Spring Boot应用添加文件秒传功能，并根据实际需求进行扩展和优化。\u003C\u002Fp>\n",19092,63,"0","",2,0,10,"文件,&quot,传,fileHash,String","1. 文件秒传技术原理\n1.1 什么是文件秒传\n文件秒传是指当用户上传一个文件时，如果服务器上已经存在相同的文件，则无需再次上传文件内容，直接返回上传成功。这种技术可以显著减少网络传输量，提升用户体验。\n1.","2025-12-20 23:48:50"]