Selaa lähdekoodia

feat: 补充认证和文档管理缺失接口

认证服务 (auth-service):
- PUT /auth/profile: 用户信息修改(用户名、邮箱、头像)
- PUT /auth/password: 密码修改(验证旧密码、新密码确认)

文档管理服务 (document-service):
- PUT /api/v1/documents/{id}: 文档更新(名称、状态、元数据)
- DELETE /api/v1/documents/{id}: 级联删除增强
  - 删除顺序: 向量嵌入 → 文本分块 → 图关系 → 图节点
            → 结构化元素 → 解析任务 → 文档记录
  - 同时清理文本文件和图片目录
- POST /api/v1/documents/batch-delete: 批量删除

更新进度报告,完善 API 接口清单
何文松 1 kuukausi sitten
vanhempi
commit
a8ac55ca2a

+ 109 - 0
backend/auth-service/src/main/java/com/lingyue/auth/controller/AuthController.java

@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -124,6 +126,85 @@ public class AuthController {
         }
     }
     
+    /**
+     * 更新用户资料
+     */
+    @PutMapping("/profile")
+    @Operation(summary = "更新用户资料", description = "更新当前登录用户的个人信息")
+    public AjaxResult<?> updateProfile(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @Valid @RequestBody UpdateProfileRequest request) {
+        
+        String userId = extractUserIdFromToken(authorization);
+        if (userId == null) {
+            return AjaxResult.error("未提供有效的认证Token");
+        }
+        
+        try {
+            User updatedUser = userService.updateProfile(
+                    userId,
+                    request.getUsername(),
+                    request.getEmail(),
+                    request.getAvatarUrl()
+            );
+            
+            // 构建响应(不返回敏感信息)
+            UserInfoResponse response = new UserInfoResponse();
+            response.setId(updatedUser.getId());
+            response.setUsername(updatedUser.getUsername());
+            response.setEmail(updatedUser.getEmail());
+            response.setAvatarUrl(updatedUser.getAvatarUrl());
+            response.setRole(updatedUser.getRole());
+            response.setLastLoginAt(updatedUser.getLastLoginAt());
+            response.setCreateTime(updatedUser.getCreateTime());
+            
+            return AjaxResult.success("资料更新成功", response);
+        } catch (Exception e) {
+            log.error("更新用户资料失败: userId={}, error={}", userId, e.getMessage());
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+    
+    /**
+     * 修改密码
+     */
+    @PutMapping("/password")
+    @Operation(summary = "修改密码", description = "修改当前登录用户的密码")
+    public AjaxResult<?> changePassword(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @Valid @RequestBody ChangePasswordRequest request) {
+        
+        String userId = extractUserIdFromToken(authorization);
+        if (userId == null) {
+            return AjaxResult.error("未提供有效的认证Token");
+        }
+        
+        try {
+            authService.changePassword(userId, request.getOldPassword(), 
+                    request.getNewPassword(), request.getConfirmPassword());
+            return AjaxResult.success("密码修改成功");
+        } catch (Exception e) {
+            log.error("修改密码失败: userId={}, error={}", userId, e.getMessage());
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+    
+    /**
+     * 从Token中提取用户ID
+     */
+    private String extractUserIdFromToken(String authorization) {
+        if (authorization == null || !authorization.startsWith("Bearer ")) {
+            return null;
+        }
+        String token = authorization.substring(7);
+        try {
+            return JwtUtil.getUserIdFromToken(token, jwtSecret);
+        } catch (Exception e) {
+            log.error("解析Token失败: {}", e.getMessage());
+            return null;
+        }
+    }
+    
     /**
      * 获取客户端IP地址
      */
@@ -139,6 +220,8 @@ public class AuthController {
         return request.getRemoteAddr();
     }
     
+    // ==================== 请求/响应 DTO ====================
+    
     /**
      * 刷新Token请求DTO
      */
@@ -162,5 +245,31 @@ public class AuthController {
         private java.util.Date lastLoginAt;
         private java.util.Date createTime;
     }
+    
+    /**
+     * 更新资料请求DTO
+     */
+    @Data
+    public static class UpdateProfileRequest {
+        private String username;  // 可选,修改用户名
+        private String email;     // 可选,修改邮箱
+        private String avatarUrl; // 可选,修改头像
+    }
+    
+    /**
+     * 修改密码请求DTO
+     */
+    @Data
+    public static class ChangePasswordRequest {
+        @NotBlank(message = "旧密码不能为空")
+        private String oldPassword;
+        
+        @NotBlank(message = "新密码不能为空")
+        @Size(min = 6, message = "新密码长度不能少于6位")
+        private String newPassword;
+        
+        @NotBlank(message = "确认密码不能为空")
+        private String confirmPassword;
+    }
 }
 

+ 41 - 1
backend/auth-service/src/main/java/com/lingyue/auth/service/AuthService.java

@@ -19,7 +19,6 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
-import java.util.UUID;
 
 /**
  * 认证服务
@@ -162,6 +161,47 @@ public class AuthService {
         return createAuthResponse(user);
     }
     
+    /**
+     * 修改密码
+     * 
+     * @param userId 用户ID
+     * @param oldPassword 旧密码
+     * @param newPassword 新密码
+     * @param confirmPassword 确认密码
+     */
+    @Transactional
+    public void changePassword(String userId, String oldPassword, String newPassword, String confirmPassword) {
+        // 获取用户
+        User user = userService.getUserById(userId);
+        if (user == null) {
+            throw new ServiceException("用户不存在", HttpStatus.NOT_FOUND);
+        }
+        
+        // 验证旧密码
+        if (!PasswordUtil.matches(oldPassword, user.getPasswordHash())) {
+            throw new ServiceException("旧密码错误", HttpStatus.BAD_REQUEST);
+        }
+        
+        // 验证新密码与确认密码一致
+        if (!newPassword.equals(confirmPassword)) {
+            throw new ServiceException("新密码与确认密码不一致", HttpStatus.BAD_REQUEST);
+        }
+        
+        // 验证新密码不能与旧密码相同
+        if (PasswordUtil.matches(newPassword, user.getPasswordHash())) {
+            throw new ServiceException("新密码不能与旧密码相同", HttpStatus.BAD_REQUEST);
+        }
+        
+        // 更新密码
+        String newPasswordHash = PasswordUtil.encode(newPassword);
+        userService.updatePasswordHash(userId, newPasswordHash);
+        
+        // 可选:清除该用户的其他会话(强制其他设备重新登录)
+        // clearOtherSessions(userId, currentSessionId);
+        
+        log.info("用户密码修改成功: userId={}", userId);
+    }
+    
     /**
      * 创建认证响应(包含Token和会话)
      */

+ 75 - 0
backend/auth-service/src/main/java/com/lingyue/auth/service/UserService.java

@@ -3,12 +3,19 @@ package com.lingyue.auth.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.lingyue.auth.entity.User;
 import com.lingyue.auth.repository.UserRepository;
+import com.lingyue.common.exception.ServiceException;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.Date;
 
 /**
  * 用户服务
  */
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class UserService {
@@ -39,5 +46,73 @@ public class UserService {
         wrapper.eq(User::getEmail, email);
         return userRepository.selectOne(wrapper);
     }
+    
+    /**
+     * 更新用户资料
+     * 
+     * @param userId 用户ID
+     * @param username 新用户名(可选)
+     * @param email 新邮箱(可选)
+     * @param avatarUrl 新头像URL(可选)
+     * @return 更新后的用户
+     */
+    @Transactional
+    public User updateProfile(String userId, String username, String email, String avatarUrl) {
+        User user = userRepository.selectById(userId);
+        if (user == null) {
+            throw new ServiceException("用户不存在", 404);
+        }
+        
+        // 校验用户名唯一性
+        if (StringUtils.hasText(username) && !username.equals(user.getUsername())) {
+            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(User::getUsername, username);
+            if (userRepository.selectCount(wrapper) > 0) {
+                throw new ServiceException("用户名已被使用", 422);
+            }
+            user.setUsername(username);
+        }
+        
+        // 校验邮箱唯一性
+        if (StringUtils.hasText(email) && !email.equals(user.getEmail())) {
+            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(User::getEmail, email);
+            if (userRepository.selectCount(wrapper) > 0) {
+                throw new ServiceException("邮箱已被使用", 422);
+            }
+            user.setEmail(email);
+        }
+        
+        // 更新头像
+        if (avatarUrl != null) {
+            user.setAvatarUrl(avatarUrl);
+        }
+        
+        user.setUpdateTime(new Date());
+        userRepository.updateById(user);
+        
+        log.info("用户资料更新成功: userId={}", userId);
+        return user;
+    }
+    
+    /**
+     * 更新用户密码哈希
+     * 
+     * @param userId 用户ID
+     * @param newPasswordHash 新密码哈希
+     */
+    @Transactional
+    public void updatePasswordHash(String userId, String newPasswordHash) {
+        User user = userRepository.selectById(userId);
+        if (user == null) {
+            throw new ServiceException("用户不存在", 404);
+        }
+        
+        user.setPasswordHash(newPasswordHash);
+        user.setUpdateTime(new Date());
+        userRepository.updateById(user);
+        
+        log.info("用户密码更新成功: userId={}", userId);
+    }
 }
 

+ 85 - 4
backend/document-service/src/main/java/com/lingyue/document/controller/DocumentController.java

@@ -10,11 +10,14 @@ import com.lingyue.common.domain.AjaxResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -130,16 +133,37 @@ public class DocumentController {
     }
     
     /**
-     * 删除文档
+     * 更新文档信息
+     */
+    @PutMapping("/{documentId}")
+    @Operation(summary = "更新文档", description = "更新文档的名称、状态等信息")
+    public AjaxResult<?> updateDocument(
+            @Parameter(description = "文档ID", required = true)
+            @PathVariable String documentId,
+            @Valid @RequestBody UpdateDocumentRequest request) {
+        
+        try {
+            Document document = documentService.updateDocument(documentId, request.getName(), 
+                    request.getStatus(), request.getMetadata());
+            log.info("更新文档成功: documentId={}", documentId);
+            return AjaxResult.success("更新成功", document);
+        } catch (Exception e) {
+            log.error("更新文档失败: documentId={}", documentId, e);
+            return AjaxResult.error("更新文档失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除文档(级联删除所有关联数据)
      */
     @DeleteMapping("/{documentId}")
-    @Operation(summary = "删除文档", description = "删除指定文档及其关联数据")
+    @Operation(summary = "删除文档", description = "删除指定文档及其所有关联数据(图节点、向量、结构化元素等)")
     public AjaxResult<?> deleteDocument(
             @Parameter(description = "文档ID", required = true) 
             @PathVariable String documentId) {
         
         try {
-            documentService.deleteDocument(documentId);
+            documentService.deleteDocumentCascade(documentId);
             log.info("删除文档成功: documentId={}", documentId);
             return AjaxResult.success("删除成功");
         } catch (Exception e) {
@@ -148,6 +172,42 @@ public class DocumentController {
         }
     }
     
+    /**
+     * 批量删除文档
+     */
+    @PostMapping("/batch-delete")
+    @Operation(summary = "批量删除文档", description = "批量删除多个文档及其关联数据")
+    public AjaxResult<?> batchDeleteDocuments(
+            @Valid @RequestBody BatchDeleteRequest request) {
+        
+        List<String> successIds = new ArrayList<>();
+        List<String> failedIds = new ArrayList<>();
+        
+        for (String documentId : request.getDocumentIds()) {
+            try {
+                documentService.deleteDocumentCascade(documentId);
+                successIds.add(documentId);
+            } catch (Exception e) {
+                log.error("批量删除文档失败: documentId={}, error={}", documentId, e.getMessage());
+                failedIds.add(documentId);
+            }
+        }
+        
+        BatchDeleteResponse response = new BatchDeleteResponse();
+        response.setSuccessIds(successIds);
+        response.setFailedIds(failedIds);
+        response.setSuccessCount(successIds.size());
+        response.setFailedCount(failedIds.size());
+        
+        if (failedIds.isEmpty()) {
+            return AjaxResult.success("批量删除成功", response);
+        } else if (successIds.isEmpty()) {
+            return AjaxResult.error("批量删除全部失败", response);
+        } else {
+            return AjaxResult.success("批量删除部分成功", response);
+        }
+    }
+    
     /**
      * 获取文档的结构化内容
      * 包含段落、图片、表格,按原始顺序排列
@@ -197,7 +257,7 @@ public class DocumentController {
         return AjaxResult.success(tables);
     }
     
-    // ==================== 响应 DTO ====================
+    // ==================== 请求/响应 DTO ====================
     
     @Data
     public static class DocumentTextResponse {
@@ -222,4 +282,25 @@ public class DocumentController {
         private List<DocumentElement> elements;
         private Map<String, Object> stats;
     }
+    
+    @Data
+    public static class UpdateDocumentRequest {
+        private String name;      // 文档名称
+        private String status;    // 状态(可选)
+        private Object metadata;  // 元数据(可选)
+    }
+    
+    @Data
+    public static class BatchDeleteRequest {
+        @NotEmpty(message = "文档ID列表不能为空")
+        private List<String> documentIds;
+    }
+    
+    @Data
+    public static class BatchDeleteResponse {
+        private List<String> successIds;
+        private List<String> failedIds;
+        private int successCount;
+        private int failedCount;
+    }
 }

+ 256 - 2
backend/document-service/src/main/java/com/lingyue/document/service/DocumentService.java

@@ -3,17 +3,27 @@ package com.lingyue.document.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.lingyue.common.exception.ServiceException;
 import com.lingyue.document.entity.Document;
+import com.lingyue.document.repository.DocumentElementRepository;
 import com.lingyue.document.repository.DocumentRepository;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Date;
 
 /**
  * 文档服务
@@ -27,10 +37,17 @@ import java.nio.file.Path;
 public class DocumentService {
     
     private final DocumentRepository documentRepository;
+    private final DocumentElementRepository documentElementRepository;
+    
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
     
     @Value("${file.storage.text-path:/data/lingyue/texts}")
     private String textStoragePath;
     
+    @Value("${file.storage.base-path:/data/lingyue/files}")
+    private String fileStoragePath;
+    
     /**
      * 根据ID获取文档
      */
@@ -121,12 +138,249 @@ public class DocumentService {
     }
     
     /**
-     * 删除文档
+     * 更新文档信息
+     * 
+     * @param documentId 文档ID
+     * @param name 文档名称(可选)
+     * @param status 状态(可选)
+     * @param metadata 元数据(可选)
+     * @return 更新后的文档
+     */
+    @Transactional
+    public Document updateDocument(String documentId, String name, String status, Object metadata) {
+        Document document = documentRepository.selectById(documentId);
+        if (document == null) {
+            throw new ServiceException("文档不存在: " + documentId, 404);
+        }
+        
+        if (StringUtils.hasText(name)) {
+            document.setName(name);
+        }
+        if (StringUtils.hasText(status)) {
+            document.setStatus(status);
+        }
+        if (metadata != null) {
+            document.setMetadata(metadata);
+        }
+        
+        document.setUpdateTime(new Date());
+        documentRepository.updateById(document);
+        
+        log.info("更新文档: documentId={}, name={}", documentId, name);
+        return document;
+    }
+    
+    /**
+     * 删除文档(简单删除,不级联)
+     * @deprecated 使用 {@link #deleteDocumentCascade(String)} 代替
      */
+    @Deprecated
     public void deleteDocument(String documentId) {
         documentRepository.deleteById(documentId);
         log.info("删除文档: documentId={}", documentId);
-        // TODO: 同时删除关联的文本文件、图节点等
+    }
+    
+    /**
+     * 级联删除文档及其所有关联数据
+     * 
+     * 删除顺序(遵循外键约束):
+     * 1. vector_embeddings (通过 chunk_id 关联)
+     * 2. text_chunks (document_id)
+     * 3. graph_relations (通过 node_id 关联)
+     * 4. graph_nodes (document_id)
+     * 5. document_elements (document_id)
+     * 6. parse_tasks (document_id)
+     * 7. documents (主表)
+     * 8. 文本文件
+     * 9. 图片目录
+     * 
+     * @param documentId 文档ID
+     */
+    @Transactional
+    public void deleteDocumentCascade(String documentId) {
+        Document document = documentRepository.selectById(documentId);
+        if (document == null) {
+            throw new ServiceException("文档不存在: " + documentId, 404);
+        }
+        
+        String userId = document.getUserId();
+        log.info("开始级联删除文档: documentId={}, userId={}", documentId, userId);
+        
+        // 1. 删除向量嵌入(通过 text_chunks 关联)
+        int vectorCount = deleteVectorEmbeddingsByDocumentId(documentId);
+        log.debug("删除向量嵌入: count={}", vectorCount);
+        
+        // 2. 删除文本分块
+        int chunkCount = deleteTextChunksByDocumentId(documentId);
+        log.debug("删除文本分块: count={}", chunkCount);
+        
+        // 3. 删除图关系(通过 graph_nodes 关联)
+        int relationCount = deleteGraphRelationsByDocumentId(documentId);
+        log.debug("删除图关系: count={}", relationCount);
+        
+        // 4. 删除图节点
+        int nodeCount = deleteGraphNodesByDocumentId(documentId);
+        log.debug("删除图节点: count={}", nodeCount);
+        
+        // 5. 删除结构化元素
+        int elementCount = documentElementRepository.deleteByDocumentId(documentId);
+        log.debug("删除结构化元素: count={}", elementCount);
+        
+        // 6. 删除解析任务
+        int taskCount = deleteParseTasksByDocumentId(documentId);
+        log.debug("删除解析任务: count={}", taskCount);
+        
+        // 7. 删除文档记录
+        documentRepository.deleteById(documentId);
+        log.debug("删除文档记录");
+        
+        // 8. 删除文本文件(不影响事务)
+        deleteTextFile(documentId);
+        
+        // 9. 删除图片目录(不影响事务)
+        deleteImageDirectory(userId, documentId);
+        
+        log.info("级联删除文档完成: documentId={}, 删除向量={}, 分块={}, 关系={}, 节点={}, 元素={}, 任务={}",
+                documentId, vectorCount, chunkCount, relationCount, nodeCount, elementCount, taskCount);
+    }
+    
+    /**
+     * 删除向量嵌入
+     */
+    private int deleteVectorEmbeddingsByDocumentId(String documentId) {
+        if (jdbcTemplate == null) {
+            log.warn("JdbcTemplate 未注入,跳过向量嵌入删除");
+            return 0;
+        }
+        try {
+            String sql = """
+                DELETE FROM vector_embeddings 
+                WHERE chunk_id IN (
+                    SELECT id FROM text_chunks WHERE document_id = ?
+                )
+                """;
+            return jdbcTemplate.update(sql, documentId);
+        } catch (Exception e) {
+            log.warn("删除向量嵌入失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+    
+    /**
+     * 删除文本分块
+     */
+    private int deleteTextChunksByDocumentId(String documentId) {
+        if (jdbcTemplate == null) {
+            log.warn("JdbcTemplate 未注入,跳过文本分块删除");
+            return 0;
+        }
+        try {
+            return jdbcTemplate.update("DELETE FROM text_chunks WHERE document_id = ?", documentId);
+        } catch (Exception e) {
+            log.warn("删除文本分块失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+    
+    /**
+     * 删除图关系(通过节点关联)
+     */
+    private int deleteGraphRelationsByDocumentId(String documentId) {
+        if (jdbcTemplate == null) {
+            log.warn("JdbcTemplate 未注入,跳过图关系删除");
+            return 0;
+        }
+        try {
+            String sql = """
+                DELETE FROM graph_relations 
+                WHERE from_node_id IN (
+                    SELECT id FROM graph_nodes WHERE document_id = ?
+                )
+                OR to_node_id IN (
+                    SELECT id FROM graph_nodes WHERE document_id = ?
+                )
+                """;
+            return jdbcTemplate.update(sql, documentId, documentId);
+        } catch (Exception e) {
+            log.warn("删除图关系失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+    
+    /**
+     * 删除图节点
+     */
+    private int deleteGraphNodesByDocumentId(String documentId) {
+        if (jdbcTemplate == null) {
+            log.warn("JdbcTemplate 未注入,跳过图节点删除");
+            return 0;
+        }
+        try {
+            return jdbcTemplate.update("DELETE FROM graph_nodes WHERE document_id = ?", documentId);
+        } catch (Exception e) {
+            log.warn("删除图节点失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+    
+    /**
+     * 删除解析任务
+     */
+    private int deleteParseTasksByDocumentId(String documentId) {
+        if (jdbcTemplate == null) {
+            log.warn("JdbcTemplate 未注入,跳过解析任务删除");
+            return 0;
+        }
+        try {
+            return jdbcTemplate.update("DELETE FROM parse_tasks WHERE document_id = ?", documentId);
+        } catch (Exception e) {
+            log.warn("删除解析任务失败: {}", e.getMessage());
+            return 0;
+        }
+    }
+    
+    /**
+     * 删除文本文件
+     */
+    private void deleteTextFile(String documentId) {
+        try {
+            String subDir = documentId.substring(0, 2);
+            Path textFilePath = Path.of(textStoragePath, subDir, documentId + ".txt");
+            if (Files.exists(textFilePath)) {
+                Files.delete(textFilePath);
+                log.debug("删除文本文件: {}", textFilePath);
+            }
+        } catch (Exception e) {
+            log.warn("删除文本文件失败: documentId={}, error={}", documentId, e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除图片目录
+     */
+    private void deleteImageDirectory(String userId, String documentId) {
+        try {
+            Path imageDir = Path.of(fileStoragePath, userId, documentId);
+            if (Files.exists(imageDir) && Files.isDirectory(imageDir)) {
+                // 递归删除目录及其内容
+                Files.walkFileTree(imageDir, new SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                        Files.delete(file);
+                        return FileVisitResult.CONTINUE;
+                    }
+                    
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
+                log.debug("删除图片目录: {}", imageDir);
+            }
+        } catch (Exception e) {
+            log.warn("删除图片目录失败: documentId={}, error={}", documentId, e.getMessage());
+        }
     }
     
     /**

+ 43 - 2
进度报告.md

@@ -13,7 +13,7 @@
 - **数据库重建脚本** → ✅ 完整的 `rebuild_all.sh` 脚本
 
 ### 核心模块现状
-- 📁 文档管理、解析、认证 → 框架完成
+- 📁 **文档管理、解析、认证** → ✅ 功能完善
 - 🤖 AI服务、图谱服务 → 数据层完成,RAG 功能已实现
 - 🔍 **RAG 向量化存储** → ✅ 已完成
 - 🏷️ **NER 实体识别服务** → ✅ 已完成并测试验证
@@ -21,7 +21,21 @@
 - 📦 **数据源管理** → ✅ 已完成(CRUD + 取值 + 聚合)
 - ⏱️ **任务中心** → ✅ 已完成(多阶段进度跟踪)
 
-### 新增功能(2026-01-22)✅ 一键上传全自动处理 + 数据源管理
+### 新增功能(2026-01-22 下午)✅ 补充缺失接口
+
+- ✅ **认证服务接口完善**
+  - `PUT /auth/profile` - 用户信息修改(用户名、邮箱、头像)
+  - `PUT /auth/password` - 密码修改(验证旧密码、新密码确认)
+  
+- ✅ **文档管理接口完善**
+  - `PUT /api/v1/documents/{id}` - 文档更新(名称、状态、元数据)
+  - `DELETE /api/v1/documents/{id}` - **级联删除增强**
+    - 删除向量嵌入、文本分块、图关系、图节点
+    - 删除结构化元素、解析任务、文档记录
+    - 删除文本文件和图片目录
+  - `POST /api/v1/documents/batch-delete` - 批量删除(返回成功/失败列表)
+
+### 新增功能(2026-01-22 上午)✅ 一键上传全自动处理 + 数据源管理
 
 - ✅ **一键上传全自动处理流程**
   - 上传接口:`POST /api/v1/parse/upload`(唯一入口)
@@ -299,6 +313,33 @@ database/
 
 ## 📋 API 接口清单
 
+### 认证服务(auth-service)
+
+| 接口 | 方法 | 说明 | 状态 |
+|------|------|------|------|
+| `/auth/register` | POST | 用户注册 | ✅ |
+| `/auth/login` | POST | 用户登录 | ✅ |
+| `/auth/logout` | POST | 用户登出 | ✅ |
+| `/auth/refresh` | POST | 刷新Token | ✅ |
+| `/auth/me` | GET | 获取当前用户 | ✅ |
+| `/auth/profile` | PUT | **更新用户资料** | ✅ |
+| `/auth/password` | PUT | **修改密码** | ✅ |
+
+### 文档管理服务(document-service)
+
+| 接口 | 方法 | 说明 | 状态 |
+|------|------|------|------|
+| `/api/v1/documents` | GET | 文档列表(分页) | ✅ |
+| `/api/v1/documents/{id}` | GET | 文档详情 | ✅ |
+| `/api/v1/documents/{id}` | PUT | **更新文档** | ✅ |
+| `/api/v1/documents/{id}` | DELETE | **级联删除** | ✅ |
+| `/api/v1/documents/batch-delete` | POST | **批量删除** | ✅ |
+| `/api/v1/documents/{id}/text` | GET | 获取文档文本 | ✅ |
+| `/api/v1/documents/{id}/parse-status` | GET | 解析状态 | ✅ |
+| `/api/v1/documents/{id}/elements` | GET | 结构化元素 | ✅ |
+| `/api/v1/documents/{id}/images` | GET | 图片列表 | ✅ |
+| `/api/v1/documents/{id}/tables` | GET | 表格列表 | ✅ |
+
 ### 文件上传(唯一入口)
 
 | 接口 | 方法 | 说明 | 状态 |