Browse Source

fix(模板编辑): 修复模板编辑器无法显示文档内容的问题

问题:文档解析和NER完成后,模板编辑页面显示空白
原因:
1. 前端尝试读取 template.content 字段(不存在)
2. NER完成后生成的DocumentBlock没有保存到数据库

修复:
- 前端添加 documentApi 接口,根据 baseDocumentId 获取结构化文档
- 后端添加批量保存 blocks 的接口 POST /documents/{id}/blocks/batch
- NER完成后自动生成并保存 DocumentBlock 到数据库
何文松 1 tháng trước cách đây
mục cha
commit
08594c6776

+ 26 - 0
backend/document-service/src/main/java/com/lingyue/document/controller/StructuredDocumentController.java

@@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 结构化文档控制器(参考飞书设计)
@@ -181,8 +182,33 @@ public class StructuredDocumentController {
         return AjaxResult.success("确认成功");
     }
     
+    // ==================== 批量操作 ====================
+    
+    /**
+     * 批量保存文档块(用于 NER 完成后生成结构化文档)
+     */
+    @PostMapping("/{documentId}/blocks/batch")
+    @Operation(summary = "批量保存块", description = "批量保存文档块,用于 NER 完成后生成结构化文档")
+    public AjaxResult<?> saveBlocksBatch(
+            @PathVariable String documentId,
+            @RequestBody SaveBlocksBatchRequest request) {
+        
+        try {
+            int savedCount = structuredDocumentService.saveBlocksBatch(documentId, request.getBlocks());
+            return AjaxResult.success("保存成功", savedCount);
+        } catch (Exception e) {
+            log.error("批量保存块失败: documentId={}", documentId, e);
+            return AjaxResult.error("保存失败: " + e.getMessage());
+        }
+    }
+    
     // ==================== 请求 DTO ====================
     
+    @Data
+    public static class SaveBlocksBatchRequest {
+        private List<Map<String, Object>> blocks;
+    }
+    
     @Data
     public static class UpdateElementsRequest {
         private List<TextElement> elements;

+ 96 - 0
backend/document-service/src/main/java/com/lingyue/document/service/StructuredDocumentService.java

@@ -394,4 +394,100 @@ public class StructuredDocumentService {
         blockRepository.deleteById(blockId);
         log.info("删除块: blockId={}", blockId);
     }
+    
+    // ==================== 批量操作 ====================
+    
+    /**
+     * 批量保存文档块(用于 NER 完成后生成结构化文档)
+     * 
+     * @param documentId 文档ID
+     * @param blocks 块列表(来自 DocumentBlockGeneratorService)
+     * @return 保存的块数量
+     */
+    @Transactional
+    public int saveBlocksBatch(String documentId, List<Map<String, Object>> blocks) {
+        if (blocks == null || blocks.isEmpty()) {
+            log.warn("批量保存块: 块列表为空, documentId={}", documentId);
+            return 0;
+        }
+        
+        // 先删除该文档的旧块
+        int deleted = blockRepository.deleteByDocumentId(documentId);
+        if (deleted > 0) {
+            log.info("删除旧块: documentId={}, count={}", documentId, deleted);
+        }
+        
+        // 保存新块
+        int savedCount = 0;
+        for (Map<String, Object> blockMap : blocks) {
+            try {
+                DocumentBlock block = convertMapToBlock(blockMap);
+                block.setDocumentId(documentId);
+                block.setCreateTime(new Date());
+                block.setUpdateTime(new Date());
+                
+                blockRepository.insert(block);
+                savedCount++;
+            } catch (Exception e) {
+                log.error("保存块失败: documentId={}, block={}, error={}", 
+                        documentId, blockMap, e.getMessage());
+            }
+        }
+        
+        log.info("批量保存块完成: documentId={}, savedCount={}", documentId, savedCount);
+        return savedCount;
+    }
+    
+    /**
+     * 将 Map 转换为 DocumentBlock
+     */
+    @SuppressWarnings("unchecked")
+    private DocumentBlock convertMapToBlock(Map<String, Object> blockMap) {
+        DocumentBlock block = new DocumentBlock();
+        
+        block.setId((String) blockMap.get("blockId"));
+        block.setDocumentId((String) blockMap.get("documentId"));
+        block.setParentId((String) blockMap.get("parentId"));
+        block.setBlockIndex(getIntValue(blockMap, "blockIndex", 0));
+        block.setBlockType((String) blockMap.get("blockType"));
+        
+        // 转换 children
+        Object childrenObj = blockMap.get("children");
+        if (childrenObj instanceof List) {
+            block.setChildren((List<String>) childrenObj);
+        }
+        
+        // 转换 elements
+        Object elementsObj = blockMap.get("elements");
+        if (elementsObj instanceof List) {
+            List<Map<String, Object>> elementMaps = (List<Map<String, Object>>) elementsObj;
+            List<TextElement> elements = new ArrayList<>();
+            
+            for (Map<String, Object> elMap : elementMaps) {
+                TextElement el = new TextElement();
+                el.setType((String) elMap.get("type"));
+                el.setContent((String) elMap.get("content"));
+                el.setEntityId((String) elMap.get("entityId"));
+                el.setEntityText((String) elMap.get("entityText"));
+                el.setEntityType((String) elMap.get("entityType"));
+                el.setConfirmed((Boolean) elMap.get("confirmed"));
+                el.setUrl((String) elMap.get("url"));
+                el.setRefDocId((String) elMap.get("refDocId"));
+                el.setRefDocTitle((String) elMap.get("refDocTitle"));
+                elements.add(el);
+            }
+            
+            block.setElements(elements);
+        }
+        
+        return block;
+    }
+    
+    private int getIntValue(Map<String, Object> map, String key, int defaultValue) {
+        Object value = map.get(key);
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return defaultValue;
+    }
 }

+ 79 - 4
backend/graph-service/src/main/java/com/lingyue/graph/listener/DocumentParsedEventListener.java

@@ -3,6 +3,8 @@ package com.lingyue.graph.listener;
 import com.lingyue.common.event.DocumentParsedEvent;
 import com.lingyue.document.entity.Document;
 import com.lingyue.document.repository.DocumentRepository;
+import com.lingyue.graph.service.DocumentBlockGeneratorService;
+import com.lingyue.graph.service.DocumentBlockGeneratorService.BlockDTO;
 import com.lingyue.graph.service.GraphNerService;
 import com.lingyue.graph.service.NerToBlockService;
 import com.lingyue.graph.service.NerToBlockService.TextElementDTO;
@@ -36,6 +38,7 @@ public class DocumentParsedEventListener {
 
     private final GraphNerService graphNerService;
     private final NerToBlockService nerToBlockService;
+    private final DocumentBlockGeneratorService blockGeneratorService;
     private final RestTemplate restTemplate;
     private final DocumentRepository documentRepository;
 
@@ -212,10 +215,17 @@ public class DocumentParsedEventListener {
             List<Map<String, Object>> relations = (List<Map<String, Object>>) nerResponse.get("relations");
             int relationCount = graphNerService.saveRelationsToGraph(relations, tempIdToNodeId);
             
-            // 5. 将 NER 结果转换为 TextElement 格式(用于结构化文档)
-            List<TextElementDTO> textElements = nerToBlockService.convertToTextElements(text, entities);
-            log.debug("NER 结果已转换为 TextElement: documentId={}, elementCount={}", 
-                    documentId, textElements.size());
+            // 5. 生成并保存文档块结构
+            try {
+                List<BlockDTO> blocks = blockGeneratorService.generateBlocks(documentId, text, entities);
+                if (!blocks.isEmpty()) {
+                    // 调用 document-service 保存 blocks
+                    saveBlocksToDocumentService(documentId, blocks);
+                    log.info("文档块生成并保存完成: documentId={}, blockCount={}", documentId, blocks.size());
+                }
+            } catch (Exception e) {
+                log.warn("生成或保存文档块失败(不影响主流程): documentId={}, error={}", documentId, e.getMessage());
+            }
 
             long processingTime = System.currentTimeMillis() - startTime;
             
@@ -395,6 +405,71 @@ public class DocumentParsedEventListener {
         }
     }
     
+    // ==================== 文档块保存 ====================
+    
+    /**
+     * 调用 document-service 保存文档块
+     */
+    private void saveBlocksToDocumentService(String documentId, List<BlockDTO> blocks) {
+        try {
+            String url = "http://localhost:" + serverPort + "/api/v1/documents/" + documentId + "/blocks/batch";
+            
+            // 将 BlockDTO 转换为 Map 列表
+            List<Map<String, Object>> blockMaps = new ArrayList<>();
+            for (BlockDTO block : blocks) {
+                Map<String, Object> blockMap = new HashMap<>();
+                blockMap.put("blockId", block.getBlockId());
+                blockMap.put("documentId", block.getDocumentId());
+                blockMap.put("parentId", block.getParentId());
+                blockMap.put("children", block.getChildren());
+                blockMap.put("blockIndex", block.getBlockIndex());
+                blockMap.put("blockType", block.getBlockType());
+                blockMap.put("charStart", block.getCharStart());
+                blockMap.put("charEnd", block.getCharEnd());
+                
+                // 转换 elements
+                if (block.getElements() != null) {
+                    List<Map<String, Object>> elementMaps = new ArrayList<>();
+                    for (TextElementDTO el : block.getElements()) {
+                        Map<String, Object> elMap = new HashMap<>();
+                        elMap.put("type", el.getType());
+                        elMap.put("content", el.getContent());
+                        elMap.put("entityId", el.getEntityId());
+                        elMap.put("entityText", el.getEntityText());
+                        elMap.put("entityType", el.getEntityType());
+                        elMap.put("confirmed", el.getConfirmed());
+                        elMap.put("url", el.getUrl());
+                        elMap.put("refDocId", el.getRefDocId());
+                        elMap.put("refDocTitle", el.getRefDocTitle());
+                        elementMaps.add(elMap);
+                    }
+                    blockMap.put("elements", elementMaps);
+                }
+                
+                blockMaps.add(blockMap);
+            }
+            
+            Map<String, Object> request = new HashMap<>();
+            request.put("blocks", blockMaps);
+            
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            
+            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
+            
+            ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
+            
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("文档块保存成功: documentId={}, blockCount={}", documentId, blocks.size());
+            } else {
+                log.warn("文档块保存失败: documentId={}, status={}", documentId, response.getStatusCode());
+            }
+            
+        } catch (Exception e) {
+            log.error("调用 document-service 保存块失败: documentId={}, error={}", documentId, e.getMessage());
+        }
+    }
+    
     // ==================== 任务进度更新 ====================
     
     /**

+ 24 - 0
frontend/vue-demo/src/api/index.js

@@ -325,4 +325,28 @@ export const parseApi = {
   }
 }
 
+// ==================== 文档 API ====================
+
+export const documentApi = {
+  // 获取文档基本信息
+  getById(documentId) {
+    return api.get(`/documents/${documentId}`)
+  },
+
+  // 获取文档纯文本内容
+  getText(documentId) {
+    return api.get(`/documents/${documentId}/text`)
+  },
+
+  // 获取文档结构化元素
+  getElements(documentId) {
+    return api.get(`/documents/${documentId}/elements`)
+  },
+
+  // 获取结构化文档(用于编辑器)
+  getStructured(documentId) {
+    return api.get(`/documents/${documentId}/structured`)
+  }
+}
+
 export default api

+ 22 - 3
frontend/vue-demo/src/views/Editor.vue

@@ -306,6 +306,7 @@ import {
 } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import { useTemplateStore } from '@/stores/template'
+import { documentApi } from '@/api'
 
 const router = useRouter()
 const route = useRoute()
@@ -350,9 +351,27 @@ async function fetchTemplateData() {
     // 设置变量
     variables.value = templateStore.variables || []
     
-    // 设置文档内容(如果有的话)
-    const tplContent = templateStore.currentTemplate?.content
-    documentContent.value = tplContent || emptyPlaceholder
+    // 根据 baseDocumentId 获取文档结构化内容
+    const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
+    if (baseDocumentId) {
+      try {
+        const structuredDoc = await documentApi.getStructured(baseDocumentId)
+        // 将结构化文档的 blocks 转换为 HTML 内容
+        if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
+          // 优先使用 markedHtml(带实体标注),其次使用 html
+          documentContent.value = structuredDoc.blocks
+            .map(block => block.markedHtml || block.html || block.plainText || '')
+            .join('')
+        } else {
+          documentContent.value = emptyPlaceholder
+        }
+      } catch (docError) {
+        console.warn('获取文档内容失败:', docError)
+        documentContent.value = emptyPlaceholder
+      }
+    } else {
+      documentContent.value = emptyPlaceholder
+    }
   } catch (error) {
     console.error('加载模板失败:', error)
     ElMessage.error('加载模板失败')