Pārlūkot izejas kodu

chore: 移除 NER/Neo4j 逻辑与图表;SQL 合并为单文件 database/init.sql

- 后端: 删除 NER 控制器/服务、Neo4j 配置与同步、GraphNode/GraphRelation 持久化;监听器仅保留结构化解析;图服务改为存根
- 前端: 移除「重新生成」按钮与 regenerateBlocks 接口
- 数据库: 删除所有迁移与 backend/sql 下分散 SQL,新增 database/init.sql 单文件初始化(不含 graph_nodes/graph_relations)
- 脚本: server-deploy/start/rebuild_all 统一使用 database/init.sql

Co-authored-by: Cursor <cursoragent@cursor.com>
何文松 2 nedēļas atpakaļ
vecāks
revīzija
a9edef1bd9
51 mainītis faili ar 277 papildinājumiem un 7088 dzēšanām
  1. 1 1
      DEPLOY_SERVER.md
  2. 2 3
      backend/README.md
  3. 0 3
      backend/auth-service/src/main/java/com/lingyue/auth/config/SecurityConfig.java
  4. 13 63
      backend/document-service/src/main/java/com/lingyue/document/service/DocumentService.java
  5. 0 5
      backend/graph-service/pom.xml
  6. 0 122
      backend/graph-service/src/main/java/com/lingyue/graph/config/Neo4jConfig.java
  7. 0 122
      backend/graph-service/src/main/java/com/lingyue/graph/controller/Neo4jGraphController.java
  8. 0 252
      backend/graph-service/src/main/java/com/lingyue/graph/controller/NerBlockController.java
  9. 21 407
      backend/graph-service/src/main/java/com/lingyue/graph/listener/DocumentParsedEventListener.java
  10. 0 441
      backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jGraphService.java
  11. 0 313
      backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jNodeRepository.java
  12. 0 271
      backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jRelationRepository.java
  13. 0 57
      backend/graph-service/src/main/java/com/lingyue/graph/repository/GraphNodeRepository.java
  14. 0 55
      backend/graph-service/src/main/java/com/lingyue/graph/repository/GraphRelationRepository.java
  15. 1 26
      backend/graph-service/src/main/java/com/lingyue/graph/service/DataSourceService.java
  16. 0 315
      backend/graph-service/src/main/java/com/lingyue/graph/service/DocumentBlockGeneratorService.java
  17. 0 320
      backend/graph-service/src/main/java/com/lingyue/graph/service/GraphNerService.java
  18. 34 249
      backend/graph-service/src/main/java/com/lingyue/graph/service/GraphNodeService.java
  19. 0 183
      backend/graph-service/src/main/java/com/lingyue/graph/service/GraphSyncService.java
  20. 32 479
      backend/graph-service/src/main/java/com/lingyue/graph/service/KnowledgeGraphService.java
  21. 0 394
      backend/graph-service/src/main/java/com/lingyue/graph/service/NerToBlockService.java
  22. 1 17
      backend/lingyue-starter/src/main/resources/application.properties
  23. 0 8
      backend/pom.xml
  24. 0 16
      backend/sql/fix_graph_nodes_fk.sql
  25. 0 102
      backend/sql/graph_tables.sql
  26. 0 234
      backend/sql/init.sql
  27. 0 55
      backend/sql/init_supplement_only.sh
  28. 0 127
      backend/sql/rag_tables.sql
  29. 0 115
      backend/sql/rag_tables_compatible.sql
  30. 73 349
      backend/sql/rebuild_all.sh
  31. 3 217
      backend/sql/rebuild_database.sh
  32. 0 111
      backend/sql/supplement_tables.sql
  33. 0 200
      backend/sql/template_tables.sql
  34. 0 27
      backend/sql/text_storage_only.sql
  35. 86 344
      database/init.sql
  36. 0 57
      database/migrations/V2026_01_21_02__add_document_elements.sql
  37. 0 50
      database/migrations/V2026_01_21_03__enhance_data_sources.sql
  38. 0 69
      database/migrations/V2026_01_21__add_document_blocks_and_entities.sql
  39. 0 36
      database/migrations/V2026_01_22_01__enhance_parse_tasks_stages.sql
  40. 0 200
      database/migrations/V2026_01_22_02__create_extract_tables.sql
  41. 0 159
      database/migrations/V2026_01_23_01__refactor_extract_to_template.sql
  42. 0 34
      database/migrations/V2026_01_24_01__add_variable_category.sql
  43. 0 15
      database/migrations/V2026_01_27_01__make_base_document_id_nullable.sql
  44. 0 4
      database/migrations/V2026_01_27_02__add_ner_message_field.sql
  45. 0 8
      database/migrations/V2026_01_28_01__add_document_elements_runs.sql
  46. 0 7
      database/migrations/V2026_01_29_01__add_template_rating.sql
  47. 0 362
      docs/neo4j-local-install.md
  48. 0 5
      frontend/vue-demo/src/api/index.js
  49. 1 43
      frontend/vue-demo/src/views/Editor.vue
  50. 4 16
      server-deploy.sh
  51. 5 20
      start.sh

+ 1 - 1
DEPLOY_SERVER.md

@@ -134,7 +134,7 @@ cd /mnt/win_home/lingyue-zhibao
 git clone <your-gitlab-repo-url> .
 
 # 初始化数据库表
-psql -U lingyue -d lingyue_zhibao -f backend/sql/init.sql
+psql -U lingyue -d lingyue_zhibao -f database/init.sql
 psql -U lingyue -d lingyue_zhibao -f backend/sql/rag_tables.sql
 
 # 编译项目

+ 2 - 3
backend/README.md

@@ -38,8 +38,7 @@ backend/
 ├── ai-service/                       # AI 处理服务
 ├── graph-service/                    # 关系网络服务
 ├── notification-service/             # 通知服务
-└── sql/                              # 数据库脚本
-    └── init.sql
+└── sql/                              # 数据库重建脚本(实际初始化脚本在项目根 database/init.sql)
 ```
 
 ## 服务说明
@@ -83,7 +82,7 @@ AI 处理服务,负责要素提取、文本处理、Prompt 生成等功能。
 createdb lingyue_zhibao
 
 # 执行初始化脚本
-psql -U postgres -d lingyue_zhibao -f sql/init.sql
+psql -U postgres -d lingyue_zhibao -f ../database/init.sql
 ```
 
 ### 2. 启动基础设施

+ 0 - 3
backend/auth-service/src/main/java/com/lingyue/auth/config/SecurityConfig.java

@@ -44,7 +44,6 @@ public class SecurityConfig {
                             // 知识图谱接口(开发阶段暂时开放)
                             .requestMatchers("/api/v1/graph/**").permitAll()
                             // 结构化文档接口(开发阶段暂时开放)
-                            .requestMatchers("/api/v1/ner/**").permitAll()
                             // Actuator 健康检查
                             .requestMatchers("/actuator/**").permitAll()
                             // API 文档
@@ -53,8 +52,6 @@ public class SecurityConfig {
                             .requestMatchers("/api/v1/parse/**", "/parse/**").permitAll()
                             // RAG 接口(开发阶段暂时开放)
                             .requestMatchers("/api/rag/**").permitAll()
-                            // NER 接口(开发阶段暂时开放)
-                            .requestMatchers("/api/ner/**").permitAll()
                             // 图谱接口(开发阶段暂时开放)
                             .requestMatchers("/api/graph/**", "/api/text-storage/**").permitAll()
                             // 文件访问接口(开发阶段暂时开放)

+ 13 - 63
backend/document-service/src/main/java/com/lingyue/document/service/DocumentService.java

@@ -186,13 +186,12 @@ public class DocumentService {
      * 删除顺序(遵循外键约束):
      * 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. 图片目录
+     * 3. document_elements (document_id)
+     * 4. parse_tasks (document_id)
+     * 5. documents (主表)
+     * 6. 文本文件
+     * 7. 图片目录
+     * (graph_nodes / graph_relations 已移除)
      * 
      * @param documentId 文档ID
      */
@@ -214,34 +213,26 @@ public class DocumentService {
         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. 删除结构化元素
+        // 3. 删除结构化元素
         int elementCount = documentElementRepository.deleteByDocumentId(documentId);
         log.debug("删除结构化元素: count={}", elementCount);
         
-        // 6. 删除解析任务
+        // 4. 删除解析任务
         int taskCount = deleteParseTasksByDocumentId(documentId);
         log.debug("删除解析任务: count={}", taskCount);
         
-        // 7. 删除文档记录
+        // 5. 删除文档记录
         documentRepository.deleteById(documentId);
         log.debug("删除文档记录");
         
-        // 8. 删除文本文件(不影响事务)
+        // 6. 删除文本文件(不影响事务)
         deleteTextFile(documentId);
         
-        // 9. 删除图片目录(不影响事务)
+        // 7. 删除图片目录(不影响事务)
         deleteImageDirectory(userId, documentId);
         
-        log.info("级联删除文档完成: documentId={}, 删除向量={}, 分块={}, 关系={}, 节点={}, 元素={}, 任务={}",
-                documentId, vectorCount, chunkCount, relationCount, nodeCount, elementCount, taskCount);
+        log.info("级联删除文档完成: documentId={}, 删除向量={}, 分块={}, 元素={}, 任务={}",
+                documentId, vectorCount, chunkCount, elementCount, taskCount);
     }
     
     /**
@@ -282,47 +273,6 @@ public class DocumentService {
         }
     }
     
-    /**
-     * 删除图关系(通过节点关联)
-     */
-    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;
-        }
-    }
-    
     /**
      * 删除解析任务
      */

+ 0 - 5
backend/graph-service/pom.xml

@@ -77,11 +77,6 @@
             <optional>true</optional>
         </dependency>
         
-        <!-- Neo4j Driver -->
-        <dependency>
-            <groupId>org.neo4j.driver</groupId>
-            <artifactId>neo4j-java-driver</artifactId>
-        </dependency>
     </dependencies>
 
     <build>

+ 0 - 122
backend/graph-service/src/main/java/com/lingyue/graph/config/Neo4jConfig.java

@@ -1,122 +0,0 @@
-package com.lingyue.graph.config;
-
-import lombok.Data;
-import lombok.extern.slf4j.Slf4j;
-import org.neo4j.driver.*;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-import jakarta.annotation.PreDestroy;
-
-/**
- * Neo4j 配置类
- * 
- * 使用 Neo4j Java Driver 连接图数据库
- * 
- * 配置示例(application.yml):
- * neo4j:
- *   enabled: true
- *   uri: bolt://localhost:7687
- *   username: neo4j
- *   password: your-password
- *   database: neo4j
- *   connection-timeout: 30s
- *   max-connection-pool-size: 50
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Data
-@Configuration
-@ConfigurationProperties(prefix = "neo4j")
-public class Neo4jConfig {
-    
-    /**
-     * 是否启用 Neo4j(默认禁用,便于开发测试)
-     */
-    private boolean enabled = false;
-    
-    /**
-     * Neo4j 连接 URI
-     * 格式: bolt://host:port 或 neo4j://host:port
-     */
-    private String uri = "bolt://localhost:7687";
-    
-    /**
-     * 用户名
-     */
-    private String username = "neo4j";
-    
-    /**
-     * 密码
-     */
-    private String password;
-    
-    /**
-     * 数据库名称(Neo4j 4.0+ 支持多数据库)
-     */
-    private String database = "neo4j";
-    
-    /**
-     * 连接超时时间(秒)
-     */
-    private int connectionTimeoutSeconds = 30;
-    
-    /**
-     * 最大连接池大小
-     */
-    private int maxConnectionPoolSize = 50;
-    
-    private Driver driver;
-    
-    /**
-     * 创建 Neo4j Driver Bean
-     */
-    @Bean
-    @ConditionalOnProperty(name = "neo4j.enabled", havingValue = "true")
-    public Driver neo4jDriver() {
-        log.info("初始化 Neo4j 连接: uri={}, database={}", uri, database);
-        
-        Config config = Config.builder()
-                .withConnectionTimeout(connectionTimeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
-                .withMaxConnectionPoolSize(maxConnectionPoolSize)
-                .withDriverMetrics()
-                .build();
-        
-        // 支持无密码模式(当 Neo4j 禁用认证时)
-        AuthToken auth;
-        if (password == null || password.isEmpty()) {
-            auth = AuthTokens.none();
-            log.info("Neo4j 使用无认证模式连接");
-        } else {
-            auth = AuthTokens.basic(username, password);
-        }
-        
-        this.driver = GraphDatabase.driver(uri, auth, config);
-        
-        // 验证连接
-        try {
-            driver.verifyConnectivity();
-            log.info("Neo4j 连接成功");
-        } catch (Exception e) {
-            log.error("Neo4j 连接失败: {}", e.getMessage());
-            throw new RuntimeException("无法连接到 Neo4j: " + e.getMessage(), e);
-        }
-        
-        return driver;
-    }
-    
-    /**
-     * 关闭连接
-     */
-    @PreDestroy
-    public void close() {
-        if (driver != null) {
-            log.info("关闭 Neo4j 连接");
-            driver.close();
-        }
-    }
-}

+ 0 - 122
backend/graph-service/src/main/java/com/lingyue/graph/controller/Neo4jGraphController.java

@@ -1,122 +0,0 @@
-package com.lingyue.graph.controller;
-
-import com.lingyue.common.domain.AjaxResult;
-import com.lingyue.graph.dto.KnowledgeGraphDTO;
-import com.lingyue.graph.neo4j.Neo4jGraphService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Neo4j 图数据库控制器
- * 
- * 提供基于 Neo4j 的高级图查询功能
- * 仅在 neo4j.enabled=true 时启用
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/v1/neo4j")
-@RequiredArgsConstructor
-@ConditionalOnProperty(name = "neo4j.enabled", havingValue = "true")
-@Tag(name = "Neo4j 图数据库", description = "Neo4j 图查询接口")
-public class Neo4jGraphController {
-    
-    private final Neo4jGraphService neo4jGraphService;
-    
-    /**
-     * 获取文档的知识图谱(从 Neo4j)
-     */
-    @GetMapping("/documents/{documentId}/graph")
-    @Operation(summary = "获取文档图谱", description = "从 Neo4j 获取文档的知识图谱")
-    public AjaxResult<?> getDocumentGraph(
-            @Parameter(description = "文档ID", required = true)
-            @PathVariable String documentId) {
-        
-        KnowledgeGraphDTO graph = neo4jGraphService.getDocumentGraph(documentId);
-        if (graph == null) {
-            return AjaxResult.error("文档不存在或无图谱数据");
-        }
-        
-        return AjaxResult.success(graph);
-    }
-    
-    /**
-     * 查找最短路径
-     */
-    @GetMapping("/path")
-    @Operation(summary = "查找最短路径", description = "查找两个节点之间的最短路径")
-    public AjaxResult<?> findShortestPath(
-            @Parameter(description = "起始节点ID", required = true)
-            @RequestParam String fromNodeId,
-            @Parameter(description = "目标节点ID", required = true)
-            @RequestParam String toNodeId,
-            @Parameter(description = "最大深度")
-            @RequestParam(defaultValue = "5") int maxDepth) {
-        
-        List<Map<String, Object>> path = neo4jGraphService.findShortestPath(fromNodeId, toNodeId, maxDepth);
-        return AjaxResult.success(path);
-    }
-    
-    /**
-     * 获取节点邻域图
-     */
-    @GetMapping("/nodes/{nodeId}/neighborhood")
-    @Operation(summary = "获取邻域图", description = "获取节点的 N 跳邻居形成的子图")
-    public AjaxResult<?> getNeighborhood(
-            @Parameter(description = "节点ID", required = true)
-            @PathVariable String nodeId,
-            @Parameter(description = "邻域深度")
-            @RequestParam(defaultValue = "2") int depth) {
-        
-        KnowledgeGraphDTO graph = neo4jGraphService.getNeighborhood(nodeId, depth);
-        return AjaxResult.success(graph);
-    }
-    
-    /**
-     * 获取核心节点(按关联度排序)
-     */
-    @GetMapping("/documents/{documentId}/top-nodes")
-    @Operation(summary = "获取核心节点", description = "获取文档中最重要的节点(按关联度)")
-    public AjaxResult<?> getTopNodes(
-            @Parameter(description = "文档ID", required = true)
-            @PathVariable String documentId,
-            @Parameter(description = "返回数量")
-            @RequestParam(defaultValue = "10") int limit) {
-        
-        List<Map<String, Object>> nodes = neo4jGraphService.getTopNodesByPageRank(documentId, limit);
-        return AjaxResult.success(nodes);
-    }
-    
-    /**
-     * 删除文档图数据
-     */
-    @DeleteMapping("/documents/{documentId}")
-    @Operation(summary = "删除文档图数据", description = "删除文档在 Neo4j 中的所有节点和关系")
-    public AjaxResult<?> deleteDocumentGraph(
-            @Parameter(description = "文档ID", required = true)
-            @PathVariable String documentId) {
-        
-        neo4jGraphService.deleteDocumentGraph(documentId);
-        return AjaxResult.success("删除成功");
-    }
-    
-    /**
-     * 初始化索引
-     */
-    @PostMapping("/init-indexes")
-    @Operation(summary = "初始化索引", description = "创建 Neo4j 数据库索引")
-    public AjaxResult<?> initializeIndexes() {
-        neo4jGraphService.initializeIndexes();
-        return AjaxResult.success("索引初始化完成");
-    }
-}

+ 0 - 252
backend/graph-service/src/main/java/com/lingyue/graph/controller/NerBlockController.java

@@ -1,252 +0,0 @@
-package com.lingyue.graph.controller;
-
-import com.lingyue.common.domain.AjaxResult;
-import com.lingyue.graph.entity.GraphNode;
-import com.lingyue.graph.service.DocumentBlockGeneratorService;
-import com.lingyue.graph.service.DocumentBlockGeneratorService.BlockDTO;
-import com.lingyue.graph.service.GraphNerService;
-import com.lingyue.graph.service.GraphNodeService;
-import com.lingyue.graph.service.NerToBlockService;
-import com.lingyue.graph.service.NerToBlockService.TextElementDTO;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.Data;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.*;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.*;
-
-/**
- * NER 到 Block 转换控制器
- * 
- * 提供将 NER 结果转换为结构化 Block 的接口
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/v1/ner")
-@RequiredArgsConstructor
-@Tag(name = "NER Block 转换", description = "NER 结果转换为结构化 Block")
-public class NerBlockController {
-    
-    private final GraphNerService graphNerService;
-    private final GraphNodeService graphNodeService;
-    private final NerToBlockService nerToBlockService;
-    private final DocumentBlockGeneratorService blockGeneratorService;
-    private final RestTemplate restTemplate;
-    
-    @Value("${server.port:5232}")
-    private int serverPort;
-    
-    /**
-     * 将文本和实体转换为 TextElement 列表
-     */
-    @PostMapping("/convert-to-elements")
-    @Operation(summary = "转换为 TextElement", description = "将文本和 NER 实体转换为 TextElement 列表")
-    public AjaxResult<?> convertToElements(@RequestBody ConvertRequest request) {
-        try {
-            List<TextElementDTO> elements = nerToBlockService.convertToTextElements(
-                    request.getText(), request.getEntities());
-            return AjaxResult.success(elements);
-        } catch (Exception e) {
-            log.error("转换失败: {}", e.getMessage(), e);
-            return AjaxResult.error("转换失败: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 生成完整的文档块结构
-     */
-    @PostMapping("/generate-blocks")
-    @Operation(summary = "生成文档块", description = "将文本和 NER 实体生成完整的 Block 结构")
-    public AjaxResult<?> generateBlocks(@RequestBody GenerateBlocksRequest request) {
-        try {
-            List<BlockDTO> blocks = blockGeneratorService.generateBlocks(
-                    request.getDocumentId(), request.getText(), request.getEntities());
-            return AjaxResult.success(blocks);
-        } catch (Exception e) {
-            log.error("生成块失败: {}", e.getMessage(), e);
-            return AjaxResult.error("生成块失败: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 从已存储的文档生成块结构
-     */
-    @PostMapping("/documents/{documentId}/generate-blocks")
-    @Operation(summary = "从文档生成块", description = "读取已存储的文档文本,结合 NER 结果生成 Block 结构")
-    public AjaxResult<?> generateBlocksFromDocument(
-            @PathVariable String documentId,
-            @RequestBody(required = false) List<Map<String, Object>> entities) {
-        try {
-            // 获取文档文本
-            String text = graphNerService.getDocumentText(documentId);
-            
-            // 如果没有提供实体,从图数据库获取
-            if (entities == null || entities.isEmpty()) {
-                entities = getEntitiesFromGraphNodes(documentId);
-                log.info("从图数据库获取实体: documentId={}, count={}", documentId, entities.size());
-            }
-            
-            List<BlockDTO> blocks = blockGeneratorService.generateBlocks(documentId, text, entities);
-            return AjaxResult.success(blocks);
-        } catch (Exception e) {
-            log.error("从文档生成块失败: documentId={}, error={}", documentId, e.getMessage(), e);
-            return AjaxResult.error("生成失败: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 重新生成并保存文档块(用于已解析但没有 blocks 的文档)
-     */
-    @PostMapping("/documents/{documentId}/regenerate-blocks")
-    @Operation(summary = "重新生成并保存块", description = "从已存储的文档和NER结果重新生成并保存Block到数据库")
-    public AjaxResult<?> regenerateAndSaveBlocks(@PathVariable String documentId) {
-        try {
-            // 1. 获取文档文本
-            String text = graphNerService.getDocumentText(documentId);
-            if (text == null || text.isEmpty()) {
-                return AjaxResult.error("文档文本不存在: " + documentId);
-            }
-            
-            // 2. 从图数据库获取实体
-            List<Map<String, Object>> entities = getEntitiesFromGraphNodes(documentId);
-            log.info("重新生成块: documentId={}, textLength={}, entityCount={}", 
-                    documentId, text.length(), entities.size());
-            
-            // 3. 生成块
-            List<BlockDTO> blocks = blockGeneratorService.generateBlocks(documentId, text, entities);
-            if (blocks.isEmpty()) {
-                return AjaxResult.error("生成块失败:没有可用的内容");
-            }
-            
-            // 4. 保存到 document-service
-            int savedCount = saveBlocksToDocumentService(documentId, blocks);
-            
-            Map<String, Object> result = new HashMap<>();
-            result.put("blockCount", blocks.size());
-            result.put("savedCount", savedCount);
-            result.put("entityCount", entities.size());
-            
-            return AjaxResult.success("重新生成并保存成功", result);
-            
-        } catch (Exception e) {
-            log.error("重新生成块失败: documentId={}, error={}", documentId, e.getMessage(), e);
-            return AjaxResult.error("重新生成失败: " + e.getMessage());
-        }
-    }
-    
-    /**
-     * 从 GraphNode 表获取实体并转换为 NER 格式
-     */
-    @SuppressWarnings("unchecked")
-    private List<Map<String, Object>> getEntitiesFromGraphNodes(String documentId) {
-        List<GraphNode> nodes = graphNodeService.getNodesByDocumentId(documentId);
-        List<Map<String, Object>> entities = new ArrayList<>();
-        
-        for (GraphNode node : nodes) {
-            Map<String, Object> entity = new HashMap<>();
-            entity.put("name", node.getName());
-            entity.put("type", node.getType());
-            entity.put("tempId", node.getId());
-            
-            // 位置信息(从 GraphNode.position JSONB 字段获取)
-            Object positionObj = node.getPosition();
-            if (positionObj instanceof Map) {
-                Map<String, Object> posMap = (Map<String, Object>) positionObj;
-                Map<String, Object> position = new HashMap<>();
-                position.put("charStart", posMap.get("charStart"));
-                position.put("charEnd", posMap.get("charEnd"));
-                entity.put("position", position);
-            }
-            
-            entities.add(entity);
-        }
-        
-        return entities;
-    }
-    
-    /**
-     * 调用 document-service 保存块
-     */
-    private int saveBlocksToDocumentService(String documentId, List<BlockDTO> blocks) {
-        try {
-            String url = "http://localhost:" + serverPort + "/api/v1/documents/" + documentId + "/blocks/batch";
-            
-            // 转换为 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());
-                
-                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());
-                        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() && response.getBody() != null) {
-                Object data = response.getBody().get("data");
-                if (data instanceof Number) {
-                    return ((Number) data).intValue();
-                }
-            }
-            
-            return blocks.size();
-            
-        } catch (Exception e) {
-            log.error("保存块到 document-service 失败: documentId={}, error={}", documentId, e.getMessage());
-            throw new RuntimeException("保存块失败: " + e.getMessage(), e);
-        }
-    }
-    
-    // ==================== 请求 DTO ====================
-    
-    @Data
-    public static class ConvertRequest {
-        private String text;
-        private List<Map<String, Object>> entities;
-    }
-    
-    @Data
-    public static class GenerateBlocksRequest {
-        private String documentId;
-        private String text;
-        private List<Map<String, Object>> entities;
-    }
-}

+ 21 - 407
backend/graph-service/src/main/java/com/lingyue/graph/listener/DocumentParsedEventListener.java

@@ -3,11 +3,6 @@ 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;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
@@ -23,118 +18,76 @@ import java.util.*;
  * 文档解析完成事件监听器
  * 监听文档解析完成事件,自动触发后续处理流程:
  * 1. 结构化解析(Word 文档 -> 段落/图片/表格)
- * 2. NER 实体提取(文本 -> 实体/关系)
- * 
- * 所有步骤同时支持手动触发 API,可单独重新生成
+ *
+ * NER 与 Neo4j 相关逻辑已移除。
  *
  * @author lingyue
  * @since 2026-01-19
- * @updated 2026-01-21 增加自动结构化解析
  */
 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class DocumentParsedEventListener {
 
-    private final GraphNerService graphNerService;
-    private final NerToBlockService nerToBlockService;
-    private final DocumentBlockGeneratorService blockGeneratorService;
     private final RestTemplate restTemplate;
     private final DocumentRepository documentRepository;
 
-    @Value("${ner.auto-extract.enabled:true}")
-    private boolean nerAutoExtractEnabled;
-    
     @Value("${parse.structured.auto-extract.enabled:true}")
     private boolean structuredAutoExtractEnabled;
-    
+
     @Value("${server.port:5232}")
     private int serverPort;
 
-    @Value("${ner.python-service.url:http://localhost:8001}")
-    private String nerServiceUrl;
-    
-    @Value("${ner.python-service.use-async:true}")
-    private boolean useAsyncApi;
-    
-    @Value("${ner.python-service.poll-interval:3000}")
-    private long pollInterval;  // 轮询间隔(毫秒)
-    
-    @Value("${ner.python-service.max-wait-time:600000}")
-    private long maxWaitTime;  // 最大等待时间(毫秒)
-
-    /**
-     * 处理文档解析完成事件
-     * 异步执行后续处理流程,不阻塞主流程
-     * 
-     * 处理顺序:
-     * 1. 结构化解析(Word 文档提取段落/图片/表格)
-     * 2. NER 实体提取(文本提取实体/关系)
-     */
     @Async
     @EventListener
     public void handleDocumentParsedEvent(DocumentParsedEvent event) {
         String documentId = event.getDocumentId();
         String userId = event.getUserId();
-        
+
         log.info("收到文档解析完成事件: documentId={}, userId={}", documentId, userId);
-        
+
         long totalStartTime = System.currentTimeMillis();
-        
-        // Step 1: 结构化解析(仅 Word 文档)
+
         if (structuredAutoExtractEnabled) {
             triggerStructuredExtraction(documentId);
         }
-        
-        // Step 2: NER 实体提取
-        if (nerAutoExtractEnabled) {
-            triggerNerExtraction(documentId, userId);
-        }
-        
+
         long totalTime = System.currentTimeMillis() - totalStartTime;
         log.info("文档后处理完成: documentId={}, totalTime={}ms", documentId, totalTime);
-        
-        // Step 3: 更新解析任务的最终状态为已完成
+
         updateParseTaskCompleted(documentId);
     }
-    
+
     /**
-     * 触发结构化解析
-     * 仅对 Word 文档有效,提取段落、图片、表格
+     * 触发结构化解析(仅 Word 文档)
      */
     private void triggerStructuredExtraction(String documentId) {
         try {
-            // 检查是否是 Word 文档
             Document document = documentRepository.selectById(documentId);
             if (document == null) {
                 log.warn("文档不存在,跳过结构化解析: documentId={}", documentId);
                 return;
             }
-            
+
             String docType = document.getType();
             if (!"word".equalsIgnoreCase(docType)) {
                 log.debug("非 Word 文档,跳过结构化解析: documentId={}, type={}", documentId, docType);
-                // 标记为完成(非Word文档无需结构化解析)
                 updateTaskProgress(documentId, "structured", "completed", 100, null);
                 return;
             }
-            
+
             log.info("开始自动结构化解析: documentId={}", documentId);
             long startTime = System.currentTimeMillis();
-            
-            // 更新进度:开始
+
             updateTaskProgress(documentId, "structured", "processing", 10, null);
-            
-            // 调用本地 API 触发结构化解析
+
             String url = "http://localhost:" + serverPort + "/api/v1/parse/structured/" + documentId;
-            
             ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
-            
-            if (response.getStatusCode().is2xxSuccessful()) {
+
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                 long time = System.currentTimeMillis() - startTime;
                 log.info("结构化解析完成: documentId={}, time={}ms", documentId, time);
-                
-                // 提取结果信息并更新进度
+
                 Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
                 if (data != null) {
                     Map<String, Object> progressData = new HashMap<>();
@@ -149,359 +102,29 @@ public class DocumentParsedEventListener {
                 log.warn("结构化解析失败: documentId={}, status={}", documentId, response.getStatusCode());
                 updateTaskProgress(documentId, "structured", "failed", 0, null);
             }
-            
         } catch (Exception e) {
             log.error("自动结构化解析异常: documentId={}, error={}", documentId, e.getMessage());
             updateTaskProgress(documentId, "structured", "failed", 0, null);
-            // 异常不向上抛出,不影响后续处理
-        }
-    }
-    
-    /**
-     * 触发 NER 实体提取
-     */
-    private void triggerNerExtraction(String documentId, String userId) {
-        long startTime = System.currentTimeMillis();
-
-        try {
-            // 1. 获取文档文本
-            if (!graphNerService.hasDocumentText(documentId)) {
-                log.warn("文档文本不存在,跳过 NER: documentId={}", documentId);
-                return;
-            }
-            
-            String text = graphNerService.getDocumentText(documentId);
-            if (text == null || text.isEmpty()) {
-                log.warn("文档文本为空,跳过 NER: documentId={}", documentId);
-                return;
-            }
-            
-            log.info("开始自动 NER 提取: documentId={}", documentId);
-            
-            // 更新进度:开始
-            updateTaskProgress(documentId, "ner", "processing", 5, null);
-
-            // 2. 调用 Python NER 服务(根据配置选择异步轮询或同步 API)
-            Map<String, Object> nerResponse;
-            if (useAsyncApi) {
-                nerResponse = callPythonNerServiceAsync(documentId, text, userId);
-            } else {
-                nerResponse = callPythonNerService(documentId, text, userId);
-            }
-            
-            if (nerResponse == null || !Boolean.TRUE.equals(nerResponse.get("success"))) {
-                log.warn("NER 服务调用失败: documentId={}, error={}", 
-                        documentId, nerResponse != null ? nerResponse.get("errorMessage") : "null response");
-                updateTaskProgress(documentId, "ner", "failed", 0, null);
-                return;
-            }
-            
-            // 更新进度:NER 完成,开始保存
-            updateTaskProgress(documentId, "ner", "processing", 80, null);
-            
-            // 更新图构建进度:开始
-            updateTaskProgress(documentId, "graph", "processing", 10, null);
-
-            // 3. 保存实体到图数据库
-            @SuppressWarnings("unchecked")
-            List<Map<String, Object>> entities = (List<Map<String, Object>>) nerResponse.get("entities");
-            Map<String, String> tempIdToNodeId = graphNerService.saveEntitiesToGraph(documentId, userId, entities);
-            
-            // 更新图构建进度
-            updateTaskProgress(documentId, "graph", "processing", 50, null);
-
-            // 4. 保存关系到图数据库
-            @SuppressWarnings("unchecked")
-            List<Map<String, Object>> relations = (List<Map<String, Object>>) nerResponse.get("relations");
-            int relationCount = graphNerService.saveRelationsToGraph(relations, tempIdToNodeId);
-            
-            // 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;
-            
-            // 更新 NER 完成进度
-            int entityCount = entities != null ? entities.size() : 0;
-            Map<String, Object> nerProgressData = new HashMap<>();
-            nerProgressData.put("status", "completed");
-            nerProgressData.put("progress", 100);
-            nerProgressData.put("entityCount", entityCount);
-            nerProgressData.put("relationCount", relationCount);
-            updateTaskProgress(documentId, "ner", nerProgressData);
-            
-            // 更新图构建完成进度
-            updateTaskProgress(documentId, "graph", "completed", 100, null);
-            
-            log.info("NER 自动提取完成: documentId={}, entityCount={}, relationCount={}, time={}ms",
-                    documentId, entityCount, relationCount, processingTime);
-
-        } catch (Exception e) {
-            log.error("NER 自动提取异常: documentId={}", documentId, e);
-            updateTaskProgress(documentId, "ner", "failed", 0, null);
-            // 异常不向上抛出,不影响其他处理
         }
     }
 
-    /**
-     * 调用 Python NER 服务(同步 REST API)
-     */
-    private Map<String, Object> callPythonNerService(String documentId, String text, String userId) {
-        try {
-            String url = nerServiceUrl + "/ner/extract";
-            
-            Map<String, Object> request = new HashMap<>();
-            request.put("documentId", documentId);
-            request.put("text", text);
-            request.put("userId", userId);
-            request.put("extractRelations", true);
-            
-            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() && response.getBody() != null) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> body = response.getBody();
-                return body;
-            }
-            
-            return null;
-            
-        } catch (Exception e) {
-            log.error("调用 Python NER 服务失败: {}", e.getMessage());
-            return null;
-        }
-    }
-    
-    /**
-     * 调用 Python NER 服务(异步 + 轮询模式)
-     * 
-     * 流程:
-     * 1. 提交异步任务,立即获得 task_id
-     * 2. 定期轮询任务状态
-     * 3. 任务完成后获取结果
-     */
-    private Map<String, Object> callPythonNerServiceAsync(String documentId, String text, String userId) {
-        try {
-            // 1. 提交异步任务
-            String submitUrl = nerServiceUrl + "/ner/extract/async";
-            
-            Map<String, Object> request = new HashMap<>();
-            request.put("documentId", documentId);
-            request.put("text", text);
-            request.put("userId", userId);
-            request.put("extractRelations", true);
-            
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.APPLICATION_JSON);
-            
-            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
-            
-            @SuppressWarnings("unchecked")
-            ResponseEntity<Map<String, Object>> submitResponse = restTemplate.exchange(
-                    submitUrl, HttpMethod.POST, entity, 
-                    (Class<Map<String, Object>>) (Class<?>) Map.class);
-            
-            if (!submitResponse.getStatusCode().is2xxSuccessful() || submitResponse.getBody() == null) {
-                log.error("提交异步 NER 任务失败: documentId={}", documentId);
-                return null;
-            }
-            
-            String taskId = (String) submitResponse.getBody().get("task_id");
-            log.info("异步 NER 任务已提交: documentId={}, taskId={}", documentId, taskId);
-            
-            // 2. 轮询任务状态
-            String statusUrl = nerServiceUrl + "/ner/task/" + taskId;
-            long startTime = System.currentTimeMillis();
-            int lastProgress = -1;
-            
-            while (System.currentTimeMillis() - startTime < maxWaitTime) {
-                try {
-                    Thread.sleep(pollInterval);
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    log.warn("轮询被中断: taskId={}", taskId);
-                    break;
-                }
-                
-                try {
-                    @SuppressWarnings("unchecked")
-                    ResponseEntity<Map<String, Object>> statusResponse = restTemplate.exchange(
-                            statusUrl, HttpMethod.GET, null,
-                            (Class<Map<String, Object>>) (Class<?>) Map.class);
-                    
-                    if (!statusResponse.getStatusCode().is2xxSuccessful() || statusResponse.getBody() == null) {
-                        log.warn("查询任务状态失败: taskId={}", taskId);
-                        continue;
-                    }
-                    
-                    Map<String, Object> taskStatus = statusResponse.getBody();
-                    String status = (String) taskStatus.get("status");
-                    int progress = taskStatus.get("progress") != null ? 
-                            ((Number) taskStatus.get("progress")).intValue() : 0;
-                    String message = (String) taskStatus.get("message");
-                    
-                    // 只在进度变化时打印日志和更新前端进度
-                    if (progress != lastProgress) {
-                        log.info("NER 进度: documentId={}, taskId={}, status={}, progress={}%, message={}", 
-                                documentId, taskId, status, progress, message);
-                        lastProgress = progress;
-                        
-                        // 更新 NER 进度到任务状态(包含分块信息)
-                        Map<String, Object> nerProgressData = new HashMap<>();
-                        nerProgressData.put("status", "processing");
-                        nerProgressData.put("progress", progress);
-                        nerProgressData.put("nerTaskId", taskId);
-                        nerProgressData.put("message", message);  // 分块进度信息
-                        updateTaskProgress(documentId, "ner", nerProgressData);
-                    }
-                    
-                    // 检查任务是否完成
-                    if ("completed".equals(status)) {
-                        @SuppressWarnings("unchecked")
-                        Map<String, Object> result = (Map<String, Object>) taskStatus.get("result");
-                        log.info("异步 NER 任务完成: documentId={}, taskId={}", documentId, taskId);
-                        
-                        // 删除任务(释放服务端内存)
-                        try {
-                            restTemplate.delete(statusUrl);
-                        } catch (Exception e) {
-                            log.debug("删除任务失败(可忽略): taskId={}", taskId);
-                        }
-                        
-                        return result;
-                    } else if ("failed".equals(status)) {
-                        String error = (String) taskStatus.get("error");
-                        log.error("异步 NER 任务失败: documentId={}, taskId={}, error={}", documentId, taskId, error);
-                        return null;
-                    }
-                    
-                } catch (Exception e) {
-                    log.warn("轮询任务状态异常: taskId={}, error={}", taskId, e.getMessage());
-                }
-            }
-            
-            log.error("异步 NER 任务超时: documentId={}, taskId={}, maxWaitTime={}ms", 
-                    documentId, taskId, maxWaitTime);
-            return null;
-            
-        } catch (Exception e) {
-            log.error("调用异步 NER 服务失败: documentId={}, error={}", documentId, e.getMessage(), e);
-            // 回退到同步 API
-            log.info("回退到同步 NER API: documentId={}", documentId);
-            return callPythonNerService(documentId, text, userId);
-        }
-    }
-    
-    // ==================== 文档块保存 ====================
-    
-    /**
-     * 调用 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());
-        }
-    }
-    
-    // ==================== 任务进度更新 ====================
-    
-    /**
-     * 更新解析任务的最终状态为已完成
-     * 当所有后处理阶段(NER、图构建)都完成后调用
-     */
     private void updateParseTaskCompleted(String documentId) {
         try {
             String url = "http://localhost:" + serverPort + "/api/internal/task-progress/complete/" + documentId;
-            
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
-            
             Map<String, Object> data = new HashMap<>();
             data.put("status", "completed");
             data.put("progress", 100);
             data.put("currentStep", "completed");
-            
             HttpEntity<Map<String, Object>> entity = new HttpEntity<>(data, headers);
-            
             restTemplate.postForEntity(url, entity, Map.class);
             log.info("解析任务状态更新为已完成: documentId={}", documentId);
-            
         } catch (Exception e) {
-            log.warn("更新解析任务完成状态失败(可忽略): documentId={}, error={}", 
-                    documentId, e.getMessage());
+            log.warn("更新解析任务完成状态失败(可忽略): documentId={}, error={}", documentId, e.getMessage());
         }
     }
-    
-    /**
-     * 更新任务进度(简单版本)
-     */
+
     private void updateTaskProgress(String documentId, String stage, String status, Integer progress, String errorMessage) {
         Map<String, Object> data = new HashMap<>();
         data.put("status", status);
@@ -511,25 +134,16 @@ public class DocumentParsedEventListener {
         }
         updateTaskProgress(documentId, stage, data);
     }
-    
-    /**
-     * 更新任务进度(完整版本)
-     */
+
     private void updateTaskProgress(String documentId, String stage, Map<String, Object> data) {
         try {
             String url = "http://localhost:" + serverPort + "/api/internal/task-progress/" + stage + "/" + documentId;
-            
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
-            
             HttpEntity<Map<String, Object>> entity = new HttpEntity<>(data, headers);
-            
             restTemplate.postForEntity(url, entity, Map.class);
-            
         } catch (Exception e) {
-            // 进度更新失败不影响主流程
-            log.debug("更新任务进度失败(可忽略): documentId={}, stage={}, error={}", 
-                    documentId, stage, e.getMessage());
+            log.debug("更新任务进度失败(可忽略): documentId={}, stage={}, error={}", documentId, stage, e.getMessage());
         }
     }
 }

+ 0 - 441
backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jGraphService.java

@@ -1,441 +0,0 @@
-package com.lingyue.graph.neo4j;
-
-import com.lingyue.graph.dto.KnowledgeGraphDTO;
-import com.lingyue.graph.dto.KnowledgeGraphDTO.*;
-import com.lingyue.graph.entity.GraphNode;
-import com.lingyue.graph.entity.GraphRelation;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.neo4j.driver.Driver;
-import org.neo4j.driver.Result;
-import org.neo4j.driver.Session;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-/**
- * Neo4j 图服务
- * 
- * 提供高级图操作功能:
- * - 同步 PostgreSQL 数据到 Neo4j
- * - 图遍历和路径查询
- * - 图分析(中心性、社区检测等)
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-@ConditionalOnProperty(name = "neo4j.enabled", havingValue = "true")
-public class Neo4jGraphService {
-    
-    private final Driver driver;
-    private final Neo4jNodeRepository nodeRepository;
-    private final Neo4jRelationRepository relationRepository;
-    
-    // 类型映射
-    private static final Map<String, String> TYPE_LABELS = Map.ofEntries(
-            Map.entry("person", "Person"),
-            Map.entry("org", "Organization"),
-            Map.entry("loc", "Location"),
-            Map.entry("location", "Location"),
-            Map.entry("date", "Date"),
-            Map.entry("number", "Number"),
-            Map.entry("money", "Money"),
-            Map.entry("data", "Data"),
-            Map.entry("concept", "Concept"),
-            Map.entry("device", "Device"),
-            Map.entry("term", "Term"),
-            Map.entry("entity", "Entity")
-    );
-    
-    private static final Map<String, String> TYPE_ICONS = Map.ofEntries(
-            Map.entry("person", "👤"),
-            Map.entry("org", "🏢"),
-            Map.entry("loc", "📍"),
-            Map.entry("location", "📍"),
-            Map.entry("date", "📅"),
-            Map.entry("number", "🔢"),
-            Map.entry("money", "💰"),
-            Map.entry("data", "📊"),
-            Map.entry("concept", "💡"),
-            Map.entry("device", "🔧"),
-            Map.entry("term", "📝"),
-            Map.entry("entity", "🏷️")
-    );
-    
-    private static final Map<String, String> TYPE_COLORS = Map.ofEntries(
-            Map.entry("person", "#1890ff"),
-            Map.entry("org", "#52c41a"),
-            Map.entry("loc", "#fa8c16"),
-            Map.entry("location", "#fa8c16"),
-            Map.entry("date", "#722ed1"),
-            Map.entry("number", "#13c2c2"),
-            Map.entry("money", "#52c41a"),
-            Map.entry("data", "#13c2c2"),
-            Map.entry("concept", "#722ed1"),
-            Map.entry("device", "#eb2f96"),
-            Map.entry("term", "#2f54eb"),
-            Map.entry("entity", "#1890ff")
-    );
-    
-    // ==================== 同步方法 ====================
-    
-    /**
-     * 同步单个节点到 Neo4j
-     */
-    public void syncNode(GraphNode node) {
-        String type = node.getType() != null ? node.getType().toLowerCase() : "entity";
-        String label = TYPE_LABELS.getOrDefault(type, "Entity");
-        
-        Map<String, Object> properties = new HashMap<>();
-        properties.put("id", node.getId());
-        properties.put("name", node.getName());
-        properties.put("type", type);
-        properties.put("value", node.getValue());
-        properties.put("documentId", node.getDocumentId());
-        properties.put("userId", node.getUserId());
-        properties.put("createdAt", node.getCreateTime() != null ? node.getCreateTime().toString() : null);
-        
-        // 添加位置信息
-        if (node.getPosition() instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> pos = (Map<String, Object>) node.getPosition();
-            properties.put("charStart", pos.get("charStart"));
-            properties.put("charEnd", pos.get("charEnd"));
-        }
-        
-        nodeRepository.createOrUpdateNode(node.getId(), List.of("Entity", label), properties);
-        log.debug("同步节点到 Neo4j: id={}, name={}", node.getId(), node.getName());
-    }
-    
-    /**
-     * 批量同步节点
-     */
-    public void syncNodes(List<GraphNode> nodes) {
-        List<Map<String, Object>> nodeDataList = nodes.stream()
-                .map(node -> {
-                    String type = node.getType() != null ? node.getType().toLowerCase() : "entity";
-                    String label = TYPE_LABELS.getOrDefault(type, "Entity");
-                    
-                    Map<String, Object> data = new HashMap<>();
-                    data.put("id", node.getId());
-                    data.put("labels", List.of("Entity", label));
-                    
-                    Map<String, Object> props = new HashMap<>();
-                    props.put("name", node.getName());
-                    props.put("type", type);
-                    props.put("value", node.getValue());
-                    props.put("documentId", node.getDocumentId());
-                    props.put("userId", node.getUserId());
-                    data.put("properties", props);
-                    
-                    return data;
-                })
-                .collect(Collectors.toList());
-        
-        nodeRepository.batchCreateNodes(nodeDataList);
-        log.info("批量同步节点到 Neo4j: count={}", nodes.size());
-    }
-    
-    /**
-     * 同步单个关系到 Neo4j
-     */
-    public void syncRelation(GraphRelation relation) {
-        Map<String, Object> properties = new HashMap<>();
-        properties.put("id", relation.getId());
-        properties.put("relationType", relation.getRelationType());
-        properties.put("actionType", relation.getActionType());
-        properties.put("orderIndex", relation.getOrderIndex());
-        
-        String relType = relation.getRelationType() != null ? relation.getRelationType() : "RELATED";
-        
-        relationRepository.createRelation(
-                relation.getFromNodeId(),
-                relation.getToNodeId(),
-                relType,
-                properties
-        );
-        
-        log.debug("同步关系到 Neo4j: from={}, to={}, type={}", 
-                relation.getFromNodeId(), relation.getToNodeId(), relType);
-    }
-    
-    /**
-     * 批量同步关系
-     */
-    public void syncRelations(List<GraphRelation> relations) {
-        List<Map<String, Object>> relDataList = relations.stream()
-                .map(rel -> {
-                    Map<String, Object> data = new HashMap<>();
-                    data.put("fromId", rel.getFromNodeId());
-                    data.put("toId", rel.getToNodeId());
-                    data.put("type", rel.getRelationType() != null ? rel.getRelationType() : "RELATED");
-                    
-                    Map<String, Object> props = new HashMap<>();
-                    props.put("id", rel.getId());
-                    props.put("actionType", rel.getActionType());
-                    props.put("orderIndex", rel.getOrderIndex());
-                    data.put("properties", props);
-                    
-                    return data;
-                })
-                .collect(Collectors.toList());
-        
-        relationRepository.batchCreateRelations(relDataList);
-        log.info("批量同步关系到 Neo4j: count={}", relations.size());
-    }
-    
-    // ==================== 图查询方法 ====================
-    
-    /**
-     * 获取文档的完整知识图谱(从 Neo4j)
-     */
-    public KnowledgeGraphDTO getDocumentGraph(String documentId) {
-        List<Map<String, Object>> nodes = nodeRepository.findByDocumentId(documentId);
-        if (nodes.isEmpty()) {
-            return null;
-        }
-        
-        List<Map<String, Object>> relations = relationRepository.findByDocumentId(documentId);
-        
-        // 转换节点
-        List<NodeDTO> nodeDTOs = nodes.stream()
-                .map(this::convertToNodeDTO)
-                .collect(Collectors.toList());
-        
-        // 计算关联数量
-        Map<String, Integer> relationCounts = new HashMap<>();
-        for (Map<String, Object> rel : relations) {
-            String fromId = (String) rel.get("fromId");
-            String toId = (String) rel.get("toId");
-            relationCounts.merge(fromId, 1, Integer::sum);
-            relationCounts.merge(toId, 1, Integer::sum);
-        }
-        
-        // 更新节点的关联数量和大小
-        for (NodeDTO node : nodeDTOs) {
-            int count = relationCounts.getOrDefault(node.getId(), 0);
-            node.setRelationCount(count);
-            node.setSize(40 + Math.min(count * 5, 30));
-        }
-        
-        // 转换边
-        List<EdgeDTO> edgeDTOs = relations.stream()
-                .map(rel -> EdgeDTO.builder()
-                        .id((String) rel.get("id"))
-                        .source((String) rel.get("fromId"))
-                        .sourceName((String) rel.get("fromName"))
-                        .target((String) rel.get("toId"))
-                        .targetName((String) rel.get("toName"))
-                        .relationType((String) rel.get("type"))
-                        .label((String) rel.get("type"))
-                        .weight(1.0)
-                        .build())
-                .collect(Collectors.toList());
-        
-        // 统计
-        GraphStats stats = GraphStats.builder()
-                .totalNodes(nodeDTOs.size())
-                .totalEdges(edgeDTOs.size())
-                .nodesByType(nodes.stream()
-                        .collect(Collectors.groupingBy(
-                                n -> (String) n.getOrDefault("type", "other"),
-                                Collectors.collectingAndThen(Collectors.counting(), Long::intValue))))
-                .edgesByType(relations.stream()
-                        .collect(Collectors.groupingBy(
-                                r -> (String) r.getOrDefault("type", "RELATED"),
-                                Collectors.collectingAndThen(Collectors.counting(), Long::intValue))))
-                .build();
-        
-        return KnowledgeGraphDTO.builder()
-                .documentId(documentId)
-                .title("标记要素关系图谱")
-                .nodes(nodeDTOs)
-                .edges(edgeDTOs)
-                .stats(stats)
-                .build();
-    }
-    
-    /**
-     * 查找两节点之间的最短路径
-     */
-    public List<Map<String, Object>> findShortestPath(String fromNodeId, String toNodeId, int maxDepth) {
-        String cypher = 
-                "MATCH path = shortestPath((a {id: $fromId})-[*1.." + maxDepth + "]-(b {id: $toId})) " +
-                "RETURN [n IN nodes(path) | {id: n.id, name: n.name, type: n.type}] AS nodes, " +
-                "       [r IN relationships(path) | {type: type(r)}] AS relations";
-        
-        List<Map<String, Object>> result = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result queryResult = session.run(cypher, Map.of("fromId", fromNodeId, "toId", toNodeId));
-            if (queryResult.hasNext()) {
-                var record = queryResult.next();
-                result = record.get("nodes").asList(v -> v.asMap());
-            }
-        }
-        
-        return result;
-    }
-    
-    /**
-     * 获取节点的 N 跳邻居
-     */
-    public KnowledgeGraphDTO getNeighborhood(String nodeId, int depth) {
-        String cypher = 
-                "MATCH path = (center {id: $nodeId})-[*1.." + depth + "]-(neighbor) " +
-                "WITH center, neighbor, relationships(path) AS rels " +
-                "RETURN DISTINCT neighbor, labels(neighbor) AS labels, " +
-                "       [r IN rels | {type: type(r), from: startNode(r).id, to: endNode(r).id}] AS relInfo";
-        
-        Set<String> nodeIds = new HashSet<>();
-        nodeIds.add(nodeId);
-        
-        List<NodeDTO> nodes = new ArrayList<>();
-        List<EdgeDTO> edges = new ArrayList<>();
-        Set<String> edgeKeys = new HashSet<>();
-        
-        // 添加中心节点
-        Map<String, Object> centerNode = nodeRepository.findById(nodeId);
-        if (centerNode != null) {
-            nodes.add(convertToNodeDTO(centerNode));
-        }
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("nodeId", nodeId));
-            
-            while (result.hasNext()) {
-                var record = result.next();
-                var neighbor = record.get("neighbor").asNode();
-                var labels = record.get("labels").asList(v -> v.asString());
-                var relInfo = record.get("relInfo").asList(v -> v.asMap());
-                
-                String neighborId = neighbor.get("id").asString();
-                
-                if (!nodeIds.contains(neighborId)) {
-                    nodeIds.add(neighborId);
-                    
-                    Map<String, Object> nodeMap = new HashMap<>(neighbor.asMap());
-                    nodeMap.put("_labels", labels);
-                    nodes.add(convertToNodeDTO(nodeMap));
-                }
-                
-                // 处理关系
-                for (var rel : relInfo) {
-                    String from = (String) rel.get("from");
-                    String to = (String) rel.get("to");
-                    String type = (String) rel.get("type");
-                    String edgeKey = from + "-" + to + "-" + type;
-                    
-                    if (!edgeKeys.contains(edgeKey)) {
-                        edgeKeys.add(edgeKey);
-                        edges.add(EdgeDTO.builder()
-                                .id(edgeKey)
-                                .source(from)
-                                .target(to)
-                                .relationType(type)
-                                .label(type)
-                                .build());
-                    }
-                }
-            }
-        }
-        
-        return KnowledgeGraphDTO.builder()
-                .title("邻域图谱")
-                .nodes(nodes)
-                .edges(edges)
-                .stats(GraphStats.builder()
-                        .totalNodes(nodes.size())
-                        .totalEdges(edges.size())
-                        .build())
-                .build();
-    }
-    
-    /**
-     * 获取高度中心性节点(PageRank)
-     * 需要 Graph Data Science 插件
-     */
-    public List<Map<String, Object>> getTopNodesByPageRank(String documentId, int limit) {
-        // 简化版:按关系数量排序
-        String cypher = 
-                "MATCH (n {documentId: $documentId})-[r]-() " +
-                "WITH n, count(r) AS degree " +
-                "ORDER BY degree DESC " +
-                "LIMIT $limit " +
-                "RETURN n.id AS id, n.name AS name, n.type AS type, degree";
-        
-        List<Map<String, Object>> result = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result queryResult = session.run(cypher, Map.of("documentId", documentId, "limit", limit));
-            while (queryResult.hasNext()) {
-                var record = queryResult.next();
-                result.add(Map.of(
-                        "id", record.get("id").asString(),
-                        "name", record.get("name").asString(),
-                        "type", record.get("type").asString(),
-                        "degree", record.get("degree").asInt()
-                ));
-            }
-        }
-        
-        return result;
-    }
-    
-    // ==================== 辅助方法 ====================
-    
-    private NodeDTO convertToNodeDTO(Map<String, Object> nodeMap) {
-        String type = (String) nodeMap.getOrDefault("type", "entity");
-        if (type == null) type = "entity";
-        type = type.toLowerCase();
-        
-        return NodeDTO.builder()
-                .id((String) nodeMap.get("id"))
-                .name((String) nodeMap.get("name"))
-                .type(type)
-                .icon(TYPE_ICONS.getOrDefault(type, "📌"))
-                .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                .size(40)
-                .value((String) nodeMap.get("value"))
-                .documentIds(nodeMap.get("documentId") != null 
-                        ? List.of((String) nodeMap.get("documentId")) 
-                        : Collections.emptyList())
-                .build();
-    }
-    
-    /**
-     * 删除文档的所有图数据
-     */
-    public void deleteDocumentGraph(String documentId) {
-        nodeRepository.deleteByDocumentId(documentId);
-        log.info("删除文档图数据: documentId={}", documentId);
-    }
-    
-    /**
-     * 初始化图数据库索引
-     */
-    public void initializeIndexes() {
-        String[] indexes = {
-                "CREATE INDEX node_id IF NOT EXISTS FOR (n:Entity) ON (n.id)",
-                "CREATE INDEX node_document IF NOT EXISTS FOR (n:Entity) ON (n.documentId)",
-                "CREATE INDEX node_user IF NOT EXISTS FOR (n:Entity) ON (n.userId)",
-                "CREATE INDEX node_name IF NOT EXISTS FOR (n:Entity) ON (n.name)"
-        };
-        
-        try (Session session = driver.session()) {
-            for (String index : indexes) {
-                try {
-                    session.run(index);
-                    log.info("创建索引成功: {}", index);
-                } catch (Exception e) {
-                    log.warn("创建索引失败(可能已存在): {}", e.getMessage());
-                }
-            }
-        }
-    }
-}

+ 0 - 313
backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jNodeRepository.java

@@ -1,313 +0,0 @@
-package com.lingyue.graph.neo4j;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.neo4j.driver.Driver;
-import org.neo4j.driver.Result;
-import org.neo4j.driver.Session;
-import org.neo4j.driver.Value;
-import org.neo4j.driver.types.Node;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Repository;
-
-import java.util.*;
-
-/**
- * Neo4j 节点仓库
- * 
- * 提供图节点的 CRUD 操作
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Repository
-@RequiredArgsConstructor
-@ConditionalOnProperty(name = "neo4j.enabled", havingValue = "true")
-public class Neo4jNodeRepository {
-    
-    private final Driver driver;
-    
-    /**
-     * 创建或更新节点
-     * 
-     * @param nodeId 节点ID
-     * @param labels 节点标签列表(如 Entity, Person, Concept)
-     * @param properties 节点属性
-     * @return 创建的节点ID
-     */
-    public String createOrUpdateNode(String nodeId, List<String> labels, Map<String, Object> properties) {
-        String labelString = String.join(":", labels);
-        
-        // 使用 MERGE 确保幂等性
-        String cypher = String.format(
-                "MERGE (n:%s {id: $id}) " +
-                "SET n += $props " +
-                "RETURN n.id AS id",
-                labelString
-        );
-        
-        Map<String, Object> params = new HashMap<>();
-        params.put("id", nodeId);
-        params.put("props", properties);
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, params);
-            if (result.hasNext()) {
-                return result.next().get("id").asString();
-            }
-        } catch (Exception e) {
-            log.error("创建节点失败: nodeId={}, error={}", nodeId, e.getMessage());
-            throw new RuntimeException("Neo4j 创建节点失败", e);
-        }
-        
-        return nodeId;
-    }
-    
-    /**
-     * 批量创建节点(使用 UNWIND 提高性能)
-     * 
-     * @param nodes 节点列表,每个节点包含 id, labels, properties
-     */
-    public void batchCreateNodes(List<Map<String, Object>> nodes) {
-        if (nodes == null || nodes.isEmpty()) {
-            return;
-        }
-        
-        String cypher = 
-                "UNWIND $nodes AS node " +
-                "CALL apoc.merge.node(node.labels, {id: node.id}, node.properties) YIELD node AS n " +
-                "RETURN count(n) AS count";
-        
-        // 如果没有 APOC 插件,使用简单版本
-        String simpleCypher = 
-                "UNWIND $nodes AS node " +
-                "MERGE (n:Entity {id: node.id}) " +
-                "SET n += node.properties " +
-                "RETURN count(n) AS count";
-        
-        try (Session session = driver.session()) {
-            try {
-                Result result = session.run(cypher, Map.of("nodes", nodes));
-                log.info("批量创建节点完成: count={}", result.single().get("count").asInt());
-            } catch (Exception e) {
-                // 回退到简单版本
-                log.warn("APOC 不可用,使用简单模式");
-                Result result = session.run(simpleCypher, Map.of("nodes", nodes));
-                log.info("批量创建节点完成: count={}", result.single().get("count").asInt());
-            }
-        }
-    }
-    
-    /**
-     * 根据ID查找节点
-     */
-    public Map<String, Object> findById(String nodeId) {
-        String cypher = "MATCH (n {id: $id}) RETURN n, labels(n) AS labels";
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("id", nodeId));
-            if (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("n").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                return nodeMap;
-            }
-        }
-        
-        return null;
-    }
-    
-    /**
-     * 根据文档ID查找所有节点
-     */
-    public List<Map<String, Object>> findByDocumentId(String documentId) {
-        String cypher = 
-                "MATCH (n) WHERE n.documentId = $documentId " +
-                "RETURN n, labels(n) AS labels";
-        
-        List<Map<String, Object>> nodes = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("n").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                nodes.add(nodeMap);
-            }
-        }
-        
-        return nodes;
-    }
-    
-    /**
-     * 根据用户ID查找所有节点
-     */
-    public List<Map<String, Object>> findByUserId(String userId, int limit) {
-        String cypher = 
-                "MATCH (n) WHERE n.userId = $userId " +
-                "RETURN n, labels(n) AS labels " +
-                "LIMIT $limit";
-        
-        List<Map<String, Object>> nodes = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("userId", userId, "limit", limit));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("n").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                nodes.add(nodeMap);
-            }
-        }
-        
-        return nodes;
-    }
-    
-    /**
-     * 按类型查找节点
-     */
-    public List<Map<String, Object>> findByType(String documentId, String type) {
-        String cypher = 
-                "MATCH (n) WHERE n.documentId = $documentId AND n.type = $type " +
-                "RETURN n, labels(n) AS labels";
-        
-        List<Map<String, Object>> nodes = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId, "type", type));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("n").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                nodes.add(nodeMap);
-            }
-        }
-        
-        return nodes;
-    }
-    
-    /**
-     * 搜索节点(按名称模糊匹配)
-     */
-    public List<Map<String, Object>> searchByName(String keyword, String documentId, int limit) {
-        String cypher;
-        Map<String, Object> params = new HashMap<>();
-        params.put("keyword", "(?i).*" + keyword + ".*");
-        params.put("limit", limit);
-        
-        if (documentId != null && !documentId.isEmpty()) {
-            cypher = 
-                    "MATCH (n) WHERE n.documentId = $documentId AND n.name =~ $keyword " +
-                    "RETURN n, labels(n) AS labels " +
-                    "LIMIT $limit";
-            params.put("documentId", documentId);
-        } else {
-            cypher = 
-                    "MATCH (n) WHERE n.name =~ $keyword " +
-                    "RETURN n, labels(n) AS labels " +
-                    "LIMIT $limit";
-        }
-        
-        List<Map<String, Object>> nodes = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, params);
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("n").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                nodes.add(nodeMap);
-            }
-        }
-        
-        return nodes;
-    }
-    
-    /**
-     * 删除节点
-     */
-    public void deleteById(String nodeId) {
-        String cypher = "MATCH (n {id: $id}) DETACH DELETE n";
-        
-        try (Session session = driver.session()) {
-            session.run(cypher, Map.of("id", nodeId));
-            log.info("删除节点: nodeId={}", nodeId);
-        }
-    }
-    
-    /**
-     * 删除文档的所有节点和关系
-     */
-    public void deleteByDocumentId(String documentId) {
-        String cypher = "MATCH (n) WHERE n.documentId = $documentId DETACH DELETE n";
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            log.info("删除文档节点: documentId={}", documentId);
-        }
-    }
-    
-    /**
-     * 获取节点的所有关联节点
-     */
-    public List<Map<String, Object>> findRelatedNodes(String nodeId) {
-        String cypher = 
-                "MATCH (n {id: $id})-[r]-(m) " +
-                "RETURN m, labels(m) AS labels, type(r) AS relationType, " +
-                "       CASE WHEN startNode(r) = n THEN 'outgoing' ELSE 'incoming' END AS direction";
-        
-        List<Map<String, Object>> nodes = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("id", nodeId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Node node = record.get("m").asNode();
-                List<String> labels = record.get("labels").asList(Value::asString);
-                String relationType = record.get("relationType").asString();
-                String direction = record.get("direction").asString();
-                
-                Map<String, Object> nodeMap = new HashMap<>(node.asMap());
-                nodeMap.put("_labels", labels);
-                nodeMap.put("_relationType", relationType);
-                nodeMap.put("_direction", direction);
-                nodes.add(nodeMap);
-            }
-        }
-        
-        return nodes;
-    }
-    
-    /**
-     * 统计节点数量
-     */
-    public long countByDocumentId(String documentId) {
-        String cypher = "MATCH (n) WHERE n.documentId = $documentId RETURN count(n) AS count";
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            if (result.hasNext()) {
-                return result.next().get("count").asLong();
-            }
-        }
-        
-        return 0;
-    }
-}

+ 0 - 271
backend/graph-service/src/main/java/com/lingyue/graph/neo4j/Neo4jRelationRepository.java

@@ -1,271 +0,0 @@
-package com.lingyue.graph.neo4j;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.neo4j.driver.Driver;
-import org.neo4j.driver.Result;
-import org.neo4j.driver.Session;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Repository;
-
-import java.util.*;
-
-/**
- * Neo4j 关系仓库
- * 
- * 提供图关系的 CRUD 操作
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Repository
-@RequiredArgsConstructor
-@ConditionalOnProperty(name = "neo4j.enabled", havingValue = "true")
-public class Neo4jRelationRepository {
-    
-    private final Driver driver;
-    
-    /**
-     * 创建关系
-     * 
-     * @param fromNodeId 源节点ID
-     * @param toNodeId 目标节点ID
-     * @param relationType 关系类型
-     * @param properties 关系属性
-     * @return 关系ID
-     */
-    public String createRelation(String fromNodeId, String toNodeId, String relationType, 
-                                  Map<String, Object> properties) {
-        // 动态关系类型需要使用 APOC 或字符串拼接
-        String cypher = String.format(
-                "MATCH (a {id: $fromId}), (b {id: $toId}) " +
-                "MERGE (a)-[r:%s]->(b) " +
-                "SET r += $props " +
-                "RETURN id(r) AS relId",
-                sanitizeRelationType(relationType)
-        );
-        
-        Map<String, Object> params = new HashMap<>();
-        params.put("fromId", fromNodeId);
-        params.put("toId", toNodeId);
-        params.put("props", properties != null ? properties : Collections.emptyMap());
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, params);
-            if (result.hasNext()) {
-                long relId = result.next().get("relId").asLong();
-                return String.valueOf(relId);
-            }
-        } catch (Exception e) {
-            log.error("创建关系失败: from={}, to={}, type={}, error={}", 
-                    fromNodeId, toNodeId, relationType, e.getMessage());
-            throw new RuntimeException("Neo4j 创建关系失败", e);
-        }
-        
-        return null;
-    }
-    
-    /**
-     * 批量创建关系
-     * 
-     * @param relations 关系列表,每个包含 fromId, toId, type, properties
-     */
-    public void batchCreateRelations(List<Map<String, Object>> relations) {
-        if (relations == null || relations.isEmpty()) {
-            return;
-        }
-        
-        // 按关系类型分组处理(因为 Cypher 不支持动态关系类型)
-        Map<String, List<Map<String, Object>>> byType = new HashMap<>();
-        for (Map<String, Object> rel : relations) {
-            String type = (String) rel.getOrDefault("type", "RELATED");
-            byType.computeIfAbsent(type, k -> new ArrayList<>()).add(rel);
-        }
-        
-        try (Session session = driver.session()) {
-            for (Map.Entry<String, List<Map<String, Object>>> entry : byType.entrySet()) {
-                String relationType = sanitizeRelationType(entry.getKey());
-                List<Map<String, Object>> typeRelations = entry.getValue();
-                
-                String cypher = String.format(
-                        "UNWIND $relations AS rel " +
-                        "MATCH (a {id: rel.fromId}), (b {id: rel.toId}) " +
-                        "MERGE (a)-[r:%s]->(b) " +
-                        "SET r += rel.properties " +
-                        "RETURN count(r) AS count",
-                        relationType
-                );
-                
-                Result result = session.run(cypher, Map.of("relations", typeRelations));
-                log.info("批量创建关系完成: type={}, count={}", 
-                        relationType, result.single().get("count").asInt());
-            }
-        }
-    }
-    
-    /**
-     * 查找节点的所有关系
-     */
-    public List<Map<String, Object>> findByNodeId(String nodeId) {
-        String cypher = 
-                "MATCH (n {id: $nodeId})-[r]-(m) " +
-                "RETURN id(r) AS relId, type(r) AS relationType, " +
-                "       startNode(r).id AS fromId, endNode(r).id AS toId, " +
-                "       properties(r) AS properties";
-        
-        List<Map<String, Object>> relations = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("nodeId", nodeId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Map<String, Object> rel = new HashMap<>();
-                rel.put("id", String.valueOf(record.get("relId").asLong()));
-                rel.put("type", record.get("relationType").asString());
-                rel.put("fromId", record.get("fromId").asString());
-                rel.put("toId", record.get("toId").asString());
-                rel.put("properties", record.get("properties").asMap());
-                relations.add(rel);
-            }
-        }
-        
-        return relations;
-    }
-    
-    /**
-     * 查找两个节点之间的关系
-     */
-    public List<Map<String, Object>> findBetween(String fromNodeId, String toNodeId) {
-        String cypher = 
-                "MATCH (a {id: $fromId})-[r]->(b {id: $toId}) " +
-                "RETURN id(r) AS relId, type(r) AS relationType, properties(r) AS properties";
-        
-        List<Map<String, Object>> relations = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("fromId", fromNodeId, "toId", toNodeId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Map<String, Object> rel = new HashMap<>();
-                rel.put("id", String.valueOf(record.get("relId").asLong()));
-                rel.put("type", record.get("relationType").asString());
-                rel.put("fromId", fromNodeId);
-                rel.put("toId", toNodeId);
-                rel.put("properties", record.get("properties").asMap());
-                relations.add(rel);
-            }
-        }
-        
-        return relations;
-    }
-    
-    /**
-     * 按文档查找所有关系
-     */
-    public List<Map<String, Object>> findByDocumentId(String documentId) {
-        String cypher = 
-                "MATCH (a)-[r]->(b) " +
-                "WHERE a.documentId = $documentId AND b.documentId = $documentId " +
-                "RETURN id(r) AS relId, type(r) AS relationType, " +
-                "       a.id AS fromId, a.name AS fromName, " +
-                "       b.id AS toId, b.name AS toName, " +
-                "       properties(r) AS properties";
-        
-        List<Map<String, Object>> relations = new ArrayList<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                Map<String, Object> rel = new HashMap<>();
-                rel.put("id", String.valueOf(record.get("relId").asLong()));
-                rel.put("type", record.get("relationType").asString());
-                rel.put("fromId", record.get("fromId").asString());
-                rel.put("fromName", record.get("fromName").asString());
-                rel.put("toId", record.get("toId").asString());
-                rel.put("toName", record.get("toName").asString());
-                rel.put("properties", record.get("properties").asMap());
-                relations.add(rel);
-            }
-        }
-        
-        return relations;
-    }
-    
-    /**
-     * 删除关系
-     */
-    public void deleteById(long relationId) {
-        String cypher = "MATCH ()-[r]->() WHERE id(r) = $relId DELETE r";
-        
-        try (Session session = driver.session()) {
-            session.run(cypher, Map.of("relId", relationId));
-            log.info("删除关系: relId={}", relationId);
-        }
-    }
-    
-    /**
-     * 删除两节点间的所有关系
-     */
-    public void deleteBetween(String fromNodeId, String toNodeId) {
-        String cypher = "MATCH (a {id: $fromId})-[r]->(b {id: $toId}) DELETE r";
-        
-        try (Session session = driver.session()) {
-            session.run(cypher, Map.of("fromId", fromNodeId, "toId", toNodeId));
-            log.info("删除关系: from={}, to={}", fromNodeId, toNodeId);
-        }
-    }
-    
-    /**
-     * 统计关系数量
-     */
-    public long countByDocumentId(String documentId) {
-        String cypher = 
-                "MATCH (a {documentId: $documentId})-[r]->(b {documentId: $documentId}) " +
-                "RETURN count(r) AS count";
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            if (result.hasNext()) {
-                return result.next().get("count").asLong();
-            }
-        }
-        
-        return 0;
-    }
-    
-    /**
-     * 获取关系类型统计
-     */
-    public Map<String, Long> getRelationTypeStats(String documentId) {
-        String cypher = 
-                "MATCH (a {documentId: $documentId})-[r]->(b {documentId: $documentId}) " +
-                "RETURN type(r) AS relationType, count(r) AS count";
-        
-        Map<String, Long> stats = new HashMap<>();
-        
-        try (Session session = driver.session()) {
-            Result result = session.run(cypher, Map.of("documentId", documentId));
-            while (result.hasNext()) {
-                org.neo4j.driver.Record record = result.next();
-                stats.put(record.get("relationType").asString(), record.get("count").asLong());
-            }
-        }
-        
-        return stats;
-    }
-    
-    /**
-     * 清理关系类型名称(Neo4j 关系类型有限制)
-     */
-    private String sanitizeRelationType(String type) {
-        if (type == null || type.isEmpty()) {
-            return "RELATED";
-        }
-        // 只保留字母、数字和下划线,转大写
-        return type.toUpperCase()
-                .replaceAll("[^A-Z0-9_]", "_")
-                .replaceAll("^[0-9]", "_$0"); // 不能以数字开头
-    }
-}

+ 0 - 57
backend/graph-service/src/main/java/com/lingyue/graph/repository/GraphNodeRepository.java

@@ -1,57 +0,0 @@
-package com.lingyue.graph.repository;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lingyue.graph.entity.GraphNode;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Select;
-
-import java.util.List;
-
-/**
- * 图节点Repository
- * 
- * @author lingyue
- * @since 2026-01-14
- */
-@Mapper
-public interface GraphNodeRepository extends BaseMapper<GraphNode> {
-    
-    /**
-     * 根据文档ID查询节点列表
-     * 
-     * @param documentId 文档ID
-     * @return 节点列表
-     */
-    @Select("SELECT * FROM graph_nodes WHERE document_id = #{documentId} ORDER BY level, create_time")
-    List<GraphNode> findByDocumentId(@Param("documentId") String documentId);
-    
-    /**
-     * 根据父节点ID查询子节点
-     * 
-     * @param parentId 父节点ID
-     * @return 子节点列表
-     */
-    @Select("SELECT * FROM graph_nodes WHERE parent_id = #{parentId} ORDER BY create_time")
-    List<GraphNode> findByParentId(@Param("parentId") String parentId);
-    
-    /**
-     * 根据文档ID和类型查询节点
-     * 
-     * @param documentId 文档ID
-     * @param type 节点类型
-     * @return 节点列表
-     */
-    @Select("SELECT * FROM graph_nodes WHERE document_id = #{documentId} AND type = #{type}")
-    List<GraphNode> findByDocumentIdAndType(@Param("documentId") String documentId, 
-                                            @Param("type") String type);
-    
-    /**
-     * 根据用户ID查询节点列表
-     * 
-     * @param userId 用户ID
-     * @return 节点列表
-     */
-    @Select("SELECT * FROM graph_nodes WHERE user_id = #{userId} ORDER BY create_time DESC")
-    List<GraphNode> findByUserId(@Param("userId") String userId);
-}

+ 0 - 55
backend/graph-service/src/main/java/com/lingyue/graph/repository/GraphRelationRepository.java

@@ -1,55 +0,0 @@
-package com.lingyue.graph.repository;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lingyue.graph.entity.GraphRelation;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Select;
-
-import java.util.List;
-
-/**
- * 图关系Repository
- * 
- * @author lingyue
- * @since 2026-01-14
- */
-@Mapper
-public interface GraphRelationRepository extends BaseMapper<GraphRelation> {
-    
-    /**
-     * 根据源节点ID查询关系列表
-     * 
-     * @param fromNodeId 源节点ID
-     * @return 关系列表
-     */
-    @Select("SELECT * FROM graph_relations WHERE from_node_id = #{fromNodeId} ORDER BY order_index")
-    List<GraphRelation> findByFromNodeId(@Param("fromNodeId") String fromNodeId);
-    
-    /**
-     * 根据目标节点ID查询关系列表
-     * 
-     * @param toNodeId 目标节点ID
-     * @return 关系列表
-     */
-    @Select("SELECT * FROM graph_relations WHERE to_node_id = #{toNodeId}")
-    List<GraphRelation> findByToNodeId(@Param("toNodeId") String toNodeId);
-    
-    /**
-     * 查询节点的所有关系(包括作为源节点和目标节点)
-     * 
-     * @param nodeId 节点ID
-     * @return 关系列表
-     */
-    @Select("SELECT * FROM graph_relations WHERE from_node_id = #{nodeId} OR to_node_id = #{nodeId}")
-    List<GraphRelation> findByNodeId(@Param("nodeId") String nodeId);
-    
-    /**
-     * 根据关系类型查询
-     * 
-     * @param relationType 关系类型
-     * @return 关系列表
-     */
-    @Select("SELECT * FROM graph_relations WHERE relation_type = #{relationType}")
-    List<GraphRelation> findByRelationType(@Param("relationType") String relationType);
-}

+ 1 - 26
backend/graph-service/src/main/java/com/lingyue/graph/service/DataSourceService.java

@@ -7,9 +7,7 @@ import com.lingyue.document.entity.DocumentElement;
 import com.lingyue.document.repository.DocumentElementRepository;
 import com.lingyue.graph.dto.*;
 import com.lingyue.graph.entity.DataSource;
-import com.lingyue.graph.entity.GraphNode;
 import com.lingyue.graph.repository.DataSourceRepository;
-import com.lingyue.graph.repository.GraphNodeRepository;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -31,7 +29,6 @@ import java.util.stream.Collectors;
 public class DataSourceService {
     
     private final DataSourceRepository dataSourceRepository;
-    private final GraphNodeRepository graphNodeRepository;
     private final DocumentElementRepository documentElementRepository;
     private final ObjectMapper objectMapper;
     
@@ -267,17 +264,7 @@ public class DataSourceService {
      */
     private List<NodeValueItem> fetchNodeValues(NodeRefs refs) {
         List<NodeValueItem> items = new ArrayList<>();
-        
-        // 获取图节点
-        List<String> graphNodeIds = refs.getGraphNodeIds();
-        if (!graphNodeIds.isEmpty()) {
-            List<GraphNode> graphNodes = graphNodeRepository.selectBatchIds(graphNodeIds);
-            for (GraphNode node : graphNodes) {
-                items.add(createValueItemFromGraphNode(node));
-            }
-        }
-        
-        // 获取文档元素
+        // 图节点表已移除,仅支持文档元素
         List<String> elementIds = refs.getDocumentElementIds();
         if (!elementIds.isEmpty()) {
             List<DocumentElement> elements = documentElementRepository.selectBatchIds(elementIds);
@@ -289,18 +276,6 @@ public class DataSourceService {
         return items;
     }
     
-    /**
-     * 从图节点创建值项
-     */
-    private NodeValueItem createValueItemFromGraphNode(GraphNode node) {
-        return NodeValueItem.text(
-                NodeRef.TYPE_GRAPH_NODE,
-                node.getId(),
-                node.getValue(),
-                node.getName()
-        );
-    }
-    
     /**
      * 从文档元素创建值项
      */

+ 0 - 315
backend/graph-service/src/main/java/com/lingyue/graph/service/DocumentBlockGeneratorService.java

@@ -1,315 +0,0 @@
-package com.lingyue.graph.service;
-
-import com.lingyue.graph.service.NerToBlockService.TextElementDTO;
-import lombok.Data;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * 文档块生成服务
- * 
- * 职责:
- * 1. 将纯文本拆分为段落块
- * 2. 识别标题、列表等结构
- * 3. 结合 NER 结果生成完整的 Block 结构
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class DocumentBlockGeneratorService {
-    
-    private final NerToBlockService nerToBlockService;
-    
-    // 标题模式:一、二、三... 或 1. 2. 3...
-    private static final Pattern HEADING_PATTERN_CN = Pattern.compile("^[一二三四五六七八九十]+、.+");
-    private static final Pattern HEADING_PATTERN_NUM = Pattern.compile("^(\\d+\\.)+\\s*.+");
-    private static final Pattern SUBHEADING_PATTERN = Pattern.compile("^(\\d+\\.\\d+)\\s+.+");
-    
-    // 列表模式
-    private static final Pattern BULLET_PATTERN = Pattern.compile("^[•●○▪\\-\\*]\\s+.+");
-    private static final Pattern ORDERED_PATTERN = Pattern.compile("^\\d+[.、))]\\s+.+");
-    
-    /**
-     * 将纯文本和 NER 实体生成完整的 Block 结构
-     * 
-     * @param documentId 文档ID
-     * @param text 纯文本内容
-     * @param entities NER 提取的实体列表
-     * @return Block 列表
-     */
-    public List<BlockDTO> generateBlocks(String documentId, String text, List<Map<String, Object>> entities) {
-        if (text == null || text.isEmpty()) {
-            return Collections.emptyList();
-        }
-        
-        // 1. 将文本拆分为段落
-        List<ParagraphInfo> paragraphs = splitIntoParagraphs(text);
-        
-        // 2. 为每个段落生成 Block
-        List<BlockDTO> blocks = new ArrayList<>();
-        String rootBlockId = UUID.randomUUID().toString().replace("-", "");
-        
-        // 创建根块(page)
-        BlockDTO rootBlock = new BlockDTO();
-        rootBlock.setBlockId(rootBlockId);
-        rootBlock.setDocumentId(documentId);
-        rootBlock.setBlockType("page");
-        rootBlock.setBlockIndex(0);
-        rootBlock.setChildren(new ArrayList<>());
-        blocks.add(rootBlock);
-        
-        // 为每个段落创建 Block
-        int blockIndex = 1;
-        for (ParagraphInfo para : paragraphs) {
-            if (para.getText().trim().isEmpty()) {
-                continue; // 跳过空段落
-            }
-            
-            BlockDTO block = new BlockDTO();
-            block.setBlockId(UUID.randomUUID().toString().replace("-", ""));
-            block.setDocumentId(documentId);
-            block.setParentId(rootBlockId);
-            block.setBlockIndex(blockIndex++);
-            block.setBlockType(detectBlockType(para.getText()));
-            block.setCharStart(para.getCharStart());
-            block.setCharEnd(para.getCharEnd());
-            
-            // 筛选属于该段落的实体
-            List<Map<String, Object>> paragraphEntities = filterEntitiesForParagraph(
-                    entities, para.getCharStart(), para.getCharEnd());
-            
-            // 将实体偏移转换为段落内偏移
-            List<Map<String, Object>> localizedEntities = localizeEntities(
-                    paragraphEntities, para.getCharStart());
-            
-            // 生成 TextElement 列表
-            List<TextElementDTO> elements = nerToBlockService.convertToTextElements(
-                    para.getText(), localizedEntities);
-            block.setElements(elements);
-            
-            blocks.add(block);
-            rootBlock.getChildren().add(block.getBlockId());
-        }
-        
-        log.info("生成文档块完成: documentId={}, blockCount={}", documentId, blocks.size());
-        return blocks;
-    }
-    
-    /**
-     * 将文本拆分为段落
-     */
-    private List<ParagraphInfo> splitIntoParagraphs(String text) {
-        List<ParagraphInfo> paragraphs = new ArrayList<>();
-        
-        // 按换行符拆分
-        String[] lines = text.split("\n");
-        int currentPos = 0;
-        StringBuilder currentParagraph = new StringBuilder();
-        int paragraphStart = 0;
-        
-        for (String line : lines) {
-            if (line.trim().isEmpty()) {
-                // 空行,结束当前段落
-                if (currentParagraph.length() > 0) {
-                    ParagraphInfo para = new ParagraphInfo();
-                    para.setText(currentParagraph.toString());
-                    para.setCharStart(paragraphStart);
-                    para.setCharEnd(currentPos);
-                    paragraphs.add(para);
-                    currentParagraph = new StringBuilder();
-                }
-                paragraphStart = currentPos + line.length() + 1;
-            } else {
-                // 检查是否是新段落的开始(标题、列表等)
-                if (isNewParagraphStart(line) && currentParagraph.length() > 0) {
-                    // 保存当前段落
-                    ParagraphInfo para = new ParagraphInfo();
-                    para.setText(currentParagraph.toString());
-                    para.setCharStart(paragraphStart);
-                    para.setCharEnd(currentPos);
-                    paragraphs.add(para);
-                    
-                    currentParagraph = new StringBuilder();
-                    paragraphStart = currentPos;
-                }
-                
-                if (currentParagraph.length() > 0) {
-                    currentParagraph.append(" "); // 多行合并时用空格连接
-                }
-                currentParagraph.append(line.trim());
-            }
-            
-            currentPos += line.length() + 1; // +1 for newline
-        }
-        
-        // 处理最后一个段落
-        if (currentParagraph.length() > 0) {
-            ParagraphInfo para = new ParagraphInfo();
-            para.setText(currentParagraph.toString());
-            para.setCharStart(paragraphStart);
-            para.setCharEnd(text.length());
-            paragraphs.add(para);
-        }
-        
-        return paragraphs;
-    }
-    
-    /**
-     * 判断是否是新段落的开始
-     */
-    private boolean isNewParagraphStart(String line) {
-        String trimmed = line.trim();
-        return HEADING_PATTERN_CN.matcher(trimmed).matches()
-                || HEADING_PATTERN_NUM.matcher(trimmed).matches()
-                || SUBHEADING_PATTERN.matcher(trimmed).matches()
-                || BULLET_PATTERN.matcher(trimmed).matches()
-                || ORDERED_PATTERN.matcher(trimmed).matches();
-    }
-    
-    /**
-     * 检测块类型
-     */
-    private String detectBlockType(String text) {
-        String trimmed = text.trim();
-        
-        // 标题检测
-        if (HEADING_PATTERN_CN.matcher(trimmed).matches()) {
-            // 一、二、三... 格式的一级标题
-            return "heading1";
-        }
-        
-        Matcher numMatcher = HEADING_PATTERN_NUM.matcher(trimmed);
-        if (numMatcher.matches()) {
-            // 计算数字层级
-            String prefix = trimmed.split("\\s")[0];
-            int dotCount = (int) prefix.chars().filter(c -> c == '.').count();
-            if (dotCount <= 1) return "heading1";
-            if (dotCount == 2) return "heading2";
-            return "heading3";
-        }
-        
-        if (SUBHEADING_PATTERN.matcher(trimmed).matches()) {
-            return "heading2";
-        }
-        
-        // 列表检测
-        if (BULLET_PATTERN.matcher(trimmed).matches()) {
-            return "bullet";
-        }
-        
-        if (ORDERED_PATTERN.matcher(trimmed).matches()) {
-            return "ordered";
-        }
-        
-        // 默认为普通段落
-        return "text";
-    }
-    
-    /**
-     * 筛选属于指定段落范围的实体
-     */
-    private List<Map<String, Object>> filterEntitiesForParagraph(List<Map<String, Object>> entities,
-                                                                   int paragraphStart, int paragraphEnd) {
-        if (entities == null) {
-            return Collections.emptyList();
-        }
-        
-        List<Map<String, Object>> result = new ArrayList<>();
-        
-        for (Map<String, Object> entity : entities) {
-            int charStart = getEntityCharStart(entity);
-            int charEnd = getEntityCharEnd(entity);
-            
-            // 实体完全在段落范围内
-            if (charStart >= paragraphStart && charEnd <= paragraphEnd) {
-                result.add(entity);
-            }
-        }
-        
-        return result;
-    }
-    
-    /**
-     * 将实体的全局偏移转换为段落内偏移
-     */
-    private List<Map<String, Object>> localizeEntities(List<Map<String, Object>> entities, int offset) {
-        List<Map<String, Object>> result = new ArrayList<>();
-        
-        for (Map<String, Object> entity : entities) {
-            Map<String, Object> localized = new HashMap<>(entity);
-            
-            Object positionObj = entity.get("position");
-            if (positionObj instanceof Map) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> posMap = new HashMap<>((Map<String, Object>) positionObj);
-                int charStart = getIntValue(posMap, "charStart", 0);
-                int charEnd = getIntValue(posMap, "charEnd", 0);
-                posMap.put("charStart", charStart - offset);
-                posMap.put("charEnd", charEnd - offset);
-                localized.put("position", posMap);
-            }
-            
-            result.add(localized);
-        }
-        
-        return result;
-    }
-    
-    private int getEntityCharStart(Map<String, Object> entity) {
-        Object positionObj = entity.get("position");
-        if (positionObj instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> posMap = (Map<String, Object>) positionObj;
-            return getIntValue(posMap, "charStart", -1);
-        }
-        return -1;
-    }
-    
-    private int getEntityCharEnd(Map<String, Object> entity) {
-        Object positionObj = entity.get("position");
-        if (positionObj instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> posMap = (Map<String, Object>) positionObj;
-            return getIntValue(posMap, "charEnd", -1);
-        }
-        return -1;
-    }
-    
-    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;
-    }
-    
-    // ==================== 数据传输对象 ====================
-    
-    @Data
-    public static class BlockDTO {
-        private String blockId;
-        private String documentId;
-        private String parentId;
-        private List<String> children;
-        private Integer blockIndex;
-        private String blockType;
-        private Integer charStart;
-        private Integer charEnd;
-        private List<TextElementDTO> elements;
-    }
-    
-    @Data
-    private static class ParagraphInfo {
-        private String text;
-        private int charStart;
-        private int charEnd;
-    }
-}

+ 0 - 320
backend/graph-service/src/main/java/com/lingyue/graph/service/GraphNerService.java

@@ -1,320 +0,0 @@
-package com.lingyue.graph.service;
-
-import com.lingyue.common.exception.ServiceException;
-import com.lingyue.graph.entity.GraphNode;
-import com.lingyue.graph.entity.GraphRelation;
-import com.lingyue.graph.entity.TextStorage;
-import com.lingyue.graph.repository.GraphNodeRepository;
-import com.lingyue.graph.repository.GraphRelationRepository;
-import com.lingyue.graph.repository.TextStorageRepository;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.*;
-
-/**
- * 图数据库 NER 服务
- * 
- * 负责将 NER 提取结果保存到图数据库。
- * 此服务位于 graph-service 模块,可以直接访问图数据库相关的实体和仓库。
- *
- * @author lingyue
- * @since 2026-01-19
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class GraphNerService {
-
-    private final TextStorageRepository textStorageRepository;
-    private final GraphNodeRepository graphNodeRepository;
-    private final GraphRelationRepository graphRelationRepository;
-    private final GraphSyncService graphSyncService;
-
-    /**
-     * 获取文档的文本内容
-     *
-     * @param documentId 文档ID
-     * @return 文本内容
-     */
-    public String getDocumentText(String documentId) {
-        TextStorage textStorage = textStorageRepository.findByDocumentId(documentId);
-        if (textStorage == null) {
-            throw new ServiceException("文档文本存储记录不存在: documentId=" + documentId);
-        }
-        
-        String filePath = textStorage.getFilePath();
-        try {
-            return Files.readString(Path.of(filePath), StandardCharsets.UTF_8);
-        } catch (Exception e) {
-            throw new ServiceException("读取文档文本失败: " + e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 检查文档是否存在文本存储
-     *
-     * @param documentId 文档ID
-     * @return 是否存在
-     */
-    public boolean hasDocumentText(String documentId) {
-        TextStorage textStorage = textStorageRepository.findByDocumentId(documentId);
-        return textStorage != null;
-    }
-
-    /**
-     * 批量保存 NER 实体到图数据库
-     *
-     * @param documentId 文档ID
-     * @param userId     用户ID
-     * @param entities   实体列表(包含 name, type, value, position, context, confidence, tempId)
-     * @return tempId 到实际 nodeId 的映射
-     */
-    @Transactional
-    public Map<String, String> saveEntitiesToGraph(String documentId, String userId, 
-                                                    List<Map<String, Object>> entities) {
-        Map<String, String> tempIdToNodeId = new HashMap<>();
-        
-        if (entities == null || entities.isEmpty()) {
-            log.debug("无实体需要保存");
-            return tempIdToNodeId;
-        }
-        
-        log.info("开始保存实体到图数据库: documentId={}, count={}", documentId, entities.size());
-        
-        for (Map<String, Object> entity : entities) {
-            GraphNode node = new GraphNode();
-            node.setId(UUID.randomUUID().toString().replace("-", ""));
-            node.setDocumentId(documentId);
-            node.setUserId(userId);  // 可为 null,自动 NER 提取时没有用户上下文
-            node.setName(getStringValue(entity, "name"));
-            node.setType(getStringValue(entity, "type", "other").toLowerCase());
-            node.setValue(getStringValue(entity, "value"));
-            node.setLevel(0);
-            node.setCreateTime(new Date());
-            node.setUpdateTime(new Date());
-            
-            // 转换位置信息(直接使用字符偏移)
-            Object positionObj = entity.get("position");
-            if (positionObj instanceof Map) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> posMap = (Map<String, Object>) positionObj;
-                log.debug("实体位置信息: name={}, position={}", node.getName(), posMap);
-                node.setPosition(posMap);
-            } else {
-                log.debug("实体无位置信息: name={}, positionObj={}", node.getName(), positionObj);
-            }
-            
-            // 保存元数据
-            Map<String, Object> metadata = new HashMap<>();
-            String context = getStringValue(entity, "context");
-            if (context != null) {
-                metadata.put("context", context);
-            }
-            Object confidence = entity.get("confidence");
-            if (confidence != null) {
-                metadata.put("confidence", confidence);
-            }
-            metadata.put("source", "ner");
-            node.setMetadata(metadata);
-            
-            graphNodeRepository.insert(node);
-            
-            // 同步到 Neo4j
-            graphSyncService.syncNode(node);
-            
-            // 记录 tempId 到 nodeId 的映射
-            String tempId = getStringValue(entity, "tempId");
-            if (tempId != null) {
-                tempIdToNodeId.put(tempId, node.getId());
-            }
-        }
-        
-        log.info("实体保存完成: documentId={}, savedCount={}", documentId, entities.size());
-        return tempIdToNodeId;
-    }
-
-    /**
-     * 批量保存 NER 关系到图数据库
-     *
-     * @param relations       关系列表
-     * @param tempIdToNodeId  tempId 到 nodeId 的映射
-     * @return 保存成功的数量
-     */
-    @Transactional
-    public int saveRelationsToGraph(List<Map<String, Object>> relations, 
-                                     Map<String, String> tempIdToNodeId) {
-        if (relations == null || relations.isEmpty()) {
-            log.debug("无关系需要保存");
-            return 0;
-        }
-        
-        log.info("开始保存关系到图数据库: count={}", relations.size());
-        
-        int savedCount = 0;
-        for (Map<String, Object> relation : relations) {
-            // 通过 tempId 获取实际的 nodeId
-            String fromEntityId = getStringValue(relation, "fromEntityId");
-            String toEntityId = getStringValue(relation, "toEntityId");
-            
-            String fromNodeId = fromEntityId != null ? tempIdToNodeId.get(fromEntityId) : null;
-            String toNodeId = toEntityId != null ? tempIdToNodeId.get(toEntityId) : null;
-            
-            // 如果无法找到对应的节点,跳过
-            if (fromNodeId == null || toNodeId == null) {
-                log.debug("跳过关系保存(节点不存在): from={}, to={}", 
-                        getStringValue(relation, "fromEntity"), 
-                        getStringValue(relation, "toEntity"));
-                continue;
-            }
-            
-            GraphRelation graphRelation = new GraphRelation();
-            graphRelation.setId(UUID.randomUUID().toString().replace("-", ""));
-            graphRelation.setFromNodeId(fromNodeId);
-            graphRelation.setToNodeId(toNodeId);
-            graphRelation.setRelationType(mapRelationType(getStringValue(relation, "relationType")));
-            graphRelation.setOrderIndex(0);
-            graphRelation.setCreateTime(new Date());
-            graphRelation.setUpdateTime(new Date());
-            
-            // 保存元数据
-            Map<String, Object> metadata = new HashMap<>();
-            Object confidence = relation.get("confidence");
-            if (confidence != null) {
-                metadata.put("confidence", confidence);
-            }
-            metadata.put("originalType", getStringValue(relation, "relationType"));
-            metadata.put("source", "ner");
-            graphRelation.setMetadata(metadata);
-            
-            graphRelationRepository.insert(graphRelation);
-            
-            // 同步到 Neo4j
-            graphSyncService.syncRelation(graphRelation);
-            
-            savedCount++;
-        }
-        
-        log.info("关系保存完成: savedCount={}", savedCount);
-        return savedCount;
-    }
-
-    /**
-     * 删除文档的所有 NER 生成的节点和关系
-     *
-     * @param documentId 文档ID
-     * @return 删除的节点数量
-     */
-    @Transactional
-    public int deleteNerResultsByDocumentId(String documentId) {
-        log.info("删除文档 NER 结果: documentId={}", documentId);
-        
-        // 获取文档的所有节点
-        List<GraphNode> nodes = graphNodeRepository.findByDocumentId(documentId);
-        
-        int deletedCount = 0;
-        for (GraphNode node : nodes) {
-            // 检查是否是 NER 生成的节点
-            if (node.getMetadata() instanceof Map) {
-                Map<?, ?> metadata = (Map<?, ?>) node.getMetadata();
-                if ("ner".equals(metadata.get("source"))) {
-                    // 先删除相关的关系
-                    List<GraphRelation> relations = graphRelationRepository.findByNodeId(node.getId());
-                    for (GraphRelation relation : relations) {
-                        graphRelationRepository.deleteById(relation.getId());
-                    }
-                    // 再删除节点
-                    graphNodeRepository.deleteById(node.getId());
-                    deletedCount++;
-                }
-            }
-        }
-        
-        // 从 Neo4j 删除
-        graphSyncService.deleteDocumentFromNeo4j(documentId);
-        
-        log.info("NER 结果删除完成: documentId={}, deletedCount={}", documentId, deletedCount);
-        return deletedCount;
-    }
-
-    /**
-     * 获取文档的 NER 统计信息
-     *
-     * @param documentId 文档ID
-     * @return 统计信息
-     */
-    public Map<String, Object> getNerStatsByDocumentId(String documentId) {
-        List<GraphNode> nodes = graphNodeRepository.findByDocumentId(documentId);
-        
-        int nerNodeCount = 0;
-        Map<String, Integer> typeStats = new HashMap<>();
-        
-        for (GraphNode node : nodes) {
-            if (node.getMetadata() instanceof Map) {
-                Map<?, ?> metadata = (Map<?, ?>) node.getMetadata();
-                if ("ner".equals(metadata.get("source"))) {
-                    nerNodeCount++;
-                    String type = node.getType();
-                    typeStats.put(type, typeStats.getOrDefault(type, 0) + 1);
-                }
-            }
-        }
-        
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("documentId", documentId);
-        stats.put("totalNerNodes", nerNodeCount);
-        stats.put("typeStats", typeStats);
-        
-        return stats;
-    }
-
-    /**
-     * 映射关系类型到系统定义的类型
-     */
-    private String mapRelationType(String originalType) {
-        if (originalType == null) {
-            return "DEP";
-        }
-        
-        // 映射常见关系类型
-        switch (originalType) {
-            case "负责":
-            case "管理":
-            case "承担":
-            case "属于":
-            case "隶属":
-            case "包含":
-            case "包括":
-            case "位于":
-            case "在":
-            case "使用":
-            case "采用":
-                return "DEP";  // 依赖关系
-            default:
-                return "DEP";  // 默认为依赖关系
-        }
-    }
-
-    /**
-     * 从 Map 中获取字符串值
-     */
-    private String getStringValue(Map<String, Object> map, String key) {
-        return getStringValue(map, key, null);
-    }
-
-    /**
-     * 从 Map 中获取字符串值,带默认值
-     */
-    private String getStringValue(Map<String, Object> map, String key, String defaultValue) {
-        Object value = map.get(key);
-        if (value == null) {
-            return defaultValue;
-        }
-        return value.toString();
-    }
-}

+ 34 - 249
backend/graph-service/src/main/java/com/lingyue/graph/service/GraphNodeService.java

@@ -1,47 +1,34 @@
 package com.lingyue.graph.service;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.lingyue.common.exception.ServiceException;
 import com.lingyue.graph.dto.BatchCreateNodesRequest;
 import com.lingyue.graph.dto.BatchCreateRelationsRequest;
 import com.lingyue.graph.dto.CreateNodeRequest;
 import com.lingyue.graph.dto.CreateRelationRequest;
 import com.lingyue.graph.entity.GraphNode;
 import com.lingyue.graph.entity.GraphRelation;
-import com.lingyue.graph.repository.GraphNodeRepository;
-import com.lingyue.graph.repository.GraphRelationRepository;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.util.*;
 
 /**
- * 图节点服务
- * 提供图节点和关系的 CRUD 及批量操作
+ * 图节点服务(存根)
+ * graph_nodes / graph_relations 表已移除,仅保留空实现以兼容 API。
  *
  * @author lingyue
  * @since 2026-01-19
  */
 @Slf4j
 @Service
-@RequiredArgsConstructor
 public class GraphNodeService {
 
-    private final GraphNodeRepository graphNodeRepository;
-    private final GraphRelationRepository graphRelationRepository;
+    // ==================== 节点操作(不持久化) ====================
 
-    // ==================== 节点操作 ====================
-
-    /**
-     * 创建单个节点
-     */
     public GraphNode createNode(CreateNodeRequest request) {
         GraphNode node = new GraphNode();
         node.setId(UUID.randomUUID().toString().replace("-", ""));
         node.setDocumentId(request.getDocumentId());
-        node.setUserId(request.getUserId());  // 可为 null
+        node.setUserId(request.getUserId());
         node.setName(request.getName());
         node.setType(request.getType());
         node.setValue(request.getValue());
@@ -51,167 +38,55 @@ public class GraphNodeService {
         node.setMetadata(request.getMetadata());
         node.setCreateTime(new Date());
         node.setUpdateTime(new Date());
-
-        graphNodeRepository.insert(node);
-        log.debug("创建节点: id={}, name={}, type={}", node.getId(), node.getName(), node.getType());
-
+        log.debug("图节点表已移除,createNode 仅返回内存对象: id={}", node.getId());
         return node;
     }
 
-    /**
-     * 批量创建节点
-     *
-     * @return 创建的节点列表
-     */
-    @Transactional
     public List<GraphNode> batchCreateNodes(BatchCreateNodesRequest request) {
         if (request.getNodes() == null || request.getNodes().isEmpty()) {
             return Collections.emptyList();
         }
-
-        String documentId = request.getDocumentId();
-        String userId = request.getUserId();
-
-        // 如果需要替换已有节点,先删除
-        if (Boolean.TRUE.equals(request.getReplaceExisting()) && documentId != null) {
-            deleteNodesByDocumentId(documentId);
-        }
-
-        List<GraphNode> createdNodes = new ArrayList<>();
-        for (CreateNodeRequest nodeRequest : request.getNodes()) {
-            // 设置文档ID和用户ID
-            if (nodeRequest.getDocumentId() == null) {
-                nodeRequest.setDocumentId(documentId);
-            }
-            if (nodeRequest.getUserId() == null) {
-                nodeRequest.setUserId(userId);
-            }
-
-            GraphNode node = createNode(nodeRequest);
-            createdNodes.add(node);
+        List<GraphNode> created = new ArrayList<>();
+        for (CreateNodeRequest req : request.getNodes()) {
+            if (req.getDocumentId() == null) req.setDocumentId(request.getDocumentId());
+            if (req.getUserId() == null) req.setUserId(request.getUserId());
+            created.add(createNode(req));
         }
-
-        log.info("批量创建节点完成: documentId={}, count={}", documentId, createdNodes.size());
-        return createdNodes;
+        return created;
     }
 
-    /**
-     * 根据ID获取节点
-     */
     public GraphNode getNodeById(String nodeId) {
-        return graphNodeRepository.selectById(nodeId);
+        return null;
     }
 
-    /**
-     * 根据文档ID获取所有节点
-     */
     public List<GraphNode> getNodesByDocumentId(String documentId) {
-        return graphNodeRepository.findByDocumentId(documentId);
+        return Collections.emptyList();
     }
 
-    /**
-     * 根据文档ID和类型获取节点
-     */
     public List<GraphNode> getNodesByDocumentIdAndType(String documentId, String type) {
-        return graphNodeRepository.findByDocumentIdAndType(documentId, type);
+        return Collections.emptyList();
     }
 
-    /**
-     * 根据用户ID获取节点
-     */
     public List<GraphNode> getNodesByUserId(String userId) {
-        return graphNodeRepository.findByUserId(userId);
+        return Collections.emptyList();
     }
 
-    /**
-     * 更新节点
-     */
     public GraphNode updateNode(String nodeId, CreateNodeRequest request) {
-        GraphNode node = graphNodeRepository.selectById(nodeId);
-        if (node == null) {
-            throw new ServiceException("节点不存在: " + nodeId);
-        }
-
-        if (request.getName() != null) {
-            node.setName(request.getName());
-        }
-        if (request.getType() != null) {
-            node.setType(request.getType());
-        }
-        if (request.getValue() != null) {
-            node.setValue(request.getValue());
-        }
-        if (request.getPosition() != null) {
-            node.setPosition(request.getPosition());
-        }
-        if (request.getParentId() != null) {
-            node.setParentId(request.getParentId());
-        }
-        if (request.getLevel() != null) {
-            node.setLevel(request.getLevel());
-        }
-        if (request.getMetadata() != null) {
-            node.setMetadata(request.getMetadata());
-        }
-        node.setUpdateTime(new Date());
-
-        graphNodeRepository.updateById(node);
-        log.debug("更新节点: id={}", nodeId);
-
-        return node;
+        log.debug("图节点表已移除,updateNode 忽略: nodeId={}", nodeId);
+        return null;
     }
 
-    /**
-     * 删除节点(同时删除相关关系)
-     */
-    @Transactional
     public void deleteNode(String nodeId) {
-        // 先删除相关关系
-        deleteRelationsByNodeId(nodeId);
-        // 再删除节点
-        graphNodeRepository.deleteById(nodeId);
-        log.debug("删除节点: id={}", nodeId);
+        log.debug("图节点表已移除,deleteNode 忽略: nodeId={}", nodeId);
     }
 
-    /**
-     * 根据文档ID删除所有节点(同时删除相关关系)
-     */
-    @Transactional
     public int deleteNodesByDocumentId(String documentId) {
-        // 获取所有节点ID
-        List<GraphNode> nodes = graphNodeRepository.findByDocumentId(documentId);
-        if (nodes.isEmpty()) {
-            return 0;
-        }
-
-        // 删除所有相关关系
-        for (GraphNode node : nodes) {
-            deleteRelationsByNodeId(node.getId());
-        }
-
-        // 删除节点
-        LambdaQueryWrapper<GraphNode> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(GraphNode::getDocumentId, documentId);
-        int count = graphNodeRepository.delete(wrapper);
-
-        log.info("删除文档节点: documentId={}, count={}", documentId, count);
-        return count;
+        return 0;
     }
 
-    // ==================== 关系操作 ====================
+    // ==================== 关系操作(不持久化) ====================
 
-    /**
-     * 创建单个关系
-     */
     public GraphRelation createRelation(CreateRelationRequest request) {
-        // 验证节点存在
-        if (graphNodeRepository.selectById(request.getFromNodeId()) == null) {
-            throw new ServiceException("源节点不存在: " + request.getFromNodeId());
-        }
-        if (graphNodeRepository.selectById(request.getToNodeId()) == null) {
-            throw new ServiceException("目标节点不存在: " + request.getToNodeId());
-        }
-
         GraphRelation relation = new GraphRelation();
         relation.setId(UUID.randomUUID().toString().replace("-", ""));
         relation.setFromNodeId(request.getFromNodeId());
@@ -224,142 +99,52 @@ public class GraphNodeService {
         relation.setMetadata(request.getMetadata());
         relation.setCreateTime(new Date());
         relation.setUpdateTime(new Date());
-
-        graphRelationRepository.insert(relation);
-        log.debug("创建关系: id={}, from={}, to={}, type={}",
-                relation.getId(), relation.getFromNodeId(), relation.getToNodeId(), relation.getRelationType());
-
+        log.debug("图关系表已移除,createRelation 仅返回内存对象: id={}", relation.getId());
         return relation;
     }
 
-    /**
-     * 批量创建关系
-     */
-    @Transactional
     public List<GraphRelation> batchCreateRelations(BatchCreateRelationsRequest request) {
         if (request.getRelations() == null || request.getRelations().isEmpty()) {
             return Collections.emptyList();
         }
-
-        List<GraphRelation> createdRelations = new ArrayList<>();
-        boolean skipInvalid = Boolean.TRUE.equals(request.getSkipInvalidNodes());
-
-        for (CreateRelationRequest relationRequest : request.getRelations()) {
-            try {
-                GraphRelation relation = createRelation(relationRequest);
-                createdRelations.add(relation);
-            } catch (ServiceException e) {
-                if (skipInvalid) {
-                    log.warn("跳过无效关系: from={}, to={}, error={}",
-                            relationRequest.getFromNodeId(), relationRequest.getToNodeId(), e.getMessage());
-                } else {
-                    throw e;
-                }
-            }
+        List<GraphRelation> created = new ArrayList<>();
+        for (CreateRelationRequest req : request.getRelations()) {
+            created.add(createRelation(req));
         }
-
-        log.info("批量创建关系完成: count={}", createdRelations.size());
-        return createdRelations;
+        return created;
     }
 
-    /**
-     * 根据ID获取关系
-     */
     public GraphRelation getRelationById(String relationId) {
-        return graphRelationRepository.selectById(relationId);
+        return null;
     }
 
-    /**
-     * 根据源节点ID获取关系
-     */
     public List<GraphRelation> getRelationsByFromNodeId(String fromNodeId) {
-        return graphRelationRepository.findByFromNodeId(fromNodeId);
+        return Collections.emptyList();
     }
 
-    /**
-     * 根据目标节点ID获取关系
-     */
     public List<GraphRelation> getRelationsByToNodeId(String toNodeId) {
-        return graphRelationRepository.findByToNodeId(toNodeId);
+        return Collections.emptyList();
     }
 
-    /**
-     * 根据节点ID获取所有相关关系
-     */
     public List<GraphRelation> getRelationsByNodeId(String nodeId) {
-        return graphRelationRepository.findByNodeId(nodeId);
+        return Collections.emptyList();
     }
 
-    /**
-     * 删除关系
-     */
     public void deleteRelation(String relationId) {
-        graphRelationRepository.deleteById(relationId);
-        log.debug("删除关系: id={}", relationId);
-    }
-
-    /**
-     * 删除节点相关的所有关系
-     */
-    public int deleteRelationsByNodeId(String nodeId) {
-        LambdaQueryWrapper<GraphRelation> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(GraphRelation::getFromNodeId, nodeId)
-                .or()
-                .eq(GraphRelation::getToNodeId, nodeId);
-        int count = graphRelationRepository.delete(wrapper);
-        log.debug("删除节点相关关系: nodeId={}, count={}", nodeId, count);
-        return count;
+        log.debug("图关系表已移除,deleteRelation 忽略: relationId={}", relationId);
     }
 
-    // ==================== 统计操作 ====================
-
-    /**
-     * 获取文档的节点统计
-     */
     public Map<String, Object> getNodeStatsByDocumentId(String documentId) {
-        List<GraphNode> nodes = graphNodeRepository.findByDocumentId(documentId);
-
-        Map<String, Long> typeCount = new HashMap<>();
-        for (GraphNode node : nodes) {
-            String type = node.getType() != null ? node.getType() : "other";
-            typeCount.put(type, typeCount.getOrDefault(type, 0L) + 1);
-        }
-
         Map<String, Object> stats = new HashMap<>();
-        stats.put("totalNodes", nodes.size());
-        stats.put("typeDistribution", typeCount);
-
+        stats.put("totalNodes", 0);
+        stats.put("typeDistribution", Collections.emptyMap());
         return stats;
     }
 
-    /**
-     * 获取文档的关系统计
-     */
     public Map<String, Object> getRelationStatsByDocumentId(String documentId) {
-        List<GraphNode> nodes = graphNodeRepository.findByDocumentId(documentId);
-        Set<String> nodeIds = new HashSet<>();
-        for (GraphNode node : nodes) {
-            nodeIds.add(node.getId());
-        }
-
-        int relationCount = 0;
-        Map<String, Long> typeCount = new HashMap<>();
-
-        for (String nodeId : nodeIds) {
-            List<GraphRelation> relations = graphRelationRepository.findByFromNodeId(nodeId);
-            for (GraphRelation relation : relations) {
-                if (nodeIds.contains(relation.getToNodeId())) {
-                    relationCount++;
-                    String type = relation.getRelationType() != null ? relation.getRelationType() : "OTHER";
-                    typeCount.put(type, typeCount.getOrDefault(type, 0L) + 1);
-                }
-            }
-        }
-
         Map<String, Object> stats = new HashMap<>();
-        stats.put("totalRelations", relationCount);
-        stats.put("typeDistribution", typeCount);
-
+        stats.put("totalRelations", 0);
+        stats.put("typeDistribution", Collections.emptyMap());
         return stats;
     }
 }

+ 0 - 183
backend/graph-service/src/main/java/com/lingyue/graph/service/GraphSyncService.java

@@ -1,183 +0,0 @@
-package com.lingyue.graph.service;
-
-import com.lingyue.graph.entity.GraphNode;
-import com.lingyue.graph.entity.GraphRelation;
-import com.lingyue.graph.neo4j.Neo4jGraphService;
-import com.lingyue.graph.repository.GraphNodeRepository;
-import com.lingyue.graph.repository.GraphRelationRepository;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Service;
-
-import java.util.List;
-
-/**
- * 图数据同步服务
- * 
- * 负责将 PostgreSQL 中的图数据同步到 Neo4j
- * 
- * 同步策略:
- * 1. 实时同步:每次 CRUD 操作后立即同步
- * 2. 批量同步:定时任务或手动触发全量同步
- * 3. 按需同步:查询时如果 Neo4j 无数据则同步
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class GraphSyncService {
-    
-    private final GraphNodeRepository nodeRepository;
-    private final GraphRelationRepository relationRepository;
-    
-    // Neo4j 服务可能不存在(未启用时)
-    @Autowired(required = false)
-    private Neo4jGraphService neo4jGraphService;
-    
-    /**
-     * 检查 Neo4j 是否可用
-     */
-    public boolean isNeo4jEnabled() {
-        return neo4jGraphService != null;
-    }
-    
-    /**
-     * 同步单个节点到 Neo4j
-     */
-    public void syncNode(GraphNode node) {
-        if (!isNeo4jEnabled()) {
-            return;
-        }
-        
-        try {
-            neo4jGraphService.syncNode(node);
-        } catch (Exception e) {
-            log.error("同步节点到 Neo4j 失败: nodeId={}, error={}", node.getId(), e.getMessage());
-        }
-    }
-    
-    /**
-     * 同步单个关系到 Neo4j
-     */
-    public void syncRelation(GraphRelation relation) {
-        if (!isNeo4jEnabled()) {
-            return;
-        }
-        
-        try {
-            neo4jGraphService.syncRelation(relation);
-        } catch (Exception e) {
-            log.error("同步关系到 Neo4j 失败: relId={}, error={}", relation.getId(), e.getMessage());
-        }
-    }
-    
-    /**
-     * 同步文档的所有数据到 Neo4j
-     */
-    @Async
-    public void syncDocument(String documentId) {
-        if (!isNeo4jEnabled()) {
-            log.debug("Neo4j 未启用,跳过同步");
-            return;
-        }
-        
-        log.info("开始同步文档到 Neo4j: documentId={}", documentId);
-        
-        try {
-            // 1. 先删除旧数据
-            neo4jGraphService.deleteDocumentGraph(documentId);
-            
-            // 2. 同步节点
-            List<GraphNode> nodes = nodeRepository.findByDocumentId(documentId);
-            if (!nodes.isEmpty()) {
-                neo4jGraphService.syncNodes(nodes);
-            }
-            
-            // 3. 同步关系
-            // 获取所有节点ID
-            for (GraphNode node : nodes) {
-                List<GraphRelation> relations = relationRepository.findByNodeId(node.getId());
-                if (!relations.isEmpty()) {
-                    neo4jGraphService.syncRelations(relations);
-                }
-            }
-            
-            log.info("文档同步到 Neo4j 完成: documentId={}, nodes={}", documentId, nodes.size());
-            
-        } catch (Exception e) {
-            log.error("同步文档到 Neo4j 失败: documentId={}, error={}", documentId, e.getMessage(), e);
-        }
-    }
-    
-    /**
-     * 同步用户的所有数据到 Neo4j
-     */
-    @Async
-    public void syncUser(String userId) {
-        if (!isNeo4jEnabled()) {
-            return;
-        }
-        
-        log.info("开始同步用户数据到 Neo4j: userId={}", userId);
-        
-        try {
-            // 获取用户的所有节点
-            List<GraphNode> nodes = nodeRepository.findByUserId(userId);
-            if (!nodes.isEmpty()) {
-                neo4jGraphService.syncNodes(nodes);
-            }
-            
-            // 同步关系
-            for (GraphNode node : nodes) {
-                List<GraphRelation> relations = relationRepository.findByNodeId(node.getId());
-                if (!relations.isEmpty()) {
-                    neo4jGraphService.syncRelations(relations);
-                }
-            }
-            
-            log.info("用户数据同步到 Neo4j 完成: userId={}, nodes={}", userId, nodes.size());
-            
-        } catch (Exception e) {
-            log.error("同步用户数据到 Neo4j 失败: userId={}, error={}", userId, e.getMessage(), e);
-        }
-    }
-    
-    /**
-     * 全量同步(谨慎使用)
-     */
-    public void fullSync() {
-        if (!isNeo4jEnabled()) {
-            log.warn("Neo4j 未启用,无法执行全量同步");
-            return;
-        }
-        
-        log.info("开始全量同步到 Neo4j...");
-        
-        // 初始化索引
-        neo4jGraphService.initializeIndexes();
-        
-        // TODO: 分批查询所有节点和关系进行同步
-        // 这里需要添加分页逻辑以避免内存溢出
-        
-        log.info("全量同步完成");
-    }
-    
-    /**
-     * 删除文档在 Neo4j 中的数据
-     */
-    public void deleteDocumentFromNeo4j(String documentId) {
-        if (!isNeo4jEnabled()) {
-            return;
-        }
-        
-        try {
-            neo4jGraphService.deleteDocumentGraph(documentId);
-        } catch (Exception e) {
-            log.error("从 Neo4j 删除文档失败: documentId={}, error={}", documentId, e.getMessage());
-        }
-    }
-}

+ 32 - 479
backend/graph-service/src/main/java/com/lingyue/graph/service/KnowledgeGraphService.java

@@ -1,517 +1,70 @@
 package com.lingyue.graph.service;
 
 import com.lingyue.graph.dto.KnowledgeGraphDTO;
-import com.lingyue.graph.dto.KnowledgeGraphDTO.*;
-import com.lingyue.graph.entity.GraphNode;
-import com.lingyue.graph.entity.GraphRelation;
-import com.lingyue.graph.repository.GraphNodeRepository;
-import com.lingyue.graph.repository.GraphRelationRepository;
-import lombok.RequiredArgsConstructor;
+import com.lingyue.graph.dto.KnowledgeGraphDTO.EntityDetailDTO;
+import com.lingyue.graph.dto.KnowledgeGraphDTO.EntityGroupDTO;
+import com.lingyue.graph.dto.KnowledgeGraphDTO.EntityListItemDTO;
+import com.lingyue.graph.dto.KnowledgeGraphDTO.GraphStats;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.Collections;
+import java.util.List;
 
 /**
- * 知识图谱服务
- * 
- * 提供知识图谱的查询和可视化数据转换功能
- * 
+ * 知识图谱服务(存根)
+ * graph_nodes / graph_relations 表已移除,仅返回空数据以兼容 API。
+ *
  * @author lingyue
  * @since 2026-01-21
  */
 @Slf4j
 @Service
-@RequiredArgsConstructor
 public class KnowledgeGraphService {
-    
-    private final GraphNodeRepository nodeRepository;
-    private final GraphRelationRepository relationRepository;
-    
-    // 类型到显示名称的映射
-    private static final Map<String, String> TYPE_NAMES = Map.ofEntries(
-            Map.entry("person", "人物"),
-            Map.entry("org", "机构"),
-            Map.entry("loc", "地点"),
-            Map.entry("location", "地点"),
-            Map.entry("date", "日期"),
-            Map.entry("number", "数值"),
-            Map.entry("money", "金额"),
-            Map.entry("data", "数据"),
-            Map.entry("concept", "概念"),
-            Map.entry("device", "设备"),
-            Map.entry("term", "术语"),
-            Map.entry("entity", "实体"),
-            Map.entry("other", "其他")
-    );
-    
-    // 类型到图标的映射
-    private static final Map<String, String> TYPE_ICONS = Map.ofEntries(
-            Map.entry("person", "👤"),
-            Map.entry("org", "🏢"),
-            Map.entry("loc", "📍"),
-            Map.entry("location", "📍"),
-            Map.entry("date", "📅"),
-            Map.entry("number", "🔢"),
-            Map.entry("money", "💰"),
-            Map.entry("data", "📊"),
-            Map.entry("concept", "💡"),
-            Map.entry("device", "🔧"),
-            Map.entry("term", "📝"),
-            Map.entry("entity", "🏷️"),
-            Map.entry("other", "📌")
-    );
-    
-    // 类型到颜色的映射
-    private static final Map<String, String> TYPE_COLORS = Map.ofEntries(
-            Map.entry("person", "#1890ff"),
-            Map.entry("org", "#52c41a"),
-            Map.entry("loc", "#fa8c16"),
-            Map.entry("location", "#fa8c16"),
-            Map.entry("date", "#722ed1"),
-            Map.entry("number", "#13c2c2"),
-            Map.entry("money", "#52c41a"),
-            Map.entry("data", "#13c2c2"),
-            Map.entry("concept", "#722ed1"),
-            Map.entry("device", "#eb2f96"),
-            Map.entry("term", "#2f54eb"),
-            Map.entry("entity", "#1890ff"),
-            Map.entry("other", "#8c8c8c")
-    );
-    
+
     /**
-     * 获取文档的知识图谱
+     * 获取文档的知识图谱(无数据)
      */
     public KnowledgeGraphDTO getDocumentGraph(String documentId) {
-        // 1. 获取文档的所有节点
-        List<GraphNode> nodes = nodeRepository.findByDocumentId(documentId);
-        if (nodes.isEmpty()) {
-            return null;
-        }
-        
-        // 2. 获取节点ID集合
-        Set<String> nodeIds = nodes.stream()
-                .map(GraphNode::getId)
-                .collect(Collectors.toSet());
-        
-        // 3. 获取节点间的关系
-        List<GraphRelation> relations = new ArrayList<>();
-        for (String nodeId : nodeIds) {
-            List<GraphRelation> nodeRelations = relationRepository.findByNodeId(nodeId);
-            for (GraphRelation rel : nodeRelations) {
-                // 只保留两端都在当前节点集中的关系
-                if (nodeIds.contains(rel.getFromNodeId()) && nodeIds.contains(rel.getToNodeId())) {
-                    relations.add(rel);
-                }
-            }
-        }
-        
-        // 4. 去重关系
-        relations = relations.stream()
-                .collect(Collectors.collectingAndThen(
-                        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(GraphRelation::getId))),
-                        ArrayList::new));
-        
-        // 5. 计算每个节点的关联数量
-        Map<String, Integer> relationCounts = new HashMap<>();
-        for (GraphRelation rel : relations) {
-            relationCounts.merge(rel.getFromNodeId(), 1, Integer::sum);
-            relationCounts.merge(rel.getToNodeId(), 1, Integer::sum);
-        }
-        
-        // 6. 构建节点 DTO
-        Map<String, GraphNode> nodeMap = nodes.stream()
-                .collect(Collectors.toMap(GraphNode::getId, n -> n));
-        
-        List<NodeDTO> nodeDTOs = nodes.stream()
-                .map(node -> buildNodeDTO(node, relationCounts.getOrDefault(node.getId(), 0)))
-                .collect(Collectors.toList());
-        
-        // 7. 构建边 DTO
-        List<EdgeDTO> edgeDTOs = relations.stream()
-                .map(rel -> buildEdgeDTO(rel, nodeMap))
-                .collect(Collectors.toList());
-        
-        // 8. 统计信息
-        GraphStats stats = buildGraphStats(nodes, relations);
-        
-        return KnowledgeGraphDTO.builder()
-                .documentId(documentId)
-                .title("标记要素关系图谱")
-                .nodes(nodeDTOs)
-                .edges(edgeDTOs)
-                .stats(stats)
-                .build();
+        log.debug("图表已移除,返回空图谱: documentId={}", documentId);
+        return null;
     }
-    
+
     /**
-     * 获取用户的全局知识图谱(跨文档
+     * 获取用户的全局知识图谱(无数据)
      */
     public KnowledgeGraphDTO getUserGraph(String userId, int limit) {
-        // 获取用户的所有节点
-        List<GraphNode> allNodes = nodeRepository.findByUserId(userId);
-        
-        // 限制数量(取关联最多的)
-        if (allNodes.size() > limit) {
-            // TODO: 按重要性排序后截取
-            allNodes = allNodes.subList(0, limit);
-        }
-        
-        if (allNodes.isEmpty()) {
-            return KnowledgeGraphDTO.builder()
-                    .title("我的知识图谱")
-                    .nodes(Collections.emptyList())
-                    .edges(Collections.emptyList())
-                    .stats(GraphStats.builder()
-                            .totalNodes(0)
-                            .totalEdges(0)
-                            .nodesByType(Collections.emptyMap())
-                            .edgesByType(Collections.emptyMap())
-                            .build())
-                    .build();
-        }
-        
-        Set<String> nodeIds = allNodes.stream()
-                .map(GraphNode::getId)
-                .collect(Collectors.toSet());
-        
-        List<GraphRelation> relations = new ArrayList<>();
-        for (String nodeId : nodeIds) {
-            List<GraphRelation> nodeRelations = relationRepository.findByNodeId(nodeId);
-            for (GraphRelation rel : nodeRelations) {
-                if (nodeIds.contains(rel.getFromNodeId()) && nodeIds.contains(rel.getToNodeId())) {
-                    relations.add(rel);
-                }
-            }
-        }
-        
-        // 去重
-        relations = relations.stream()
-                .collect(Collectors.collectingAndThen(
-                        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(GraphRelation::getId))),
-                        ArrayList::new));
-        
-        Map<String, Integer> relationCounts = new HashMap<>();
-        for (GraphRelation rel : relations) {
-            relationCounts.merge(rel.getFromNodeId(), 1, Integer::sum);
-            relationCounts.merge(rel.getToNodeId(), 1, Integer::sum);
-        }
-        
-        Map<String, GraphNode> nodeMap = allNodes.stream()
-                .collect(Collectors.toMap(GraphNode::getId, n -> n));
-        
-        List<NodeDTO> nodeDTOs = allNodes.stream()
-                .map(node -> buildNodeDTO(node, relationCounts.getOrDefault(node.getId(), 0)))
-                .collect(Collectors.toList());
-        
-        List<EdgeDTO> edgeDTOs = relations.stream()
-                .map(rel -> buildEdgeDTO(rel, nodeMap))
-                .collect(Collectors.toList());
-        
-        GraphStats stats = buildGraphStats(allNodes, relations);
-        
         return KnowledgeGraphDTO.builder()
                 .title("我的知识图谱")
-                .nodes(nodeDTOs)
-                .edges(edgeDTOs)
-                .stats(stats)
+                .nodes(Collections.emptyList())
+                .edges(Collections.emptyList())
+                .stats(GraphStats.builder()
+                        .totalNodes(0)
+                        .totalEdges(0)
+                        .nodesByType(Collections.emptyMap())
+                        .edgesByType(Collections.emptyMap())
+                        .build())
                 .build();
     }
-    
+
     /**
-     * 获取实体列表(按类型分组)
-     * 
-     * 优化:批量获取关系,避免 N+1 查询
+     * 获取实体列表(按类型分组)(无数据)
      */
     public List<EntityGroupDTO> getEntityListGroupedByType(String documentId, String filterType) {
-        List<GraphNode> nodes = nodeRepository.findByDocumentId(documentId);
-        
-        if (filterType != null && !filterType.isEmpty() && !"all".equals(filterType)) {
-            nodes = nodes.stream()
-                    .filter(n -> filterType.equalsIgnoreCase(n.getType()))
-                    .collect(Collectors.toList());
-        }
-        
-        if (nodes.isEmpty()) {
-            return Collections.emptyList();
-        }
-        
-        // 按类型分组
-        Map<String, List<GraphNode>> nodesByType = nodes.stream()
-                .collect(Collectors.groupingBy(n -> n.getType() != null ? n.getType().toLowerCase() : "other"));
-        
-        // 构建节点ID到节点的映射(用于快速查找)
-        Map<String, GraphNode> nodeMap = nodes.stream()
-                .collect(Collectors.toMap(GraphNode::getId, n -> n));
-        Set<String> nodeIds = nodeMap.keySet();
-        
-        // 批量获取所有相关关系(一次查询)
-        // 注意:这里假设文档的节点数量不会太多,实际生产环境可能需要分批处理
-        List<GraphRelation> allRelations = new ArrayList<>();
-        for (GraphNode node : nodes) {
-            allRelations.addAll(relationRepository.findByNodeId(node.getId()));
-        }
-        
-        // 去重并只保留两端都在当前节点集中的关系
-        Set<String> seenRelIds = new HashSet<>();
-        List<GraphRelation> filteredRelations = allRelations.stream()
-                .filter(r -> seenRelIds.add(r.getId()))
-                .filter(r -> nodeIds.contains(r.getFromNodeId()) || nodeIds.contains(r.getToNodeId()))
-                .collect(Collectors.toList());
-        
-        // 计算每个节点的关联数和关联实体预览
-        Map<String, List<RelatedEntityDTO>> relatedEntitiesMap = new HashMap<>();
-        Map<String, Integer> relationCountMap = new HashMap<>();
-        
-        for (GraphRelation rel : filteredRelations) {
-            // 处理 from 节点
-            if (nodeIds.contains(rel.getFromNodeId())) {
-                relationCountMap.merge(rel.getFromNodeId(), 1, Integer::sum);
-                
-                // 添加关联实体预览(限制3个)
-                GraphNode otherNode = nodeMap.get(rel.getToNodeId());
-                if (otherNode != null) {
-                    List<RelatedEntityDTO> related = relatedEntitiesMap.computeIfAbsent(
-                            rel.getFromNodeId(), k -> new ArrayList<>());
-                    if (related.size() < 3) {
-                        related.add(RelatedEntityDTO.builder()
-                                .id(otherNode.getId())
-                                .name(otherNode.getName())
-                                .relationType(rel.getRelationType())
-                                .build());
-                    }
-                }
-            }
-            
-            // 处理 to 节点
-            if (nodeIds.contains(rel.getToNodeId())) {
-                relationCountMap.merge(rel.getToNodeId(), 1, Integer::sum);
-                
-                GraphNode otherNode = nodeMap.get(rel.getFromNodeId());
-                if (otherNode != null) {
-                    List<RelatedEntityDTO> related = relatedEntitiesMap.computeIfAbsent(
-                            rel.getToNodeId(), k -> new ArrayList<>());
-                    if (related.size() < 3) {
-                        related.add(RelatedEntityDTO.builder()
-                                .id(otherNode.getId())
-                                .name(otherNode.getName())
-                                .relationType(rel.getRelationType())
-                                .build());
-                    }
-                }
-            }
-        }
-        
-        // 构建分组列表
-        List<EntityGroupDTO> groups = new ArrayList<>();
-        
-        for (Map.Entry<String, List<GraphNode>> entry : nodesByType.entrySet()) {
-            String type = entry.getKey();
-            List<GraphNode> typeNodes = entry.getValue();
-            
-            List<EntityListItemDTO> items = typeNodes.stream()
-                    .map(node -> EntityListItemDTO.builder()
-                            .id(node.getId())
-                            .name(node.getName())
-                            .type(type)
-                            .typeName(TYPE_NAMES.getOrDefault(type, type))
-                            .icon(TYPE_ICONS.getOrDefault(type, "📌"))
-                            .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                            .occurrenceCount(1) // TODO: 计算实际出现次数
-                            .relationCount(relationCountMap.getOrDefault(node.getId(), 0))
-                            .relatedEntities(relatedEntitiesMap.getOrDefault(node.getId(), Collections.emptyList()))
-                            .build())
-                    .collect(Collectors.toList());
-            
-            groups.add(EntityGroupDTO.builder()
-                    .type(type)
-                    .typeName(TYPE_NAMES.getOrDefault(type, type))
-                    .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                    .count(items.size())
-                    .entities(items)
-                    .build());
-        }
-        
-        // 按预定义顺序排序
-        List<String> typeOrder = List.of("entity", "concept", "data", "number", "money", "person", "org", "loc", "location", "date", "device", "term", "other");
-        groups.sort((a, b) -> {
-            int indexA = typeOrder.indexOf(a.getType());
-            int indexB = typeOrder.indexOf(b.getType());
-            if (indexA < 0) indexA = 999;
-            if (indexB < 0) indexB = 999;
-            return indexA - indexB;
-        });
-        
-        return groups;
+        return Collections.emptyList();
     }
-    
+
     /**
-     * 获取实体详情
+     * 获取实体详情(无数据)
      */
     public EntityDetailDTO getEntityDetail(String entityId) {
-        GraphNode node = nodeRepository.selectById(entityId);
-        if (node == null) {
-            return null;
-        }
-        
-        String type = node.getType() != null ? node.getType().toLowerCase() : "other";
-        
-        // 获取所有关联实体
-        List<GraphRelation> relations = relationRepository.findByNodeId(entityId);
-        List<RelatedEntityDTO> allRelations = new ArrayList<>();
-        
-        for (GraphRelation rel : relations) {
-            String otherId = rel.getFromNodeId().equals(entityId) ? rel.getToNodeId() : rel.getFromNodeId();
-            GraphNode otherNode = nodeRepository.selectById(otherId);
-            if (otherNode != null) {
-                allRelations.add(RelatedEntityDTO.builder()
-                        .id(otherNode.getId())
-                        .name(otherNode.getName())
-                        .relationType(rel.getRelationType())
-                        .build());
-            }
-        }
-        
-        // 获取出现位置
-        List<OccurrenceDTO> occurrences = new ArrayList<>();
-        if (node.getPosition() instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> pos = (Map<String, Object>) node.getPosition();
-            occurrences.add(OccurrenceDTO.builder()
-                    .documentId(node.getDocumentId())
-                    .position(pos)
-                    .build());
-        }
-        
-        // 提取上下文
-        if (node.getMetadata() instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> meta = (Map<String, Object>) node.getMetadata();
-            String context = (String) meta.get("context");
-            if (context != null && !occurrences.isEmpty()) {
-                occurrences.get(0).setContext(context);
-            }
-        }
-        
-        return EntityDetailDTO.builder()
-                .id(node.getId())
-                .name(node.getName())
-                .type(type)
-                .typeName(TYPE_NAMES.getOrDefault(type, type))
-                .value(node.getValue())
-                .icon(TYPE_ICONS.getOrDefault(type, "📌"))
-                .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                .occurrenceCount(1)
-                .allRelations(allRelations)
-                .occurrences(occurrences)
-                .metadata(node.getMetadata() instanceof Map ? (Map<String, Object>) node.getMetadata() : null)
-                .build();
+        return null;
     }
-    
+
     /**
-     * 搜索实体
+     * 搜索实体(无数据)
      */
     public List<EntityListItemDTO> searchEntities(String keyword, String documentId, int limit) {
-        List<GraphNode> nodes;
-        
-        if (documentId != null && !documentId.isEmpty()) {
-            nodes = nodeRepository.findByDocumentId(documentId);
-        } else {
-            // 全局搜索需要另外的查询方法
-            // TODO: 添加全局搜索的 Repository 方法
-            nodes = new ArrayList<>();
-        }
-        
-        // 过滤匹配的节点
-        String lowerKeyword = keyword.toLowerCase();
-        List<GraphNode> matchedNodes = nodes.stream()
-                .filter(n -> n.getName() != null && n.getName().toLowerCase().contains(lowerKeyword))
-                .limit(limit)
-                .collect(Collectors.toList());
-        
-        return matchedNodes.stream()
-                .map(node -> {
-                    String type = node.getType() != null ? node.getType().toLowerCase() : "other";
-                    return EntityListItemDTO.builder()
-                            .id(node.getId())
-                            .name(node.getName())
-                            .type(type)
-                            .typeName(TYPE_NAMES.getOrDefault(type, type))
-                            .icon(TYPE_ICONS.getOrDefault(type, "📌"))
-                            .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                            .build();
-                })
-                .collect(Collectors.toList());
-    }
-    
-    // ==================== 辅助方法 ====================
-    
-    private NodeDTO buildNodeDTO(GraphNode node, int relationCount) {
-        String type = node.getType() != null ? node.getType().toLowerCase() : "other";
-        
-        // 根据关联数量计算节点大小
-        int size = 40 + Math.min(relationCount * 5, 30);
-        
-        return NodeDTO.builder()
-                .id(node.getId())
-                .name(node.getName())
-                .type(type)
-                .icon(TYPE_ICONS.getOrDefault(type, "📌"))
-                .color(TYPE_COLORS.getOrDefault(type, "#8c8c8c"))
-                .size(size)
-                .relationCount(relationCount)
-                .occurrenceCount(1)
-                .value(node.getValue())
-                .documentIds(node.getDocumentId() != null ? List.of(node.getDocumentId()) : Collections.emptyList())
-                .position(node.getPosition() instanceof Map ? (Map<String, Object>) node.getPosition() : null)
-                .metadata(node.getMetadata() instanceof Map ? (Map<String, Object>) node.getMetadata() : null)
-                .build();
-    }
-    
-    private EdgeDTO buildEdgeDTO(GraphRelation relation, Map<String, GraphNode> nodeMap) {
-        GraphNode fromNode = nodeMap.get(relation.getFromNodeId());
-        GraphNode toNode = nodeMap.get(relation.getToNodeId());
-        
-        String label = relation.getRelationType();
-        if (relation.getMetadata() instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> meta = (Map<String, Object>) relation.getMetadata();
-            String originalType = (String) meta.get("originalType");
-            if (originalType != null) {
-                label = originalType;
-            }
-        }
-        
-        return EdgeDTO.builder()
-                .id(relation.getId())
-                .source(relation.getFromNodeId())
-                .sourceName(fromNode != null ? fromNode.getName() : null)
-                .target(relation.getToNodeId())
-                .targetName(toNode != null ? toNode.getName() : null)
-                .relationType(relation.getRelationType())
-                .label(label)
-                .weight(1.0)
-                .metadata(relation.getMetadata() instanceof Map ? (Map<String, Object>) relation.getMetadata() : null)
-                .build();
-    }
-    
-    private GraphStats buildGraphStats(List<GraphNode> nodes, List<GraphRelation> relations) {
-        Map<String, Integer> nodesByType = nodes.stream()
-                .collect(Collectors.groupingBy(
-                        n -> n.getType() != null ? n.getType().toLowerCase() : "other",
-                        Collectors.collectingAndThen(Collectors.counting(), Long::intValue)));
-        
-        Map<String, Integer> edgesByType = relations.stream()
-                .collect(Collectors.groupingBy(
-                        r -> r.getRelationType() != null ? r.getRelationType() : "unknown",
-                        Collectors.collectingAndThen(Collectors.counting(), Long::intValue)));
-        
-        return GraphStats.builder()
-                .totalNodes(nodes.size())
-                .totalEdges(relations.size())
-                .nodesByType(nodesByType)
-                .edgesByType(edgesByType)
-                .build();
+        return Collections.emptyList();
     }
 }

+ 0 - 394
backend/graph-service/src/main/java/com/lingyue/graph/service/NerToBlockService.java

@@ -1,394 +0,0 @@
-package com.lingyue.graph.service;
-
-import lombok.Data;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-/**
- * NER 结果转换为 Block TextElement 的服务
- * 
- * 核心职责:
- * 1. 将 NER 提取的实体(带字符偏移)转换为 TextElement 结构
- * 2. 将纯文本拆分为 text_run 和 entity 元素的混合列表
- * 
- * @author lingyue
- * @since 2026-01-21
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class NerToBlockService {
-    
-    /**
-     * 将纯文本和 NER 实体转换为 TextElement 列表
-     * 
-     * @param text 纯文本内容
-     * @param entities NER 提取的实体列表,每个实体需包含 name, type, charStart, charEnd
-     * @return TextElement 列表
-     */
-    public List<TextElementDTO> convertToTextElements(String text, List<Map<String, Object>> entities) {
-        if (text == null || text.isEmpty()) {
-            return Collections.emptyList();
-        }
-        
-        if (entities == null || entities.isEmpty()) {
-            // 无实体,整个文本作为一个 text_run
-            TextElementDTO element = new TextElementDTO();
-            element.setType("text_run");
-            element.setContent(text);
-            return Collections.singletonList(element);
-        }
-        
-        // 1. 提取并排序实体位置
-        List<EntityPosition> positions = extractEntityPositions(text, entities);
-        
-        // 2. 根据实体位置拆分文本
-        return splitTextByEntities(text, positions);
-    }
-    
-    /**
-     * 将文档级别的 NER 结果分配到各个 Block
-     * 
-     * @param blocks 文档的所有块(需包含 charStart, charEnd, text)
-     * @param entities 文档级别的 NER 实体(globalCharStart, globalCharEnd)
-     * @return 每个 blockId 对应的 TextElement 列表
-     */
-    public Map<String, List<TextElementDTO>> distributeEntitiesToBlocks(
-            List<BlockInfo> blocks, List<Map<String, Object>> entities) {
-        
-        Map<String, List<TextElementDTO>> result = new LinkedHashMap<>();
-        
-        if (blocks == null || blocks.isEmpty()) {
-            return result;
-        }
-        
-        // 按 charStart 排序块
-        List<BlockInfo> sortedBlocks = blocks.stream()
-                .sorted(Comparator.comparingInt(BlockInfo::getCharStart))
-                .collect(Collectors.toList());
-        
-        // 为每个块分配实体
-        for (BlockInfo block : sortedBlocks) {
-            // 找出落在该块范围内的实体
-            List<Map<String, Object>> blockEntities = filterEntitiesForBlock(
-                    entities, block.getCharStart(), block.getCharEnd());
-            
-            // 将全局偏移转换为块内偏移
-            List<Map<String, Object>> localizedEntities = localizeEntityPositions(
-                    blockEntities, block.getCharStart());
-            
-            // 转换为 TextElement
-            List<TextElementDTO> elements = convertToTextElements(block.getText(), localizedEntities);
-            
-            result.put(block.getBlockId(), elements);
-        }
-        
-        return result;
-    }
-    
-    /**
-     * 将单个段落文本和实体转换为 TextElement(简化版,用于单块处理)
-     */
-    public List<TextElementDTO> convertParagraphToElements(String paragraphText, 
-                                                            List<Map<String, Object>> entities) {
-        return convertToTextElements(paragraphText, entities);
-    }
-    
-    // ==================== 内部方法 ====================
-    
-    /**
-     * 从实体列表中提取位置信息
-     */
-    private List<EntityPosition> extractEntityPositions(String text, List<Map<String, Object>> entities) {
-        List<EntityPosition> positions = new ArrayList<>();
-        
-        for (Map<String, Object> entity : entities) {
-            EntityPosition pos = new EntityPosition();
-            
-            // 获取实体基本信息
-            pos.setName(getStringValue(entity, "name"));
-            pos.setType(getStringValue(entity, "type", "ENTITY"));
-            pos.setEntityId(getStringValue(entity, "tempId", UUID.randomUUID().toString().replace("-", "")));
-            
-            // 获取位置信息
-            Object positionObj = entity.get("position");
-            if (positionObj instanceof Map) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> posMap = (Map<String, Object>) positionObj;
-                pos.setCharStart(getIntValue(posMap, "charStart", -1));
-                pos.setCharEnd(getIntValue(posMap, "charEnd", -1));
-            } else {
-                // 直接从实体中获取
-                pos.setCharStart(getIntValue(entity, "charStart", -1));
-                pos.setCharEnd(getIntValue(entity, "charEnd", -1));
-            }
-            
-            // 验证位置有效性
-            if (pos.getCharStart() >= 0 && pos.getCharEnd() > pos.getCharStart() 
-                    && pos.getCharEnd() <= text.length()) {
-                
-                // 验证实体名称与文本位置是否匹配
-                String textAtPosition = text.substring(pos.getCharStart(), pos.getCharEnd());
-                if (pos.getName() != null && pos.getName().equals(textAtPosition)) {
-                    positions.add(pos);
-                } else {
-                    // 位置不匹配,尝试通过名称查找
-                    // 注意:indexOf 只返回第一个匹配位置,对于重复出现的实体可能不准确
-                    // 理想情况下 NER 服务应该提供准确的位置信息
-                    int foundIndex = text.indexOf(pos.getName());
-                    if (foundIndex >= 0) {
-                        pos.setCharStart(foundIndex);
-                        pos.setCharEnd(foundIndex + pos.getName().length());
-                        positions.add(pos);
-                        log.debug("实体位置校正: name={}, newStart={}(注意:仅匹配首次出现)", pos.getName(), foundIndex);
-                    } else {
-                        log.warn("实体位置无效且无法查找: name={}, charStart={}, charEnd={}", 
-                                pos.getName(), pos.getCharStart(), pos.getCharEnd());
-                    }
-                }
-            } else if (pos.getName() != null) {
-                // 没有有效位置,尝试通过名称查找
-                int foundIndex = text.indexOf(pos.getName());
-                if (foundIndex >= 0) {
-                    pos.setCharStart(foundIndex);
-                    pos.setCharEnd(foundIndex + pos.getName().length());
-                    positions.add(pos);
-                }
-            }
-        }
-        
-        // 按位置排序
-        positions.sort(Comparator.comparingInt(EntityPosition::getCharStart));
-        
-        // 去重和处理重叠
-        return removeOverlappingEntities(positions);
-    }
-    
-    /**
-     * 移除重叠的实体(保留较长的)
-     */
-    private List<EntityPosition> removeOverlappingEntities(List<EntityPosition> positions) {
-        if (positions.size() <= 1) {
-            return positions;
-        }
-        
-        List<EntityPosition> result = new ArrayList<>();
-        EntityPosition prev = null;
-        
-        for (EntityPosition curr : positions) {
-            if (prev == null) {
-                prev = curr;
-            } else if (curr.getCharStart() >= prev.getCharEnd()) {
-                // 不重叠
-                result.add(prev);
-                prev = curr;
-            } else {
-                // 重叠,保留较长的
-                if (curr.getCharEnd() - curr.getCharStart() > prev.getCharEnd() - prev.getCharStart()) {
-                    prev = curr;
-                }
-                // 否则保持 prev
-            }
-        }
-        
-        if (prev != null) {
-            result.add(prev);
-        }
-        
-        return result;
-    }
-    
-    /**
-     * 根据实体位置拆分文本为 TextElement 列表
-     */
-    private List<TextElementDTO> splitTextByEntities(String text, List<EntityPosition> positions) {
-        List<TextElementDTO> elements = new ArrayList<>();
-        int currentPos = 0;
-        
-        for (EntityPosition entityPos : positions) {
-            // 实体前的普通文本
-            if (entityPos.getCharStart() > currentPos) {
-                String beforeText = text.substring(currentPos, entityPos.getCharStart());
-                if (!beforeText.isEmpty()) {
-                    TextElementDTO textEl = new TextElementDTO();
-                    textEl.setType("text_run");
-                    textEl.setContent(beforeText);
-                    elements.add(textEl);
-                }
-            }
-            
-            // 实体元素
-            TextElementDTO entityEl = new TextElementDTO();
-            entityEl.setType("entity");
-            entityEl.setEntityId(entityPos.getEntityId());
-            entityEl.setEntityText(text.substring(entityPos.getCharStart(), entityPos.getCharEnd()));
-            entityEl.setEntityType(entityPos.getType().toUpperCase());
-            entityEl.setConfirmed(false); // NER 自动识别的默认未确认
-            elements.add(entityEl);
-            
-            currentPos = entityPos.getCharEnd();
-        }
-        
-        // 最后剩余的普通文本
-        if (currentPos < text.length()) {
-            String afterText = text.substring(currentPos);
-            if (!afterText.isEmpty()) {
-                TextElementDTO textEl = new TextElementDTO();
-                textEl.setType("text_run");
-                textEl.setContent(afterText);
-                elements.add(textEl);
-            }
-        }
-        
-        return elements;
-    }
-    
-    /**
-     * 筛选落在指定块范围内的实体
-     */
-    private List<Map<String, Object>> filterEntitiesForBlock(List<Map<String, Object>> entities,
-                                                               int blockStart, int blockEnd) {
-        if (entities == null) {
-            return Collections.emptyList();
-        }
-        
-        return entities.stream()
-                .filter(entity -> {
-                    int charStart = getEntityCharStart(entity);
-                    int charEnd = getEntityCharEnd(entity);
-                    // 实体完全在块范围内
-                    return charStart >= blockStart && charEnd <= blockEnd;
-                })
-                .collect(Collectors.toList());
-    }
-    
-    /**
-     * 将实体的全局偏移转换为块内偏移
-     */
-    private List<Map<String, Object>> localizeEntityPositions(List<Map<String, Object>> entities, 
-                                                                int blockStart) {
-        return entities.stream()
-                .map(entity -> {
-                    Map<String, Object> localized = new HashMap<>(entity);
-                    
-                    // 调整位置
-                    Object positionObj = entity.get("position");
-                    if (positionObj instanceof Map) {
-                        @SuppressWarnings("unchecked")
-                        Map<String, Object> posMap = new HashMap<>((Map<String, Object>) positionObj);
-                        int charStart = getIntValue(posMap, "charStart", 0);
-                        int charEnd = getIntValue(posMap, "charEnd", 0);
-                        posMap.put("charStart", charStart - blockStart);
-                        posMap.put("charEnd", charEnd - blockStart);
-                        localized.put("position", posMap);
-                    } else {
-                        int charStart = getIntValue(entity, "charStart", 0);
-                        int charEnd = getIntValue(entity, "charEnd", 0);
-                        localized.put("charStart", charStart - blockStart);
-                        localized.put("charEnd", charEnd - blockStart);
-                    }
-                    
-                    return localized;
-                })
-                .collect(Collectors.toList());
-    }
-    
-    private int getEntityCharStart(Map<String, Object> entity) {
-        Object positionObj = entity.get("position");
-        if (positionObj instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> posMap = (Map<String, Object>) positionObj;
-            return getIntValue(posMap, "charStart", -1);
-        }
-        return getIntValue(entity, "charStart", -1);
-    }
-    
-    private int getEntityCharEnd(Map<String, Object> entity) {
-        Object positionObj = entity.get("position");
-        if (positionObj instanceof Map) {
-            @SuppressWarnings("unchecked")
-            Map<String, Object> posMap = (Map<String, Object>) positionObj;
-            return getIntValue(posMap, "charEnd", -1);
-        }
-        return getIntValue(entity, "charEnd", -1);
-    }
-    
-    // ==================== 工具方法 ====================
-    
-    private String getStringValue(Map<String, Object> map, String key) {
-        return getStringValue(map, key, null);
-    }
-    
-    private String getStringValue(Map<String, Object> map, String key, String defaultValue) {
-        Object value = map.get(key);
-        return value != null ? value.toString() : defaultValue;
-    }
-    
-    private int getIntValue(Map<String, Object> map, String key, int defaultValue) {
-        Object value = map.get(key);
-        if (value instanceof Number) {
-            return ((Number) value).intValue();
-        }
-        if (value instanceof String) {
-            try {
-                return Integer.parseInt((String) value);
-            } catch (NumberFormatException e) {
-                return defaultValue;
-            }
-        }
-        return defaultValue;
-    }
-    
-    // ==================== 数据传输对象 ====================
-    
-    /**
-     * TextElement DTO(与 DocumentBlock.TextElement 对应)
-     */
-    @Data
-    public static class TextElementDTO {
-        private String type;  // text_run / entity / link / mention_doc
-        
-        // text_run
-        private String content;
-        
-        // entity
-        private String entityId;
-        private String entityText;
-        private String entityType;
-        private Boolean confirmed;
-        
-        // link
-        private String url;
-        
-        // mention_doc
-        private String refDocId;
-        private String refDocTitle;
-    }
-    
-    /**
-     * 块信息(用于分配实体)
-     */
-    @Data
-    public static class BlockInfo {
-        private String blockId;
-        private int charStart;
-        private int charEnd;
-        private String text;
-    }
-    
-    /**
-     * 实体位置(内部使用)
-     */
-    @Data
-    private static class EntityPosition {
-        private String entityId;
-        private String name;
-        private String type;
-        private int charStart;
-        private int charEnd;
-    }
-}

+ 1 - 17
backend/lingyue-starter/src/main/resources/application.properties

@@ -214,20 +214,4 @@ extract.ai.retry-delay=1000
 extract.cache.enabled=true
 extract.cache.ttl=3600
 
-# ============================================
-# Neo4j 图数据库配置
-# ============================================
-# 服务器已安装 Neo4j 2025.03.0
-neo4j.enabled=${NEO4J_ENABLED:true}
-# Neo4j 连接地址(Bolt 协议)
-neo4j.uri=${NEO4J_URI:bolt://localhost:7687}
-# Neo4j 用户名
-neo4j.username=${NEO4J_USERNAME:neo4j}
-# Neo4j 密码
-neo4j.password=${NEO4J_PASSWORD:geekgeek}
-# Neo4j 数据库名称(4.0+ 支持多数据库)
-neo4j.database=${NEO4J_DATABASE:neo4j}
-# 连接超时时间(秒)
-neo4j.connection-timeout-seconds=30
-# 最大连接池大小
-neo4j.max-connection-pool-size=50
+# Neo4j 已移除,图数据仅使用 PostgreSQL(如需要)

+ 0 - 8
backend/pom.xml

@@ -55,7 +55,6 @@
         <webjars-locator.version>0.52</webjars-locator.version>
         <pdfbox.version>3.0.1</pdfbox.version>
         <poi.version>5.2.5</poi.version>
-        <neo4j-driver.version>5.15.0</neo4j-driver.version>
     </properties>
 
     <dependencyManagement>
@@ -183,13 +182,6 @@
                 <version>${webjars-locator.version}</version>
             </dependency>
             
-            <!-- Neo4j Driver -->
-            <dependency>
-                <groupId>org.neo4j.driver</groupId>
-                <artifactId>neo4j-java-driver</artifactId>
-                <version>${neo4j-driver.version}</version>
-            </dependency>
-            
             <!-- Common Module -->
             <dependency>
                 <groupId>com.lingyue</groupId>

+ 0 - 16
backend/sql/fix_graph_nodes_fk.sql

@@ -1,16 +0,0 @@
--- 修复 graph_nodes 表的外键约束问题
--- user_id 不再需要外键约束,因为自动 NER 提取时没有用户上下文
-
--- 删除 user_id 的外键约束
-ALTER TABLE graph_nodes DROP CONSTRAINT IF EXISTS fk_graph_nodes_user;
-
--- 确认删除成功
-SELECT 
-    conname AS constraint_name,
-    conrelid::regclass AS table_name
-FROM pg_constraint 
-WHERE conrelid = 'graph_nodes'::regclass 
-  AND contype = 'f';
-
--- 完成
-SELECT 'graph_nodes user_id 外键约束已移除' AS status;

+ 0 - 102
backend/sql/graph_tables.sql

@@ -1,102 +0,0 @@
--- 灵越智报 v2.0 图数据库表结构
--- PostgreSQL 15+
--- 用于 NER 实体识别结果存储
-
--- ============================================
--- 1. 图节点表(graph_nodes)
--- ============================================
--- 存储从文档中提取的命名实体
-CREATE TABLE IF NOT EXISTS graph_nodes (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL,
-    user_id VARCHAR(36),  -- 可为空,自动提取时可能没有用户上下文
-    name VARCHAR(255) NOT NULL,
-    type VARCHAR(50) NOT NULL, -- text/table/image/number/date/ORG/PERSON/LOC/TIME/DEVICE/PROJECT/METHOD等
-    value TEXT,
-    position JSONB, -- {charStart, charEnd, line}
-    parent_id VARCHAR(36),
-    level INTEGER DEFAULT 0,
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
--- 添加外键约束(如果关联表存在)
--- 注意:user_id 不添加外键约束,因为自动 NER 提取时可能没有用户上下文
-DO $$
-BEGIN
-    IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'documents') THEN
-        ALTER TABLE graph_nodes DROP CONSTRAINT IF EXISTS fk_graph_nodes_document;
-        ALTER TABLE graph_nodes ADD CONSTRAINT fk_graph_nodes_document
-            FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE;
-    END IF;
-    
-    -- user_id 外键约束已移除,允许任意值或 NULL
-    -- 如需恢复,取消下面注释:
-    -- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'users') THEN
-    --     ALTER TABLE graph_nodes DROP CONSTRAINT IF EXISTS fk_graph_nodes_user;
-    --     ALTER TABLE graph_nodes ADD CONSTRAINT fk_graph_nodes_user
-    --         FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
-    -- END IF;
-END $$;
-
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_document_id ON graph_nodes(document_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_user_id ON graph_nodes(user_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_type ON graph_nodes(type);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_parent_id ON graph_nodes(parent_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_position ON graph_nodes USING GIN(position);
-
--- ============================================
--- 2. 图关系表(graph_relations)
--- ============================================
--- 存储实体之间的关系
-CREATE TABLE IF NOT EXISTS graph_relations (
-    id VARCHAR(36) PRIMARY KEY,
-    from_node_id VARCHAR(36) NOT NULL,
-    to_node_id VARCHAR(36) NOT NULL,
-    relation_type VARCHAR(50) NOT NULL, -- BELONGS_TO/USES/LOCATED_IN/EXECUTES/MONITORS等
-    action_type VARCHAR(50),
-    action_config JSONB,
-    order_index INTEGER DEFAULT 0,
-    condition_expr TEXT,
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    CONSTRAINT fk_graph_relations_from_node FOREIGN KEY (from_node_id) REFERENCES graph_nodes(id) ON DELETE CASCADE,
-    CONSTRAINT fk_graph_relations_to_node FOREIGN KEY (to_node_id) REFERENCES graph_nodes(id) ON DELETE CASCADE
-);
-
-CREATE INDEX IF NOT EXISTS idx_graph_relations_from_node ON graph_relations(from_node_id);
-CREATE INDEX IF NOT EXISTS idx_graph_relations_to_node ON graph_relations(to_node_id);
-CREATE INDEX IF NOT EXISTS idx_graph_relations_type ON graph_relations(relation_type);
-
--- ============================================
--- 创建更新时间触发器函数(如果不存在)
--- ============================================
-CREATE OR REPLACE FUNCTION update_updated_at_column()
-RETURNS TRIGGER AS $$
-BEGIN
-    NEW.update_time = CURRENT_TIMESTAMP;
-    RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
--- 创建触发器
-DROP TRIGGER IF EXISTS update_graph_nodes_updated_at ON graph_nodes;
-CREATE TRIGGER update_graph_nodes_updated_at 
-    BEFORE UPDATE ON graph_nodes
-    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-
-DROP TRIGGER IF EXISTS update_graph_relations_updated_at ON graph_relations;
-CREATE TRIGGER update_graph_relations_updated_at 
-    BEFORE UPDATE ON graph_relations
-    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-
--- ============================================
--- 说明
--- ============================================
--- 执行此脚本:
--- psql -U lingyue -d lingyue_zhibao -f graph_tables.sql
---
--- 或在服务器上:
--- psql -U postgres -d lingyue_zhibao -f graph_tables.sql

+ 0 - 234
backend/sql/init.sql

@@ -1,234 +0,0 @@
--- 灵越智报 v2.0 数据库初始化脚本
--- PostgreSQL 15+
--- 注意: ID 使用 VARCHAR(36) 以兼容 MyBatis-Plus 的 ASSIGN_UUID 策略
-
--- 创建数据库(如果不存在)
--- CREATE DATABASE lingyue_zhibao;
-
--- 连接到数据库
--- \c lingyue_zhibao;
-
--- 启用UUID扩展
-CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-
--- ============================================
--- 1. 用户表 (users)
--- ============================================
-CREATE TABLE IF NOT EXISTS users (
-    id VARCHAR(36) PRIMARY KEY,
-    username VARCHAR(50) UNIQUE NOT NULL,
-    email VARCHAR(100) UNIQUE NOT NULL,
-    password_hash VARCHAR(255) NOT NULL,
-    avatar_url VARCHAR(500),
-    role VARCHAR(20) NOT NULL DEFAULT 'user', -- admin/user/guest
-    preferences TEXT DEFAULT '{}', -- JSON字符串格式存储偏好设置
-    last_login_at TIMESTAMP,
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-
--- ============================================
--- 2. 文档表 (documents)
--- ============================================
-CREATE TABLE IF NOT EXISTS documents (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    name VARCHAR(255) NOT NULL,
-    type VARCHAR(20) NOT NULL, -- pdf/word/image/markdown/other
-    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/uploading/parsing/completed/failed
-    file_size BIGINT,
-    file_url VARCHAR(500),
-    thumbnail_url VARCHAR(500),
-    parsed_text TEXT,
-    parse_status VARCHAR(20), -- pending/parsing/completed/failed
-    parse_progress INTEGER DEFAULT 0, -- 0-100
-    parse_error TEXT,
-    parse_started_at TIMESTAMP,
-    parse_completed_at TIMESTAMP,
-    metadata JSONB DEFAULT '{}', -- pageCount, ocrConfidence, layoutStructure等
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_documents_user_id ON documents(user_id);
-CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status);
-CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(type);
-CREATE INDEX IF NOT EXISTS idx_documents_created_at ON documents(create_time DESC);
-CREATE INDEX IF NOT EXISTS idx_documents_metadata ON documents USING GIN(metadata);
-CREATE INDEX IF NOT EXISTS idx_documents_parsed_text ON documents USING GIN(to_tsvector('english', parsed_text)); -- 全文搜索
-
--- ============================================
--- 3. 要素表 (elements)
--- ============================================
-CREATE TABLE IF NOT EXISTS elements (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    type VARCHAR(20) NOT NULL, -- amount/company/person/location/date/other
-    label VARCHAR(100) NOT NULL,
-    value TEXT NOT NULL,
-    position JSONB, -- {page, x, y, width, height}
-    confidence DECIMAL(3,2), -- 0.00-1.00
-    extraction_method VARCHAR(20), -- ai/regex/rule/manual
-    graph_node_id VARCHAR(36), -- 关联的图节点ID
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_elements_document_id ON elements(document_id);
-CREATE INDEX IF NOT EXISTS idx_elements_user_id ON elements(user_id);
-CREATE INDEX IF NOT EXISTS idx_elements_type ON elements(type);
-CREATE INDEX IF NOT EXISTS idx_elements_graph_node_id ON elements(graph_node_id);
-CREATE INDEX IF NOT EXISTS idx_elements_position ON elements USING GIN(position);
-
--- ============================================
--- 4. 批注表 (annotations)
--- ============================================
-CREATE TABLE IF NOT EXISTS annotations (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    text TEXT NOT NULL,
-    position JSONB NOT NULL, -- {page, start: {x, y}, end: {x, y}}
-    type VARCHAR(20) NOT NULL, -- highlight/strikethrough/suggestion
-    suggestion TEXT,
-    ai_generated BOOLEAN DEFAULT FALSE,
-    confidence DECIMAL(3,2),
-    status VARCHAR(20) DEFAULT 'pending', -- pending/accepted/rejected
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_annotations_document_id ON annotations(document_id);
-CREATE INDEX IF NOT EXISTS idx_annotations_user_id ON annotations(user_id);
-CREATE INDEX IF NOT EXISTS idx_annotations_type ON annotations(type);
-CREATE INDEX IF NOT EXISTS idx_annotations_status ON annotations(status);
-
--- ============================================
--- 5. 关系网络表 (graphs)
--- ============================================
-CREATE TABLE IF NOT EXISTS graphs (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    name VARCHAR(255) NOT NULL,
-    nodes JSONB NOT NULL DEFAULT '[]', -- GraphNode数组
-    edges JSONB NOT NULL DEFAULT '[]', -- GraphEdge数组
-    calculation_result JSONB,
-    calculation_status VARCHAR(20), -- pending/completed/failed
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_graphs_document_id ON graphs(document_id);
-CREATE INDEX IF NOT EXISTS idx_graphs_user_id ON graphs(user_id);
-CREATE INDEX IF NOT EXISTS idx_graphs_nodes ON graphs USING GIN(nodes);
-CREATE INDEX IF NOT EXISTS idx_graphs_edges ON graphs USING GIN(edges);
-
--- ============================================
--- 6. 解析任务表 (parse_tasks)
--- ============================================
-CREATE TABLE IF NOT EXISTS parse_tasks (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/processing/completed/failed
-    progress INTEGER DEFAULT 0, -- 0-100
-    current_step VARCHAR(100),
-    error_message TEXT,
-    options JSONB DEFAULT '{}', -- 解析选项
-    started_at TIMESTAMP,
-    completed_at TIMESTAMP,
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_parse_tasks_document_id ON parse_tasks(document_id);
-CREATE INDEX IF NOT EXISTS idx_parse_tasks_status ON parse_tasks(status);
-
--- ============================================
--- 7. 会话表 (sessions)
--- ============================================
-CREATE TABLE IF NOT EXISTS sessions (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    token_hash VARCHAR(255) NOT NULL UNIQUE,
-    refresh_token_hash VARCHAR(255) NOT NULL UNIQUE,
-    expires_at TIMESTAMP NOT NULL,
-    ip_address VARCHAR(45),
-    user_agent TEXT,
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
-CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
-CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
-
--- ============================================
--- 创建更新时间触发器函数
--- ============================================
-CREATE OR REPLACE FUNCTION update_update_time_column()
-RETURNS TRIGGER AS $$
-BEGIN
-    NEW.update_time = CURRENT_TIMESTAMP;
-    RETURN NEW;
-END;
-$$ language 'plpgsql';
-
--- 为所有表创建更新时间触发器
-DROP TRIGGER IF EXISTS update_users_updated_at ON users;
-CREATE TRIGGER update_users_update_time BEFORE UPDATE ON users
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_documents_updated_at ON documents;
-CREATE TRIGGER update_documents_update_time BEFORE UPDATE ON documents
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_elements_updated_at ON elements;
-CREATE TRIGGER update_elements_update_time BEFORE UPDATE ON elements
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_annotations_updated_at ON annotations;
-CREATE TRIGGER update_annotations_update_time BEFORE UPDATE ON annotations
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_graphs_updated_at ON graphs;
-CREATE TRIGGER update_graphs_update_time BEFORE UPDATE ON graphs
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_parse_tasks_updated_at ON parse_tasks;
-CREATE TRIGGER update_parse_tasks_update_time BEFORE UPDATE ON parse_tasks
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_sessions_updated_at ON sessions;
-CREATE TRIGGER update_sessions_update_time BEFORE UPDATE ON sessions
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();

+ 0 - 55
backend/sql/init_supplement_only.sh

@@ -1,55 +0,0 @@
-#!/bin/bash
-
-# ============================================
-# 仅初始化补充表(不删除现有表)
-# ============================================
-
-set -e
-
-# 颜色定义
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m'
-
-# 数据库配置
-DB_HOST=${DB_HOST:-localhost}
-DB_PORT=${DB_PORT:-5432}
-DB_NAME=${DB_NAME:-lingyue_zhibao}
-DB_USER=${DB_USER:-postgres}
-
-log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
-log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-log_title() { echo -e "\n${BLUE}========== $1 ==========${NC}\n"; }
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SQL_DIR="${SCRIPT_DIR}"
-
-log_title "初始化补充表"
-echo "数据库: ${DB_NAME}"
-echo "主机: ${DB_HOST}:${DB_PORT}"
-echo "用户: ${DB_USER}"
-echo ""
-
-log_info "执行 supplement_tables.sql..."
-PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "${SQL_DIR}/supplement_tables.sql"
-
-if [ $? -eq 0 ]; then
-    log_info "补充表初始化成功"
-    
-    log_info "验证表创建..."
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
-SELECT tablename 
-FROM pg_tables 
-WHERE schemaname = 'public' 
-AND tablename IN ('text_storage', 'vector_embeddings', 'knowledge_base', 'rag_sessions', 'rag_messages')
-ORDER BY tablename;
-EOF
-    
-    log_title "完成"
-else
-    log_error "补充表初始化失败"
-    exit 1
-fi

+ 0 - 127
backend/sql/rag_tables.sql

@@ -1,127 +0,0 @@
--- ============================================
--- RAG 向量化存储相关表
--- 灵越智报 v2.0
--- ============================================
-
--- 启用 pgvector 扩展(需要先安装:apt install postgresql-15-pgvector)
-CREATE EXTENSION IF NOT EXISTS vector;
-
--- ============================================
--- 1. 文本分块表 (text_chunks)
--- ============================================
-CREATE TABLE IF NOT EXISTS text_chunks (
-    id VARCHAR(32) PRIMARY KEY,
-    document_id VARCHAR(32) NOT NULL,
-    text_storage_id VARCHAR(32),
-    chunk_index INTEGER NOT NULL,
-    content TEXT NOT NULL,
-    token_count INTEGER,
-    metadata JSONB DEFAULT '{}',
-    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
--- 索引
-CREATE INDEX IF NOT EXISTS idx_text_chunks_document_id ON text_chunks(document_id);
-CREATE INDEX IF NOT EXISTS idx_text_chunks_text_storage_id ON text_chunks(text_storage_id);
-CREATE INDEX IF NOT EXISTS idx_text_chunks_chunk_index ON text_chunks(document_id, chunk_index);
-
--- 更新时间触发器
-CREATE TRIGGER update_text_chunks_updated_at BEFORE UPDATE ON text_chunks
-    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-
--- ============================================
--- 2. 向量嵌入表 (vector_embeddings)
--- ============================================
-CREATE TABLE IF NOT EXISTS vector_embeddings (
-    id VARCHAR(32) PRIMARY KEY,
-    chunk_id VARCHAR(32) NOT NULL REFERENCES text_chunks(id) ON DELETE CASCADE,
-    embedding vector(768),  -- nomic-embed-text 维度为 768
-    model_name VARCHAR(100) DEFAULT 'nomic-embed-text',
-    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
--- 普通索引
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_chunk_id ON vector_embeddings(chunk_id);
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_model ON vector_embeddings(model_name);
-
--- HNSW 向量索引(用于高效相似度检索)
--- 使用余弦距离操作符
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_hnsw ON vector_embeddings 
-    USING hnsw (embedding vector_cosine_ops);
-
--- ============================================
--- 3. 辅助函数:向量相似度检索
--- ============================================
-
--- 按文档ID检索相似文本块
-CREATE OR REPLACE FUNCTION search_similar_chunks(
-    query_embedding vector(768),
-    target_document_id VARCHAR(32),
-    result_limit INTEGER DEFAULT 3
-)
-RETURNS TABLE (
-    chunk_id VARCHAR(32),
-    document_id VARCHAR(32),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
-BEGIN
-    RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
-    FROM text_chunks tc
-    JOIN vector_embeddings ve ON tc.id = ve.chunk_id
-    WHERE tc.document_id = target_document_id
-    ORDER BY ve.embedding <=> query_embedding
-    LIMIT result_limit;
-END;
-$$ LANGUAGE plpgsql;
-
--- 全局检索相似文本块(不限制文档)
-CREATE OR REPLACE FUNCTION search_similar_chunks_global(
-    query_embedding vector(768),
-    result_limit INTEGER DEFAULT 5
-)
-RETURNS TABLE (
-    chunk_id VARCHAR(32),
-    document_id VARCHAR(32),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
-BEGIN
-    RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
-    FROM text_chunks tc
-    JOIN vector_embeddings ve ON tc.id = ve.chunk_id
-    ORDER BY ve.embedding <=> query_embedding
-    LIMIT result_limit;
-END;
-$$ LANGUAGE plpgsql;
-
--- ============================================
--- 4. 注释
--- ============================================
-COMMENT ON TABLE text_chunks IS '文本分块表,存储文档分块后的文本片段';
-COMMENT ON COLUMN text_chunks.document_id IS '关联的文档ID';
-COMMENT ON COLUMN text_chunks.text_storage_id IS '关联的文本存储ID';
-COMMENT ON COLUMN text_chunks.chunk_index IS '分块在文档中的顺序索引';
-COMMENT ON COLUMN text_chunks.content IS '分块文本内容';
-COMMENT ON COLUMN text_chunks.token_count IS '估算的Token数量';
-COMMENT ON COLUMN text_chunks.metadata IS '元数据(如页码、段落位置等)';
-
-COMMENT ON TABLE vector_embeddings IS '向量嵌入表,存储文本块的向量表示';
-COMMENT ON COLUMN vector_embeddings.chunk_id IS '关联的文本分块ID';
-COMMENT ON COLUMN vector_embeddings.embedding IS '768维向量嵌入(nomic-embed-text)';
-COMMENT ON COLUMN vector_embeddings.model_name IS '使用的嵌入模型名称';

+ 0 - 115
backend/sql/rag_tables_compatible.sql

@@ -1,115 +0,0 @@
--- ============================================
--- RAG 向量化存储相关表(兼容 SimpleModel 版本)
--- 灵越智报 v2.0
--- 字段名与 SimpleModel 基类兼容
--- ============================================
-
--- 启用 pgvector 扩展(需要先安装:apt install postgresql-15-pgvector)
-CREATE EXTENSION IF NOT EXISTS vector;
-
--- ============================================
--- 1. 文本分块表 (text_chunks)
--- ============================================
-CREATE TABLE IF NOT EXISTS text_chunks (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL,
-    text_storage_id VARCHAR(36),
-    chunk_index INTEGER NOT NULL,
-    content TEXT NOT NULL,
-    token_count INTEGER,
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
--- 索引
-CREATE INDEX IF NOT EXISTS idx_text_chunks_document_id ON text_chunks(document_id);
-CREATE INDEX IF NOT EXISTS idx_text_chunks_text_storage_id ON text_chunks(text_storage_id);
-CREATE INDEX IF NOT EXISTS idx_text_chunks_chunk_index ON text_chunks(document_id, chunk_index);
-
--- ============================================
--- 2. 向量嵌入表 (vector_embeddings)
--- ============================================
-CREATE TABLE IF NOT EXISTS vector_embeddings (
-    id VARCHAR(36) PRIMARY KEY,
-    chunk_id VARCHAR(36) NOT NULL REFERENCES text_chunks(id) ON DELETE CASCADE,
-    embedding vector(768),  -- nomic-embed-text 维度为 768
-    model_name VARCHAR(100) DEFAULT 'nomic-embed-text',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
--- 普通索引
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_chunk_id ON vector_embeddings(chunk_id);
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_model ON vector_embeddings(model_name);
-
--- HNSW 向量索引(用于高效相似度检索)
--- 使用余弦距离操作符
-CREATE INDEX IF NOT EXISTS idx_vector_embeddings_hnsw ON vector_embeddings 
-    USING hnsw (embedding vector_cosine_ops);
-
--- ============================================
--- 3. 辅助函数:向量相似度检索
--- ============================================
-
--- 按文档ID检索相似文本块
-CREATE OR REPLACE FUNCTION search_similar_chunks(
-    query_embedding vector(768),
-    target_document_id VARCHAR(36),
-    result_limit INTEGER DEFAULT 3
-)
-RETURNS TABLE (
-    chunk_id VARCHAR(36),
-    document_id VARCHAR(36),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
-BEGIN
-    RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
-    FROM text_chunks tc
-    JOIN vector_embeddings ve ON tc.id = ve.chunk_id
-    WHERE tc.document_id = target_document_id
-    ORDER BY ve.embedding <=> query_embedding
-    LIMIT result_limit;
-END;
-$$ LANGUAGE plpgsql;
-
--- 全局检索相似文本块(不限制文档)
-CREATE OR REPLACE FUNCTION search_similar_chunks_global(
-    query_embedding vector(768),
-    result_limit INTEGER DEFAULT 5
-)
-RETURNS TABLE (
-    chunk_id VARCHAR(36),
-    document_id VARCHAR(36),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
-BEGIN
-    RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
-    FROM text_chunks tc
-    JOIN vector_embeddings ve ON tc.id = ve.chunk_id
-    ORDER BY ve.embedding <=> query_embedding
-    LIMIT result_limit;
-END;
-$$ LANGUAGE plpgsql;
-
--- 显示创建结果
-SELECT 'RAG 表创建成功(兼容 SimpleModel)' AS result;

+ 73 - 349
backend/sql/rebuild_all.sh

@@ -1,18 +1,8 @@
 #!/bin/bash
-
-# ============================================
-# 数据库完整重建脚本
-# 删除所有表并重新初始化(包含所有迁移)
-# 
-# 更新日志:
-#   2026-01-23: 添加 all_tables.sql 整合文件,--simple 模式
-#   2026-01-23: 添加 --drop-only, --init-only, --list-migrations 选项
-#   2026-01-22: 添加 extract_tables 迁移支持
-# ============================================
+# 数据库完整重建脚本:删除所有表后执行 database/init.sql(单文件初始化)
 
 set -e
 
-# 颜色定义
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 YELLOW='\033[1;33m'
@@ -20,7 +10,6 @@ BLUE='\033[0;34m'
 CYAN='\033[0;36m'
 NC='\033[0m'
 
-# 数据库配置(根据实际情况修改)
 DB_HOST=${DB_HOST:-localhost}
 DB_PORT=${DB_PORT:-5432}
 DB_NAME=${DB_NAME:-lingyue_zhibao}
@@ -33,395 +22,130 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
 log_title() { echo -e "\n${BLUE}========== $1 ==========${NC}\n"; }
 log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
 
-# 获取脚本所在目录
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-SQL_DIR="${SCRIPT_DIR}"
-MIGRATIONS_DIR="${PROJECT_ROOT}/database/migrations"
+INIT_SQL="${PROJECT_ROOT}/database/init.sql"
+
+execute_sql() {
+    local file="$1"
+    local desc="${2:-$(basename "$file")}"
+    if [ ! -f "$file" ]; then
+        log_error "文件不存在: $file"
+        return 1
+    fi
+    log_step "执行: $desc"
+    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "$file" || return 1
+}
 
-# 删除所有表
 drop_all_tables() {
     log_title "删除所有表"
-    
     if [ "$FORCE" != "yes" ]; then
         log_warn "这将删除数据库 ${DB_NAME} 中的所有表和数据!"
         read -p "是否继续?(yes/no): " confirm
-        
         if [ "$confirm" != "yes" ]; then
             log_info "操作已取消"
             exit 0
         fi
     fi
-    
     log_info "开始删除表..."
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
--- 禁用外键约束检查
+    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<'EOF'
 SET session_replication_role = 'replica';
-
--- 删除所有表
-DO \$\$
-DECLARE
-    r RECORD;
+DO $$
+DECLARE r RECORD;
 BEGIN
     FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
     LOOP
         EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
         RAISE NOTICE 'Dropped table: %', r.tablename;
     END LOOP;
-END
-\$\$;
-
--- 删除所有序列
-DO \$\$
-DECLARE
-    r RECORD;
+END $$;
+DO $$
+DECLARE r RECORD;
 BEGIN
     FOR r IN (SELECT sequencename FROM pg_sequences WHERE schemaname = 'public')
     LOOP
         EXECUTE 'DROP SEQUENCE IF EXISTS public.' || quote_ident(r.sequencename) || ' CASCADE';
     END LOOP;
-END
-\$\$;
-
--- 删除所有函数
-DO \$\$
-DECLARE
-    r RECORD;
+END $$;
+DO $$
+DECLARE r RECORD;
 BEGIN
-    FOR r IN (SELECT proname, oidvectortypes(proargtypes) as args 
-              FROM pg_proc 
-              INNER JOIN pg_namespace ns ON (pg_proc.pronamespace = ns.oid)
-              WHERE ns.nspname = 'public')
+    FOR r IN (SELECT proname, oidvectortypes(proargtypes) as args FROM pg_proc INNER JOIN pg_namespace ns ON (pg_proc.pronamespace = ns.oid) WHERE ns.nspname = 'public')
     LOOP
         EXECUTE 'DROP FUNCTION IF EXISTS public.' || quote_ident(r.proname) || '(' || r.args || ') CASCADE';
     END LOOP;
-END
-\$\$;
-
--- 恢复外键约束检查
+END $$;
 SET session_replication_role = 'origin';
-
-SELECT 'Tables remaining:' as status, COUNT(*) as count FROM pg_tables WHERE schemaname = 'public';
 EOF
-    
-    log_info "所有表已删除"
-}
-
-# 执行 SQL 文件
-execute_sql() {
-    local file=$1
-    local desc=$2
-    
-    if [ -f "$file" ]; then
-        log_info "执行: ${desc} (${file})"
-        PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "$file" 2>&1 | grep -v "^NOTICE:" || true
-        log_info "${desc} 完成"
-    else
-        log_warn "文件不存在: ${file}"
-    fi
-}
-
-# 列出迁移脚本
-list_migrations() {
-    log_title "迁移脚本列表"
-    
-    if [ -d "$MIGRATIONS_DIR" ]; then
-        local count=0
-        echo ""
-        printf "  %-5s %-60s %s\n" "序号" "文件名" "大小"
-        echo "  -------------------------------------------------------------------------"
-        for migration in $(ls -1 "${MIGRATIONS_DIR}"/*.sql 2>/dev/null | sort); do
-            if [ -f "$migration" ]; then
-                count=$((count + 1))
-                local size=$(du -h "$migration" | cut -f1)
-                printf "  %-5s %-60s %s\n" "$count" "$(basename $migration)" "$size"
-            fi
-        done
-        echo ""
-        log_info "共 ${count} 个迁移脚本"
-    else
-        log_warn "迁移目录不存在: ${MIGRATIONS_DIR}"
-    fi
-}
-
-# 简单模式初始化(使用 all_tables.sql 单文件)
-init_simple() {
-    log_title "初始化数据库表(简单模式)"
-    log_info "使用 all_tables.sql 单文件初始化所有表"
-    execute_sql "${SQL_DIR}/all_tables.sql" "全部表结构 (21个表)"
-}
-
-# 初始化所有表(分文件模式)
-init_all_tables() {
-    log_title "初始化数据库表"
-    
-    local step=1
-    
-    # 1. 基础表(users, documents 等核心表)
-    log_step "[$step/6] 基础表"
-    execute_sql "${SQL_DIR}/init.sql" "基础表 (users, documents, elements, etc.)"
-    step=$((step + 1))
-    
-    # 2. 图谱表(graph_nodes, graph_relations - 先于 supplement_tables)
-    log_step "[$step/6] 图谱表"
-    execute_sql "${SQL_DIR}/graph_tables.sql" "图谱表 (graph_nodes, graph_relations)"
-    step=$((step + 1))
-    
-    # 3. 补充表(rules, data_sources, text_storage 等,不含 templates)
-    log_step "[$step/6] 补充表"
-    execute_sql "${SQL_DIR}/supplement_tables.sql" "补充表 (rules, data_sources, text_storage)"
-    step=$((step + 1))
-    
-    # 4. RAG 表(text_chunks, vector_embeddings - 可能需要 pgvector)
-    log_step "[$step/6] RAG 表"
-    execute_sql "${SQL_DIR}/rag_tables_compatible.sql" "RAG 表 (text_chunks, vector_embeddings)"
-    step=$((step + 1))
-    
-    # 5. 模板系统表(templates, source_files, variables, generations)
-    log_step "[$step/6] 模板系统表"
-    execute_sql "${SQL_DIR}/template_tables.sql" "模板系统表 (templates, source_files, variables, generations)"
-    step=$((step + 1))
-    
-    # 6. 执行迁移文件(按文件名排序)
-    log_step "[$step/6] 迁移脚本"
-    log_title "执行迁移脚本"
-    if [ -d "$MIGRATIONS_DIR" ]; then
-        local migration_count=0
-        local total_migrations=$(ls -1 "${MIGRATIONS_DIR}"/*.sql 2>/dev/null | wc -l)
-        
-        for migration in $(ls -1 "${MIGRATIONS_DIR}"/*.sql 2>/dev/null | sort); do
-            if [ -f "$migration" ]; then
-                migration_count=$((migration_count + 1))
-                execute_sql "$migration" "[$migration_count/$total_migrations] $(basename $migration)"
-            fi
-        done
-        log_info "共执行 ${migration_count} 个迁移脚本"
-    else
-        log_warn "迁移目录不存在: ${MIGRATIONS_DIR}"
-    fi
+    log_info "删除完成"
 }
 
-# 验证表创建
-verify_tables() {
-    log_title "验证表创建"
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
-SELECT tablename as "表名"
-FROM pg_tables 
-WHERE schemaname = 'public'
-ORDER BY tablename;
-
-SELECT COUNT(*) as "表总数" FROM pg_tables WHERE schemaname = 'public';
-EOF
+init_database() {
+    log_title "初始化数据库"
+    execute_sql "${INIT_SQL}" "database/init.sql(全表结构)"
 }
 
-# 创建测试用户
 create_test_user() {
     log_title "创建测试用户"
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
--- 插入测试管理员用户(密码: Admin123!)
+    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<'EOF'
 INSERT INTO users (id, username, email, password_hash, role, create_time, update_time)
-VALUES (
-    '00000000000000000000000000000001',
-    'admin',
-    'admin@lingyue.com',
-    '\$2a\$10\$N.zmdr9k7uOCQb376NoUnuTJ8iUtkWBrq.VZbISbAi1L9sNPIiMMi',
-    'admin',
-    CURRENT_TIMESTAMP,
-    CURRENT_TIMESTAMP
-)
+VALUES ('00000000000000000000000000000001', 'admin', 'admin@lingyue.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iUtkWBrq.VZbISbAi1L9sNPIiMMi', 'admin', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
 ON CONFLICT (id) DO NOTHING;
-
--- 插入测试普通用户(密码: User1234!)
 INSERT INTO users (id, username, email, password_hash, role, create_time, update_time)
-VALUES (
-    '00000000000000000000000000000002',
-    'testuser',
-    'test@lingyue.com',
-    '\$2a\$10\$5XvCJaLqDXJz.YqL8TnNKegHH7q6KM7YFzMVxLPNMvJx5f7mxnKQi',
-    'user',
-    CURRENT_TIMESTAMP,
-    CURRENT_TIMESTAMP
-)
+VALUES ('00000000000000000000000000000002', 'testuser', 'test@lingyue.com', '$2a$10$5XvCJaLqDXJz.YqL8TnNKegHH7q6KM7YFzMVxLPNMvJx5f7mxnKQi', 'user', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
 ON CONFLICT (id) DO NOTHING;
-
 SELECT id, username, email, role FROM users;
 EOF
-    
-    log_info "测试用户创建完成"
-    log_info "  admin / Admin123!"
-    log_info "  testuser / User1234!"
+    log_info "测试用户: admin/Admin123!  testuser/User1234!"
 }
 
-# 显示帮助
 show_help() {
     cat <<EOF
-数据库完整重建脚本
-
 用法: ./rebuild_all.sh [选项]
-
 选项:
-    -h, --help          显示帮助
-    -f, --force         跳过确认提示
-    --no-test-user      不创建测试用户
-    --drop-only         只删除表,不初始化
-    --init-only         只初始化,不删除(用于全新数据库)
-    --simple            使用 all_tables.sql 单文件初始化(推荐)
-    --list-migrations   列出所有迁移脚本后退出
-
-环境变量:
-    DB_HOST         数据库主机 (默认: localhost)
-    DB_PORT         数据库端口 (默认: 5432)
-    DB_NAME         数据库名称 (默认: lingyue_zhibao)
-    DB_USER         数据库用户 (默认: postgres)
-    DB_PASSWORD     数据库密码 (默认: postgres)
-
-示例:
-    # 使用默认配置(删除 + 重建)
-    ./rebuild_all.sh
-
-    # 指定数据库配置
-    DB_PASSWORD=mypassword ./rebuild_all.sh
-
-    # 强制执行(跳过确认)
-    ./rebuild_all.sh -f
-
-    # 只删除所有表
-    ./rebuild_all.sh --drop-only -f
-
-    # 初始化全新数据库(不删除)
-    ./rebuild_all.sh --init-only
-
-    # 查看迁移脚本列表
-    ./rebuild_all.sh --list-migrations
-
-    # 简单模式(使用 all_tables.sql 单文件,推荐)
-    ./rebuild_all.sh --simple -f
-
-    # 服务器上执行
-    DB_USER=lingyue DB_PASSWORD=123123 ./rebuild_all.sh
-
-执行顺序:
-    1. 删除所有表、序列、函数
-    2. 执行 init.sql(用户、文档、元素等基础表)
-    3. 执行 graph_tables.sql(图谱表)
-    4. 执行 supplement_tables.sql(补充表,不含 templates)
-    5. 执行 rag_tables_compatible.sql(RAG表)
-    6. 执行 template_tables.sql(模板系统表 v2.0)
-       - templates: 报告模板
-       - source_files: 来源文件定义
-       - variables: 模板变量
-       - generations: 生成任务
-    7. 执行 database/migrations/*.sql(迁移脚本,按名称排序)
-    8. 创建测试用户
-    9. 验证表创建
+  -h, --help       显示帮助
+  -f, --force      跳过确认
+  --drop-only      只删除表
+  --init-only      只执行 database/init.sql(不删除)
+  --no-test-user   不创建测试用户
 
+环境变量: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
 EOF
 }
 
-# 主函数
-main() {
-    CREATE_TEST_USER=true
-    DROP_ONLY=false
-    INIT_ONLY=false
-    LIST_MIGRATIONS=false
-    SIMPLE_MODE=false
-    
-    while [[ $# -gt 0 ]]; do
-        case $1 in
-            -h|--help)
-                show_help
-                exit 0
-                ;;
-            -f|--force)
-                FORCE=yes
-                shift
-                ;;
-            --no-test-user)
-                CREATE_TEST_USER=false
-                shift
-                ;;
-            --drop-only)
-                DROP_ONLY=true
-                shift
-                ;;
-            --simple)
-                SIMPLE_MODE=true
-                shift
-                ;;
-            --init-only)
-                INIT_ONLY=true
-                shift
-                ;;
-            --list-migrations)
-                LIST_MIGRATIONS=true
-                shift
-                ;;
-            *)
-                log_error "未知选项: $1"
-                show_help
-                exit 1
-                ;;
-        esac
-    done
-    
-    log_title "数据库完整重建脚本"
-    echo "数据库: ${DB_NAME}"
-    echo "主机: ${DB_HOST}:${DB_PORT}"
-    echo "用户: ${DB_USER}"
-    echo "SQL目录: ${SQL_DIR}"
-    echo "迁移目录: ${MIGRATIONS_DIR}"
-    echo ""
-    
-    # 列出迁移脚本模式
-    if [ "$LIST_MIGRATIONS" = true ]; then
-        list_migrations
-        exit 0
-    fi
-    
-    # 只删除模式
-    if [ "$DROP_ONLY" = true ]; then
-        drop_all_tables
-        log_title "删除完成"
-        log_info "所有表已删除,数据库已清空"
-        exit 0
-    fi
-    
-    # 只初始化模式
-    if [ "$INIT_ONLY" = true ]; then
-        if [ "$SIMPLE_MODE" = true ]; then
-            init_simple
-        else
-            init_all_tables
-        fi
-        if [ "$CREATE_TEST_USER" = true ]; then
-            create_test_user
-        fi
-        verify_tables
-        log_title "初始化完成"
-        log_info "所有表已创建并验证"
-        exit 0
-    fi
-    
-    # 完整重建模式
+CREATE_TEST_USER=true
+DROP_ONLY=false
+INIT_ONLY=false
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        -h|--help) show_help; exit 0 ;;
+        -f|--force) FORCE=yes; shift ;;
+        --no-test-user) CREATE_TEST_USER=false; shift ;;
+        --drop-only) DROP_ONLY=true; shift ;;
+        --init-only) INIT_ONLY=true; shift ;;
+        *) log_error "未知选项: $1"; show_help; exit 1 ;;
+    esac
+done
+
+log_title "数据库重建"
+echo "数据库: ${DB_NAME} @ ${DB_HOST}:${DB_PORT}"
+echo "初始化脚本: ${INIT_SQL}"
+echo ""
+
+if [ "$DROP_ONLY" = true ]; then
     drop_all_tables
-    
-    if [ "$SIMPLE_MODE" = true ]; then
-        init_simple
-    else
-        init_all_tables
-    fi
-    
-    if [ "$CREATE_TEST_USER" = true ]; then
-        create_test_user
-    fi
-    
-    verify_tables
-    
-    log_title "数据库重建完成"
-    log_info "所有表已创建并验证"
-}
-
-main "$@"
+    exit 0
+fi
+
+if [ "$INIT_ONLY" = true ]; then
+    init_database
+    [ "$CREATE_TEST_USER" = true ] && create_test_user
+    log_info "初始化完成"
+    exit 0
+fi
+
+drop_all_tables
+init_database
+[ "$CREATE_TEST_USER" = true ] && create_test_user
+log_info "重建完成"

+ 3 - 217
backend/sql/rebuild_database.sh

@@ -1,220 +1,6 @@
 #!/bin/bash
-
-# ============================================
-# 数据库重建脚本
-# 删除所有表并重新初始化
-# ============================================
-
+# 数据库重建:使用 database/init.sql 单文件初始化
 set -e
-
-# 颜色定义
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m'
-
-# 数据库配置(根据实际情况修改)
-DB_HOST=${DB_HOST:-localhost}
-DB_PORT=${DB_PORT:-5432}
-DB_NAME=${DB_NAME:-lingyue_zhibao}
-DB_USER=${DB_USER:-postgres}
-
-log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
-log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
-log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
-log_title() { echo -e "\n${BLUE}========== $1 ==========${NC}\n"; }
-
-# 获取脚本所在目录
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SQL_DIR="${SCRIPT_DIR}"
-
-# 检查 SQL 文件是否存在
-check_sql_files() {
-    log_title "检查 SQL 文件"
-    
-    local missing=false
-    
-    for file in init.sql supplement_tables.sql rag_tables.sql; do
-        if [ -f "${SQL_DIR}/${file}" ]; then
-            log_info "找到: ${file}"
-        else
-            log_error "缺失: ${file}"
-            missing=true
-        fi
-    done
-    
-    if [ "$missing" = true ]; then
-        log_error "请确保在 backend/sql 目录下运行此脚本"
-        exit 1
-    fi
-}
-
-# 删除所有表
-drop_all_tables() {
-    log_title "删除所有表"
-    
-    log_warn "这将删除数据库中的所有表和数据!"
-    read -p "是否继续?(yes/no): " confirm
-    
-    if [ "$confirm" != "yes" ]; then
-        log_info "操作已取消"
-        exit 0
-    fi
-    
-    log_info "开始删除表..."
-    
-    # 删除所有表的 SQL
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
--- 禁用外键约束检查
-SET session_replication_role = 'replica';
-
--- 删除所有表
-DO \$\$
-DECLARE
-    r RECORD;
-BEGIN
-    FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
-    LOOP
-        EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
-        RAISE NOTICE 'Dropped table: %', r.tablename;
-    END LOOP;
-END
-\$\$;
-
--- 恢复外键约束检查
-SET session_replication_role = 'origin';
-
--- 显示剩余的表
-SELECT tablename FROM pg_tables WHERE schemaname = 'public';
-EOF
-    
-    log_info "所有表已删除"
-}
-
-# 初始化基础表
-init_base_tables() {
-    log_title "初始化基础表 (init.sql)"
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "${SQL_DIR}/init.sql"
-    
-    if [ $? -eq 0 ]; then
-        log_info "基础表初始化成功"
-    else
-        log_error "基础表初始化失败"
-        exit 1
-    fi
-}
-
-# 初始化补充表
-init_supplement_tables() {
-    log_title "初始化补充表 (supplement_tables.sql)"
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "${SQL_DIR}/supplement_tables.sql"
-    
-    if [ $? -eq 0 ]; then
-        log_info "补充表初始化成功"
-    else
-        log_error "补充表初始化失败"
-        exit 1
-    fi
-}
-
-# 初始化 RAG 表
-init_rag_tables() {
-    log_title "初始化 RAG 表 (rag_tables.sql)"
-    
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} -f "${SQL_DIR}/rag_tables.sql" 2>&1
-    
-    if [ $? -eq 0 ]; then
-        log_info "RAG 表初始化成功"
-    else
-        log_warn "RAG 表初始化可能失败(如果未安装 pgvector 扩展是正常的)"
-    fi
-}
-
-# 验证表创建
-verify_tables() {
-    log_title "验证表创建"
-    
-    log_info "查询所有表..."
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
-SELECT 
-    schemaname,
-    tablename,
-    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
-FROM pg_tables 
-WHERE schemaname = 'public'
-ORDER BY tablename;
-EOF
-    
-    log_info "表统计..."
-    PGPASSWORD=${DB_PASSWORD} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME} <<EOF
-SELECT COUNT(*) as table_count FROM pg_tables WHERE schemaname = 'public';
-EOF
-}
-
-# 显示帮助
-show_help() {
-    cat <<EOF
-数据库重建脚本
-
-用法: ./rebuild_database.sh [选项]
-
-环境变量:
-    DB_HOST         数据库主机 (默认: localhost)
-    DB_PORT         数据库端口 (默认: 5432)
-    DB_NAME         数据库名称 (默认: lingyue_zhibao)
-    DB_USER         数据库用户 (默认: postgres)
-    DB_PASSWORD     数据库密码 (可选,如果未设置将提示输入)
-
-示例:
-    # 使用默认配置
-    ./rebuild_database.sh
-
-    # 指定数据库配置
-    DB_HOST=localhost DB_USER=postgres DB_PASSWORD=123456 ./rebuild_database.sh
-
-    # 仅初始化新表(不删除)
-    ./init_supplement_only.sh
-
-注意:
-    - 此脚本会删除所有现有表和数据
-    - 确保已备份重要数据
-    - 需要数据库管理员权限
-
-执行步骤:
-    1. 删除所有表
-    2. 执行 init.sql(基础表)
-    3. 执行 supplement_tables.sql(补充表)
-    4. 执行 rag_tables.sql(RAG表)
-    5. 验证表创建
-
-EOF
-}
-
-# 主函数
-main() {
-    if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
-        show_help
-        exit 0
-    fi
-    
-    log_title "数据库重建脚本"
-    echo "数据库: ${DB_NAME}"
-    echo "主机: ${DB_HOST}:${DB_PORT}"
-    echo "用户: ${DB_USER}"
-    echo ""
-    
-    check_sql_files
-    drop_all_tables
-    init_base_tables
-    init_supplement_tables
-    init_rag_tables
-    verify_tables
-    
-    log_title "数据库重建完成"
-    log_info "所有表已创建并验证"
-}
-
-main "$@"
+PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+"${SCRIPT_DIR}/rebuild_all.sh" "$@"

+ 0 - 111
backend/sql/supplement_tables.sql

@@ -1,111 +0,0 @@
--- 灵越智报 v2.0 补充表结构
--- PostgreSQL 15+
--- 根据产品设计方案补充的表结构
--- 注意: ID 使用 VARCHAR(36) 以兼容 MyBatis-Plus 的 ASSIGN_UUID 策略
--- 注意: graph_nodes 和 graph_relations 表已在 graph_tables.sql 中定义,此处不再重复
-
--- ============================================
--- 1. 规则表(rules)
--- ============================================
-CREATE TABLE IF NOT EXISTS rules (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    name VARCHAR(255) NOT NULL,
-    description TEXT,
-    entry_node_id VARCHAR(36) REFERENCES graph_nodes(id) ON DELETE SET NULL,
-    exit_node_id VARCHAR(36) REFERENCES graph_nodes(id) ON DELETE SET NULL,
-    rule_chain JSONB NOT NULL DEFAULT '[]', -- 规则链(节点ID序列)
-    status VARCHAR(20) DEFAULT 'active', -- active/inactive
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_rules_user_id ON rules(user_id);
-CREATE INDEX IF NOT EXISTS idx_rules_entry_node ON rules(entry_node_id);
-CREATE INDEX IF NOT EXISTS idx_rules_exit_node ON rules(exit_node_id);
-CREATE INDEX IF NOT EXISTS idx_rules_status ON rules(status);
-
--- ============================================
--- 4. 数据源表(data_sources)
--- ============================================
-CREATE TABLE IF NOT EXISTS data_sources (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
-    name VARCHAR(255) NOT NULL,
-    type VARCHAR(50) NOT NULL, -- table/text/image
-    source_type VARCHAR(50) NOT NULL DEFAULT 'manual', -- file/manual/rule_result
-    node_ids JSONB DEFAULT '{"refs": []}', -- 节点引用列表(支持 graph_node 和 document_element)
-    config JSONB DEFAULT '{}', -- 数据源配置
-    metadata JSONB DEFAULT '{}',
-    value_type VARCHAR(20) DEFAULT 'text', -- 值类型: text/image/table/mixed
-    aggregate_type VARCHAR(20) DEFAULT 'first', -- 聚合方式: first/last/concat/sum/avg/list
-    separator VARCHAR(50) DEFAULT '', -- 聚合分隔符(concat时使用)
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_data_sources_user_id ON data_sources(user_id);
-CREATE INDEX IF NOT EXISTS idx_data_sources_document_id ON data_sources(document_id);
-CREATE INDEX IF NOT EXISTS idx_data_sources_type ON data_sources(type);
-
--- ============================================
--- 5. 模板表(templates)
--- ============================================
--- 注意: templates 表已迁移至 template_tables.sql(v2.0 版本)
--- 新版包含: templates, source_files, variables, generations
--- 请勿在此重复定义
-
--- ============================================
--- 6. 文本存储路径表(text_storage)
--- ============================================
-CREATE TABLE IF NOT EXISTS text_storage (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    file_path VARCHAR(500) NOT NULL, -- TXT文件路径
-    file_size BIGINT,
-    checksum VARCHAR(64), -- 文件校验和
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_text_storage_document_id ON text_storage(document_id);
-CREATE UNIQUE INDEX IF NOT EXISTS idx_text_storage_document_unique ON text_storage(document_id);
-
--- ============================================
--- 注意: vector_embeddings 表已在 rag_tables_compatible.sql 中定义
--- 请勿在此重复定义,以避免表结构冲突
--- ============================================
-
--- ============================================
--- 创建更新时间触发器
--- ============================================
--- 注意: graph_nodes 和 graph_relations 的触发器已在 graph_tables.sql 中定义
--- 注意: vector_embeddings 的触发器已在 rag_tables_compatible.sql 中定义
-
-DROP TRIGGER IF EXISTS update_rules_update_time ON rules;
-CREATE TRIGGER update_rules_update_time BEFORE UPDATE ON rules
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_data_sources_update_time ON data_sources;
-CREATE TRIGGER update_data_sources_update_time BEFORE UPDATE ON data_sources
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
--- templates 触发器已迁移至 template_tables.sql
-
-DROP TRIGGER IF EXISTS update_text_storage_update_time ON text_storage;
-CREATE TRIGGER update_text_storage_update_time BEFORE UPDATE ON text_storage
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();

+ 0 - 200
backend/sql/template_tables.sql

@@ -1,200 +0,0 @@
--- =====================================================
--- 报告模板系统表结构 v2.0
--- 「示例文档驱动」的模板生成系统
--- PostgreSQL 15+
--- 
--- 表列表:
---   1. templates - 报告模板
---   2. source_files - 来源文件定义
---   3. variables - 模板变量
---   4. generations - 生成任务
--- 
--- 创建时间: 2026-01-23
--- =====================================================
-
--- ============================================
--- 1. 报告模板表(templates)
--- ============================================
--- 注意: 如果 supplement_tables.sql 中已定义旧版 templates 表,
--- 需要先删除或重命名
-DROP TABLE IF EXISTS templates CASCADE;
-
-CREATE TABLE templates (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    name VARCHAR(255) NOT NULL,
-    description TEXT,
-    base_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
-    status VARCHAR(32) DEFAULT 'draft',
-    config JSONB DEFAULT '{}',
-    is_public BOOLEAN DEFAULT FALSE,
-    use_count INT DEFAULT 0,
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100)
-);
-
-CREATE INDEX idx_templates_user_id ON templates(user_id);
-CREATE INDEX idx_templates_status ON templates(status);
-CREATE INDEX idx_templates_is_public ON templates(is_public);
-CREATE INDEX idx_templates_base_document ON templates(base_document_id);
-
-COMMENT ON TABLE templates IS '报告模板';
-COMMENT ON COLUMN templates.base_document_id IS '示例报告文档ID(可选,空白模板可为空)';
-COMMENT ON COLUMN templates.status IS 'draft-草稿, published-已发布, archived-已归档';
-COMMENT ON COLUMN templates.config IS '模板配置,如默认AI模型等';
-COMMENT ON COLUMN templates.is_public IS '是否公开给其他用户使用';
-COMMENT ON COLUMN templates.use_count IS '被使用生成报告的次数';
-
--- ============================================
--- 2. 来源文件定义表(source_files)
--- ============================================
-CREATE TABLE IF NOT EXISTS source_files (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    alias VARCHAR(100) NOT NULL,
-    description TEXT,
-    file_types JSONB DEFAULT '["pdf", "docx"]',
-    required BOOLEAN DEFAULT TRUE,
-    example_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
-    display_order INT DEFAULT 0,
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uk_source_files_alias UNIQUE (template_id, alias)
-);
-
-CREATE INDEX idx_source_files_template ON source_files(template_id);
-
-COMMENT ON TABLE source_files IS '来源文件定义';
-COMMENT ON COLUMN source_files.alias IS '用户自定义的别名,用于引用,如"可研批复"';
-COMMENT ON COLUMN source_files.file_types IS '允许上传的文件类型列表';
-COMMENT ON COLUMN source_files.required IS '是否为必须提供的文件';
-COMMENT ON COLUMN source_files.example_document_id IS '创建模板时使用的示例文件,用于预览';
-
--- ============================================
--- 3. 模板变量表(variables)
--- ============================================
-CREATE TABLE IF NOT EXISTS variables (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    
-    -- 变量标识
-    name VARCHAR(100) NOT NULL,
-    display_name VARCHAR(200) NOT NULL,
-    variable_group VARCHAR(100),
-    
-    -- 在示例报告中的位置
-    location JSONB NOT NULL,
-    
-    -- 示例值
-    example_value TEXT,
-    value_type VARCHAR(32) DEFAULT 'text',
-    
-    -- 数据来源
-    source_file_alias VARCHAR(100),
-    source_type VARCHAR(32) NOT NULL,
-    source_config JSONB,
-    
-    -- 提取方式
-    extract_type VARCHAR(32),
-    extract_config JSONB,
-    
-    display_order INT DEFAULT 0,
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uk_variables_name UNIQUE (template_id, name)
-);
-
-CREATE INDEX idx_variables_template ON variables(template_id);
-CREATE INDEX idx_variables_source_alias ON variables(source_file_alias);
-CREATE INDEX idx_variables_source_type ON variables(source_type);
-
-COMMENT ON TABLE variables IS '模板变量';
-COMMENT ON COLUMN variables.name IS '变量名,模板内唯一,用于程序引用';
-COMMENT ON COLUMN variables.display_name IS '显示名称,用于用户界面';
-COMMENT ON COLUMN variables.location IS '变量在文档中的位置,包含 element_id、偏移量等';
-COMMENT ON COLUMN variables.value_type IS 'text-文本, date-日期, number-数字, table-表格';
-COMMENT ON COLUMN variables.source_type IS 'document-从来源文件提取, manual-手动输入, reference-引用其他变量, fixed-固定值';
-COMMENT ON COLUMN variables.extract_type IS 'direct-直接提取, ai_extract-AI字段提取, ai_summarize-AI总结';
-
--- ============================================
--- 4. 生成任务表(generations)
--- ============================================
-CREATE TABLE IF NOT EXISTS generations (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE RESTRICT,
-    user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    
-    name VARCHAR(255),
-    
-    -- 来源文件映射:别名 → 文档ID
-    source_file_map JSONB NOT NULL,
-    
-    -- 变量提取结果
-    variable_values JSONB,
-    
-    -- 生成的文档
-    output_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
-    output_file_path VARCHAR(500),
-    
-    status VARCHAR(32) DEFAULT 'pending',
-    error_message TEXT,
-    progress INT DEFAULT 0,
-    
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    completed_at TIMESTAMP
-);
-
-CREATE INDEX idx_generations_template ON generations(template_id);
-CREATE INDEX idx_generations_user ON generations(user_id);
-CREATE INDEX idx_generations_status ON generations(status);
-CREATE INDEX idx_generations_create_time ON generations(create_time DESC);
-
-COMMENT ON TABLE generations IS '报告生成任务';
-COMMENT ON COLUMN generations.source_file_map IS '来源文件映射,如 {"可研批复": "doc_123"}';
-COMMENT ON COLUMN generations.variable_values IS '变量提取结果,包含值、置信度、状态等';
-COMMENT ON COLUMN generations.status IS 'pending-待执行, extracting-提取中, review-待确认, completed-已完成, error-错误';
-COMMENT ON COLUMN generations.progress IS '进度百分比 0-100';
-
--- ============================================
--- 5. 更新时间触发器
--- ============================================
--- 确保 update_update_time_column 函数存在(在 init.sql 中定义)
--- 如果不存在,创建一个
-CREATE OR REPLACE FUNCTION update_update_time_column()
-RETURNS TRIGGER AS $$
-BEGIN
-    NEW.update_time = CURRENT_TIMESTAMP;
-    RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-DROP TRIGGER IF EXISTS trigger_templates_update_time ON templates;
-CREATE TRIGGER trigger_templates_update_time
-    BEFORE UPDATE ON templates
-    FOR EACH ROW
-    EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS trigger_variables_update_time ON variables;
-CREATE TRIGGER trigger_variables_update_time
-    BEFORE UPDATE ON variables
-    FOR EACH ROW
-    EXECUTE FUNCTION update_update_time_column();
-
--- ============================================
--- 6. 旧版 extract 表(v1.x 兼容)
--- ============================================
--- 以下表将在未来版本中删除,仅保留兼容性
--- extract_projects, extract_source_documents, extract_rules, 
--- extract_results, extract_rule_templates
-
--- 如需要删除旧表,取消以下注释:
--- DROP TABLE IF EXISTS extract_rule_templates CASCADE;
--- DROP TABLE IF EXISTS extract_results CASCADE;
--- DROP TABLE IF EXISTS extract_rules CASCADE;
--- DROP TABLE IF EXISTS extract_source_documents CASCADE;
--- DROP TABLE IF EXISTS extract_projects CASCADE;

+ 0 - 27
backend/sql/text_storage_only.sql

@@ -1,27 +0,0 @@
--- 灵越智报 v2.0 补充表结构(兼容 VARCHAR(36) ID 版本)
--- PostgreSQL 15+
--- 兼容 init.sql 中使用的 VARCHAR(36) ID 类型
-
--- ============================================
--- 6. 文本存储路径表(text_storage)
--- 仅创建核心需要的表
--- ============================================
-CREATE TABLE IF NOT EXISTS text_storage (
-    id VARCHAR(36) PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', ''),
-    document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    file_path VARCHAR(500) NOT NULL,
-    file_size BIGINT,
-    checksum VARCHAR(64),
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_text_storage_document_id ON text_storage(document_id);
-CREATE UNIQUE INDEX IF NOT EXISTS idx_text_storage_document_unique ON text_storage(document_id);
-
--- 显示创建结果
-SELECT 'text_storage 表创建成功,字段与 SimpleModel 兼容' AS result;

+ 86 - 344
backend/sql/all_tables.sql → database/init.sql

@@ -1,61 +1,22 @@
 -- =====================================================
--- 灵越智报 v2.0 数据库完整表结构
+-- 灵越智报 v2.0 数据库初始化脚本(单文件)
 -- PostgreSQL 15+
--- 
--- 生成时间: 2026-01-23
--- 
--- 包含所有表(按模块分组):
---   一、基础模块
---     1. users - 用户表
---     2. documents - 文档表
---     3. elements - 要素表
---     4. annotations - 批注表
---     5. graphs - 关系网络表
---     6. parse_tasks - 解析任务表
---     7. sessions - 会话表
---   
---   二、图谱模块
---     8. graph_nodes - 图节点表
---     9. graph_relations - 图关系表
---   
---   三、补充模块
---     10. rules - 规则表
---     11. data_sources - 数据源表
---     12. text_storage - 文本存储表
---   
---   四、RAG 模块
---     13. text_chunks - 文本分块表
---     14. vector_embeddings - 向量嵌入表
---   
---   五、文档结构化模块
---     15. document_blocks - 文档块表
---     16. document_entities - 文档实体表
---     17. document_elements - 文档元素表
---   
---   六、模板系统模块(v2.0)
---     18. templates - 报告模板表
---     19. source_files - 来源文件定义表
---     20. variables - 模板变量表
---     21. generations - 生成任务表
+-- 包含所有表结构,不含 graph_nodes / graph_relations(已移除)
 -- =====================================================
 
--- 启用扩展
 CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-CREATE EXTENSION IF NOT EXISTS vector;  -- pgvector 用于向量检索
+CREATE EXTENSION IF NOT EXISTS vector;
 
--- ============================================
--- 一、基础模块
--- ============================================
+-- ============================================ 一、基础模块 ============================================
 
--- 1. 用户表 (users)
 CREATE TABLE IF NOT EXISTS users (
     id VARCHAR(36) PRIMARY KEY,
     username VARCHAR(50) UNIQUE NOT NULL,
     email VARCHAR(100) UNIQUE NOT NULL,
     password_hash VARCHAR(255) NOT NULL,
     avatar_url VARCHAR(500),
-    role VARCHAR(20) NOT NULL DEFAULT 'user',  -- admin/user/guest
-    preferences TEXT DEFAULT '{}',              -- JSON字符串格式存储偏好设置
+    role VARCHAR(20) NOT NULL DEFAULT 'user',
+    preferences TEXT DEFAULT '{}',
     last_login_at TIMESTAMP,
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
@@ -64,31 +25,25 @@ CREATE TABLE IF NOT EXISTS users (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
 CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
 
-COMMENT ON TABLE users IS '用户表';
-COMMENT ON COLUMN users.role IS '角色: admin-管理员, user-普通用户, guest-访客';
-
--- 2. 文档表 (documents)
 CREATE TABLE IF NOT EXISTS documents (
     id VARCHAR(36) PRIMARY KEY,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     name VARCHAR(255) NOT NULL,
-    type VARCHAR(20) NOT NULL,                   -- pdf/word/image/markdown/other
-    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/uploading/parsing/completed/failed
+    type VARCHAR(20) NOT NULL,
+    status VARCHAR(20) NOT NULL DEFAULT 'pending',
     file_size BIGINT,
     file_url VARCHAR(500),
     thumbnail_url VARCHAR(500),
     parsed_text TEXT,
-    parse_status VARCHAR(20),                    -- pending/parsing/completed/failed
-    parse_progress INTEGER DEFAULT 0,            -- 0-100
+    parse_status VARCHAR(20),
+    parse_progress INTEGER DEFAULT 0,
     parse_error TEXT,
     parse_started_at TIMESTAMP,
     parse_completed_at TIMESTAMP,
-    metadata JSONB DEFAULT '{}',                 -- pageCount, ocrConfidence, layoutStructure等
-    -- 结构化解析增强字段
+    metadata JSONB DEFAULT '{}',
     structured_status VARCHAR(20) DEFAULT 'pending',
     image_count INT DEFAULT 0,
     table_count INT DEFAULT 0,
@@ -100,30 +55,22 @@ CREATE TABLE IF NOT EXISTS documents (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_documents_user_id ON documents(user_id);
 CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status);
 CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(type);
 CREATE INDEX IF NOT EXISTS idx_documents_created_at ON documents(create_time DESC);
 CREATE INDEX IF NOT EXISTS idx_documents_metadata ON documents USING GIN(metadata);
 
-COMMENT ON TABLE documents IS '文档表';
-COMMENT ON COLUMN documents.type IS '文档类型: pdf/word/image/markdown/other';
-COMMENT ON COLUMN documents.status IS '状态: pending/uploading/parsing/completed/failed';
-COMMENT ON COLUMN documents.structured_status IS '结构化解析状态: pending/completed/failed';
-
--- 3. 要素表 (elements)
 CREATE TABLE IF NOT EXISTS elements (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    type VARCHAR(20) NOT NULL,                   -- amount/company/person/location/date/other
+    type VARCHAR(20) NOT NULL,
     label VARCHAR(100) NOT NULL,
     value TEXT NOT NULL,
-    position JSONB,                              -- {page, x, y, width, height}
-    confidence DECIMAL(3,2),                     -- 0.00-1.00
-    extraction_method VARCHAR(20),               -- ai/regex/rule/manual
-    graph_node_id VARCHAR(36),                   -- 关联的图节点ID
+    position JSONB,
+    confidence DECIMAL(3,2),
+    extraction_method VARCHAR(20),
     metadata JSONB DEFAULT '{}',
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
@@ -132,28 +79,22 @@ CREATE TABLE IF NOT EXISTS elements (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_elements_document_id ON elements(document_id);
 CREATE INDEX IF NOT EXISTS idx_elements_user_id ON elements(user_id);
 CREATE INDEX IF NOT EXISTS idx_elements_type ON elements(type);
-CREATE INDEX IF NOT EXISTS idx_elements_graph_node_id ON elements(graph_node_id);
 CREATE INDEX IF NOT EXISTS idx_elements_position ON elements USING GIN(position);
 
-COMMENT ON TABLE elements IS '要素表 - 文档中提取的结构化要素';
-COMMENT ON COLUMN elements.type IS '要素类型: amount/company/person/location/date/other';
-
--- 4. 批注表 (annotations)
 CREATE TABLE IF NOT EXISTS annotations (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     text TEXT NOT NULL,
-    position JSONB NOT NULL,                     -- {page, start: {x, y}, end: {x, y}}
-    type VARCHAR(20) NOT NULL,                   -- highlight/strikethrough/suggestion
+    position JSONB NOT NULL,
+    type VARCHAR(20) NOT NULL,
     suggestion TEXT,
     ai_generated BOOLEAN DEFAULT FALSE,
     confidence DECIMAL(3,2),
-    status VARCHAR(20) DEFAULT 'pending',        -- pending/accepted/rejected
+    status VARCHAR(20) DEFAULT 'pending',
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
     create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -161,25 +102,20 @@ CREATE TABLE IF NOT EXISTS annotations (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_annotations_document_id ON annotations(document_id);
 CREATE INDEX IF NOT EXISTS idx_annotations_user_id ON annotations(user_id);
 CREATE INDEX IF NOT EXISTS idx_annotations_type ON annotations(type);
 CREATE INDEX IF NOT EXISTS idx_annotations_status ON annotations(status);
 
-COMMENT ON TABLE annotations IS '批注表';
-COMMENT ON COLUMN annotations.type IS '批注类型: highlight/strikethrough/suggestion';
-
--- 5. 关系网络表 (graphs)
 CREATE TABLE IF NOT EXISTS graphs (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     name VARCHAR(255) NOT NULL,
-    nodes JSONB NOT NULL DEFAULT '[]',           -- GraphNode数组
-    edges JSONB NOT NULL DEFAULT '[]',           -- GraphEdge数组
+    nodes JSONB NOT NULL DEFAULT '[]',
+    edges JSONB NOT NULL DEFAULT '[]',
     calculation_result JSONB,
-    calculation_status VARCHAR(20),              -- pending/completed/failed
+    calculation_status VARCHAR(20),
     metadata JSONB DEFAULT '{}',
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
@@ -188,26 +124,21 @@ CREATE TABLE IF NOT EXISTS graphs (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_graphs_document_id ON graphs(document_id);
 CREATE INDEX IF NOT EXISTS idx_graphs_user_id ON graphs(user_id);
 CREATE INDEX IF NOT EXISTS idx_graphs_nodes ON graphs USING GIN(nodes);
 CREATE INDEX IF NOT EXISTS idx_graphs_edges ON graphs USING GIN(edges);
 
-COMMENT ON TABLE graphs IS '关系网络表';
-
--- 6. 解析任务表 (parse_tasks)
 CREATE TABLE IF NOT EXISTS parse_tasks (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/processing/completed/failed
-    progress INTEGER DEFAULT 0,                  -- 0-100
+    status VARCHAR(20) NOT NULL DEFAULT 'pending',
+    progress INTEGER DEFAULT 0,
     current_step VARCHAR(100),
     error_message TEXT,
-    options JSONB DEFAULT '{}',                  -- 解析选项
+    options JSONB DEFAULT '{}',
     started_at TIMESTAMP,
     completed_at TIMESTAMP,
-    -- 多阶段进度跟踪
     parse_status VARCHAR(20) DEFAULT 'pending',
     parse_progress INTEGER DEFAULT 0,
     rag_status VARCHAR(20) DEFAULT 'pending',
@@ -222,6 +153,7 @@ CREATE TABLE IF NOT EXISTS parse_tasks (
     ner_task_id VARCHAR(64),
     ner_entity_count INTEGER,
     ner_relation_count INTEGER,
+    ner_message VARCHAR(255),
     graph_status VARCHAR(20) DEFAULT 'pending',
     graph_progress INTEGER DEFAULT 0,
     create_by VARCHAR(36),
@@ -231,19 +163,9 @@ CREATE TABLE IF NOT EXISTS parse_tasks (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_parse_tasks_document_id ON parse_tasks(document_id);
 CREATE INDEX IF NOT EXISTS idx_parse_tasks_status ON parse_tasks(status);
-CREATE INDEX IF NOT EXISTS idx_parse_tasks_ner_task_id ON parse_tasks(ner_task_id);
-
-COMMENT ON TABLE parse_tasks IS '解析任务表';
-COMMENT ON COLUMN parse_tasks.parse_status IS '解析阶段状态';
-COMMENT ON COLUMN parse_tasks.rag_status IS 'RAG向量化阶段状态';
-COMMENT ON COLUMN parse_tasks.structured_status IS '结构化解析阶段状态';
-COMMENT ON COLUMN parse_tasks.ner_status IS 'NER实体提取阶段状态';
-COMMENT ON COLUMN parse_tasks.graph_status IS '图构建阶段状态';
 
--- 7. 会话表 (sessions)
 CREATE TABLE IF NOT EXISTS sessions (
     id VARCHAR(36) PRIMARY KEY,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -255,78 +177,21 @@ CREATE TABLE IF NOT EXISTS sessions (
     create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
     last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
 CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
 CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
 
-COMMENT ON TABLE sessions IS '会话表';
-
--- ============================================
--- 二、图谱模块
--- ============================================
-
--- 8. 图节点表 (graph_nodes)
-CREATE TABLE IF NOT EXISTS graph_nodes (
-    id VARCHAR(36) PRIMARY KEY,
-    document_id VARCHAR(36) NOT NULL,
-    user_id VARCHAR(36),                         -- 可为空,自动提取时可能没有用户上下文
-    name VARCHAR(255) NOT NULL,
-    type VARCHAR(50) NOT NULL,                   -- text/table/image/number/date/ORG/PERSON/LOC/TIME/DEVICE/PROJECT/METHOD等
-    value TEXT,
-    position JSONB,                              -- {charStart, charEnd, line}
-    parent_id VARCHAR(36),
-    level INTEGER DEFAULT 0,
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_document_id ON graph_nodes(document_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_user_id ON graph_nodes(user_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_type ON graph_nodes(type);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_parent_id ON graph_nodes(parent_id);
-CREATE INDEX IF NOT EXISTS idx_graph_nodes_position ON graph_nodes USING GIN(position);
-
-COMMENT ON TABLE graph_nodes IS '图节点表 - NER实体识别结果';
-COMMENT ON COLUMN graph_nodes.type IS '节点类型: ORG/PERSON/LOC/TIME/DEVICE/PROJECT/METHOD等';
-
--- 9. 图关系表 (graph_relations)
-CREATE TABLE IF NOT EXISTS graph_relations (
-    id VARCHAR(36) PRIMARY KEY,
-    from_node_id VARCHAR(36) NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
-    to_node_id VARCHAR(36) NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
-    relation_type VARCHAR(50) NOT NULL,          -- BELONGS_TO/USES/LOCATED_IN/EXECUTES/MONITORS等
-    action_type VARCHAR(50),
-    action_config JSONB,
-    order_index INTEGER DEFAULT 0,
-    condition_expr TEXT,
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
+-- ============================================ 二、补充模块 ============================================
 
-CREATE INDEX IF NOT EXISTS idx_graph_relations_from_node ON graph_relations(from_node_id);
-CREATE INDEX IF NOT EXISTS idx_graph_relations_to_node ON graph_relations(to_node_id);
-CREATE INDEX IF NOT EXISTS idx_graph_relations_type ON graph_relations(relation_type);
-
-COMMENT ON TABLE graph_relations IS '图关系表 - 实体之间的关系';
-COMMENT ON COLUMN graph_relations.relation_type IS '关系类型: BELONGS_TO/USES/LOCATED_IN/EXECUTES/MONITORS等';
-
--- ============================================
--- 三、补充模块
--- ============================================
-
--- 10. 规则表 (rules)
 CREATE TABLE IF NOT EXISTS rules (
     id VARCHAR(36) PRIMARY KEY,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     name VARCHAR(255) NOT NULL,
     description TEXT,
-    entry_node_id VARCHAR(36) REFERENCES graph_nodes(id) ON DELETE SET NULL,
-    exit_node_id VARCHAR(36) REFERENCES graph_nodes(id) ON DELETE SET NULL,
-    rule_chain JSONB NOT NULL DEFAULT '[]',      -- 规则链(节点ID序列)
-    status VARCHAR(20) DEFAULT 'active',         -- active/inactive
+    entry_node_id VARCHAR(36),
+    exit_node_id VARCHAR(36),
+    rule_chain JSONB NOT NULL DEFAULT '[]',
+    status VARCHAR(20) DEFAULT 'active',
     metadata JSONB DEFAULT '{}',
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
@@ -335,28 +200,22 @@ CREATE TABLE IF NOT EXISTS rules (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_rules_user_id ON rules(user_id);
-CREATE INDEX IF NOT EXISTS idx_rules_entry_node ON rules(entry_node_id);
-CREATE INDEX IF NOT EXISTS idx_rules_exit_node ON rules(exit_node_id);
 CREATE INDEX IF NOT EXISTS idx_rules_status ON rules(status);
 
-COMMENT ON TABLE rules IS '规则表';
-
--- 11. 数据源表 (data_sources)
 CREATE TABLE IF NOT EXISTS data_sources (
     id VARCHAR(36) PRIMARY KEY,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
     name VARCHAR(255) NOT NULL,
-    type VARCHAR(50) NOT NULL,                   -- table/text/image
-    source_type VARCHAR(50) NOT NULL DEFAULT 'manual', -- file/manual/rule_result
-    node_ids JSONB DEFAULT '{"refs": []}',       -- 节点引用列表
-    config JSONB DEFAULT '{}',                   -- 数据源配置
+    type VARCHAR(50) NOT NULL,
+    source_type VARCHAR(50) NOT NULL DEFAULT 'manual',
+    node_ids JSONB DEFAULT '{"refs": []}',
+    config JSONB DEFAULT '{}',
     metadata JSONB DEFAULT '{}',
-    value_type VARCHAR(20) DEFAULT 'text',       -- text/image/table/mixed
-    aggregate_type VARCHAR(20) DEFAULT 'first',  -- first/last/concat/sum/avg/list
-    separator VARCHAR(50) DEFAULT '',            -- 聚合分隔符
+    value_type VARCHAR(20) DEFAULT 'text',
+    aggregate_type VARCHAR(20) DEFAULT 'first',
+    separator VARCHAR(50) DEFAULT '',
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
     create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -364,22 +223,16 @@ CREATE TABLE IF NOT EXISTS data_sources (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_data_sources_user_id ON data_sources(user_id);
 CREATE INDEX IF NOT EXISTS idx_data_sources_document_id ON data_sources(document_id);
 CREATE INDEX IF NOT EXISTS idx_data_sources_type ON data_sources(type);
 
-COMMENT ON TABLE data_sources IS '数据源表';
-COMMENT ON COLUMN data_sources.value_type IS '值类型: text/image/table/mixed';
-COMMENT ON COLUMN data_sources.aggregate_type IS '聚合方式: first/last/concat/sum/avg/list';
-
--- 12. 文本存储表 (text_storage)
 CREATE TABLE IF NOT EXISTS text_storage (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
-    file_path VARCHAR(500) NOT NULL,             -- TXT文件路径
+    file_path VARCHAR(500) NOT NULL,
     file_size BIGINT,
-    checksum VARCHAR(64),                        -- 文件校验和
+    checksum VARCHAR(64),
     create_by VARCHAR(36),
     create_by_name VARCHAR(100),
     create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -387,17 +240,11 @@ CREATE TABLE IF NOT EXISTS text_storage (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_text_storage_document_id ON text_storage(document_id);
 CREATE UNIQUE INDEX IF NOT EXISTS idx_text_storage_document_unique ON text_storage(document_id);
 
-COMMENT ON TABLE text_storage IS '文本存储表 - 存储文档解析后的TXT文件路径';
+-- ============================================ 三、RAG 模块 ============================================
 
--- ============================================
--- 四、RAG 模块
--- ============================================
-
--- 13. 文本分块表 (text_chunks)
 CREATE TABLE IF NOT EXISTS text_chunks (
     id VARCHAR(36) PRIMARY KEY,
     document_id VARCHAR(36) NOT NULL,
@@ -413,42 +260,31 @@ CREATE TABLE IF NOT EXISTS text_chunks (
     update_by_name VARCHAR(100),
     update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_text_chunks_document_id ON text_chunks(document_id);
 CREATE INDEX IF NOT EXISTS idx_text_chunks_text_storage_id ON text_chunks(text_storage_id);
-CREATE INDEX IF NOT EXISTS idx_text_chunks_chunk_index ON text_chunks(document_id, chunk_index);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_text_chunks_doc_index ON text_chunks(document_id, chunk_index);
 
-COMMENT ON TABLE text_chunks IS '文本分块表 - RAG分块存储';
-
--- 14. 向量嵌入表 (vector_embeddings)
 CREATE TABLE IF NOT EXISTS vector_embeddings (
     id VARCHAR(36) PRIMARY KEY,
     chunk_id VARCHAR(36) NOT NULL REFERENCES text_chunks(id) ON DELETE CASCADE,
-    embedding vector(768),                       -- nomic-embed-text 维度为 768
+    embedding vector(768),
     model_name VARCHAR(100) DEFAULT 'nomic-embed-text',
     create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_vector_embeddings_chunk_id ON vector_embeddings(chunk_id);
 CREATE INDEX IF NOT EXISTS idx_vector_embeddings_model ON vector_embeddings(model_name);
 CREATE INDEX IF NOT EXISTS idx_vector_embeddings_hnsw ON vector_embeddings USING hnsw (embedding vector_cosine_ops);
 
-COMMENT ON TABLE vector_embeddings IS '向量嵌入表 - RAG向量检索';
-COMMENT ON COLUMN vector_embeddings.embedding IS '768维向量(nomic-embed-text)';
+-- ============================================ 四、文档结构化模块 ============================================
 
--- ============================================
--- 五、文档结构化模块
--- ============================================
-
--- 15. 文档块表 (document_blocks)
 CREATE TABLE IF NOT EXISTS document_blocks (
     id VARCHAR(64) PRIMARY KEY,
     document_id VARCHAR(64) NOT NULL,
     parent_id VARCHAR(64),
     children JSONB,
     block_index INTEGER NOT NULL,
-    block_type VARCHAR(32) NOT NULL,             -- heading1/heading2/paragraph/table/list/image/quote
-    elements JSONB,                              -- TextElement数组
+    block_type VARCHAR(32) NOT NULL,
+    elements JSONB,
     style JSONB,
     metadata JSONB,
     create_by VARCHAR(64),
@@ -458,23 +294,17 @@ CREATE TABLE IF NOT EXISTS document_blocks (
     update_by_name VARCHAR(128),
     update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_document_blocks_document_id ON document_blocks(document_id);
 CREATE INDEX IF NOT EXISTS idx_document_blocks_parent_id ON document_blocks(parent_id);
 CREATE INDEX IF NOT EXISTS idx_document_blocks_block_type ON document_blocks(block_type);
 CREATE INDEX IF NOT EXISTS idx_document_blocks_elements_gin ON document_blocks USING GIN (elements jsonb_path_ops);
 
-COMMENT ON TABLE document_blocks IS '文档块表 - 参考飞书Block设计';
-COMMENT ON COLUMN document_blocks.block_type IS '块类型: heading1/heading2/paragraph/table/list/image/quote';
-COMMENT ON COLUMN document_blocks.elements IS 'TextElement数组,实体作为元素嵌入';
-
--- 16. 文档实体表 (document_entities)
 CREATE TABLE IF NOT EXISTS document_entities (
     id VARCHAR(64) PRIMARY KEY,
     document_id VARCHAR(64) NOT NULL,
     block_id VARCHAR(64),
     name VARCHAR(512) NOT NULL,
-    entity_type VARCHAR(32) NOT NULL,            -- PERSON/ORG/LOC/DATE/NUMBER/MONEY/CONCEPT/DATA/DEVICE/TERM
+    entity_type VARCHAR(32) NOT NULL,
     value TEXT,
     block_char_start INTEGER,
     block_char_end INTEGER,
@@ -482,10 +312,9 @@ CREATE TABLE IF NOT EXISTS document_entities (
     global_char_end INTEGER,
     anchor_before VARCHAR(100),
     anchor_after VARCHAR(100),
-    source VARCHAR(16) DEFAULT 'auto',           -- auto/manual
+    source VARCHAR(16) DEFAULT 'auto',
     confidence DECIMAL(5,4),
     confirmed BOOLEAN DEFAULT FALSE,
-    graph_node_id VARCHAR(64),
     metadata JSONB,
     create_by VARCHAR(64),
     create_by_name VARCHAR(128),
@@ -494,39 +323,31 @@ CREATE TABLE IF NOT EXISTS document_entities (
     update_by_name VARCHAR(128),
     update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_document_entities_document_id ON document_entities(document_id);
 CREATE INDEX IF NOT EXISTS idx_document_entities_block_id ON document_entities(block_id);
 CREATE INDEX IF NOT EXISTS idx_document_entities_type ON document_entities(entity_type);
 CREATE INDEX IF NOT EXISTS idx_document_entities_name ON document_entities(name);
 CREATE INDEX IF NOT EXISTS idx_document_entities_global_char ON document_entities(document_id, global_char_start, global_char_end);
-CREATE INDEX IF NOT EXISTS idx_document_entities_graph_node ON document_entities(graph_node_id);
 
-COMMENT ON TABLE document_entities IS '文档实体表 - 文档中标记的实体/要素';
-COMMENT ON COLUMN document_entities.entity_type IS '实体类型: PERSON/ORG/LOC/DATE/NUMBER/MONEY等';
-COMMENT ON COLUMN document_entities.source IS '来源: auto=自动识别, manual=手动标注';
-
--- 17. 文档元素表 (document_elements)
 CREATE TABLE IF NOT EXISTS document_elements (
     id VARCHAR(64) PRIMARY KEY,
     document_id VARCHAR(64) NOT NULL,
-    element_index INT NOT NULL,                  -- 元素在文档中的顺序
-    element_type VARCHAR(32) NOT NULL,           -- paragraph/heading/heading1-9/list_item/image/table/title/toc
-    content TEXT,                                -- 文本内容(文本类型)
-    style JSONB,                                 -- 样式信息
-    -- 图片相关
+    element_index INT NOT NULL,
+    element_type VARCHAR(32) NOT NULL,
+    content TEXT,
+    style JSONB,
+    runs JSONB,
     image_url VARCHAR(500),
     image_path VARCHAR(500),
     image_alt VARCHAR(255),
     image_width INT,
     image_height INT,
     image_format VARCHAR(16),
-    -- 表格相关
     table_index INT,
-    table_data JSONB,                            -- [[{row,col,text,colSpan},...],...]
+    table_data JSONB,
     table_row_count INT,
     table_col_count INT,
-    table_text TEXT,                             -- 表格文本(用于搜索)
+    table_text TEXT,
     create_by VARCHAR(64),
     create_by_name VARCHAR(128),
     create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -534,31 +355,23 @@ CREATE TABLE IF NOT EXISTS document_elements (
     update_by_name VARCHAR(128),
     update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_document_elements_document_id ON document_elements(document_id);
 CREATE INDEX IF NOT EXISTS idx_document_elements_type ON document_elements(element_type);
 CREATE INDEX IF NOT EXISTS idx_document_elements_order ON document_elements(document_id, element_index);
 
-COMMENT ON TABLE document_elements IS '文档元素表 - Word/PDF结构化提取';
-COMMENT ON COLUMN document_elements.element_type IS '元素类型: paragraph/heading/image/table等';
-COMMENT ON COLUMN document_elements.style IS '样式信息JSON: {alignment, fontSize, bold, color等}';
-COMMENT ON COLUMN document_elements.table_data IS '表格数据JSON: [[{row,col,text,colSpan},...],...]';
+-- ============================================ 五、模板系统模块 ============================================
 
--- ============================================
--- 六、模板系统模块(v2.0)
--- ============================================
-
--- 18. 报告模板表 (templates)
 CREATE TABLE IF NOT EXISTS templates (
     id VARCHAR(36) PRIMARY KEY,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     name VARCHAR(255) NOT NULL,
     description TEXT,
     base_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
-    status VARCHAR(32) DEFAULT 'draft',          -- draft/published/archived
+    status VARCHAR(32) DEFAULT 'draft',
     config JSONB DEFAULT '{}',
     is_public BOOLEAN DEFAULT FALSE,
     use_count INT DEFAULT 0,
+    rating DECIMAL(2,1) DEFAULT 0.0,
     create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     create_by VARCHAR(36),
@@ -566,114 +379,73 @@ CREATE TABLE IF NOT EXISTS templates (
     update_by VARCHAR(36),
     update_by_name VARCHAR(100)
 );
-
 CREATE INDEX IF NOT EXISTS idx_templates_user_id ON templates(user_id);
 CREATE INDEX IF NOT EXISTS idx_templates_status ON templates(status);
 CREATE INDEX IF NOT EXISTS idx_templates_is_public ON templates(is_public);
 CREATE INDEX IF NOT EXISTS idx_templates_base_document ON templates(base_document_id);
 
-COMMENT ON TABLE templates IS '报告模板表 - 示例文档驱动的模板';
-COMMENT ON COLUMN templates.base_document_id IS '示例报告文档ID(可选,空白模板可为空)';
-COMMENT ON COLUMN templates.status IS 'draft-草稿, published-已发布, archived-已归档';
-COMMENT ON COLUMN templates.is_public IS '是否公开给其他用户使用';
-
--- 19. 来源文件定义表 (source_files)
 CREATE TABLE IF NOT EXISTS source_files (
     id VARCHAR(36) PRIMARY KEY,
     template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    alias VARCHAR(100) NOT NULL,                 -- 文件别名,如"可研批复"
+    alias VARCHAR(100) NOT NULL,
     description TEXT,
-    file_types JSONB DEFAULT '["pdf", "docx"]',  -- 允许的文件类型
+    file_types JSONB DEFAULT '["pdf", "docx"]',
     required BOOLEAN DEFAULT TRUE,
     example_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
     display_order INT DEFAULT 0,
     create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
+    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     CONSTRAINT uk_source_files_alias UNIQUE (template_id, alias)
 );
-
 CREATE INDEX IF NOT EXISTS idx_source_files_template ON source_files(template_id);
 
-COMMENT ON TABLE source_files IS '来源文件定义表';
-COMMENT ON COLUMN source_files.alias IS '用户自定义的别名,用于引用';
-COMMENT ON COLUMN source_files.file_types IS '允许上传的文件类型列表';
-COMMENT ON COLUMN source_files.example_document_id IS '创建模板时使用的示例文件';
-
--- 20. 模板变量表 (variables)
 CREATE TABLE IF NOT EXISTS variables (
     id VARCHAR(36) PRIMARY KEY,
     template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    -- 变量标识
-    name VARCHAR(100) NOT NULL,                  -- 变量名(程序用)
-    display_name VARCHAR(200) NOT NULL,          -- 显示名称
-    variable_group VARCHAR(100),                 -- 变量分组
-    category VARCHAR(32),                        -- 变量类别(前端显示用): entity/concept/data/location/asset
-    -- 在示例报告中的位置
-    location JSONB NOT NULL,                     -- {elementId, type, startOffset, endOffset, rowIndex, colIndex}
-    -- 示例值
+    name VARCHAR(100) NOT NULL,
+    display_name VARCHAR(200) NOT NULL,
+    variable_group VARCHAR(100),
+    category VARCHAR(32),
+    location JSONB NOT NULL,
     example_value TEXT,
-    value_type VARCHAR(32) DEFAULT 'text',       -- text/date/number/table
-    -- 数据来源
-    source_file_alias VARCHAR(100),              -- 来源文件别名
-    source_type VARCHAR(32) NOT NULL,            -- document/manual/reference/fixed
+    value_type VARCHAR(32) DEFAULT 'text',
+    source_file_alias VARCHAR(100),
+    source_type VARCHAR(32) NOT NULL,
     source_config JSONB,
-    -- 提取方式
-    extract_type VARCHAR(32),                    -- direct/ai_extract/ai_summarize
+    extract_type VARCHAR(32),
     extract_config JSONB,
     display_order INT DEFAULT 0,
     create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
     CONSTRAINT uk_variables_name UNIQUE (template_id, name)
 );
-
 CREATE INDEX IF NOT EXISTS idx_variables_template ON variables(template_id);
 CREATE INDEX IF NOT EXISTS idx_variables_source_alias ON variables(source_file_alias);
 CREATE INDEX IF NOT EXISTS idx_variables_source_type ON variables(source_type);
 CREATE INDEX IF NOT EXISTS idx_variables_category ON variables(category);
 
-COMMENT ON TABLE variables IS '模板变量表';
-COMMENT ON COLUMN variables.name IS '变量名,模板内唯一';
-COMMENT ON COLUMN variables.location IS '变量在文档中的位置';
-COMMENT ON COLUMN variables.source_type IS 'document-从来源文件提取, manual-手动输入, reference-引用其他变量, fixed-固定值';
-COMMENT ON COLUMN variables.extract_type IS 'direct-直接提取, ai_extract-AI字段提取, ai_summarize-AI总结';
-COMMENT ON COLUMN variables.category IS 'entity-核心实体, concept-概念/技术, data-数据/指标, location-地点/组织, asset-资源模板';
-
--- 21. 生成任务表 (generations)
 CREATE TABLE IF NOT EXISTS generations (
     id VARCHAR(36) PRIMARY KEY,
     template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE RESTRICT,
     user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     name VARCHAR(255),
-    -- 来源文件映射
-    source_file_map JSONB NOT NULL,              -- {"可研批复": "doc_123", ...}
-    -- 变量提取结果
-    variable_values JSONB,                       -- {varName: {value, confidence, status, ...}}
-    -- 生成的文档
+    source_file_map JSONB NOT NULL,
+    variable_values JSONB,
     output_document_id VARCHAR(36) REFERENCES documents(id) ON DELETE SET NULL,
     output_file_path VARCHAR(500),
-    status VARCHAR(32) DEFAULT 'pending',        -- pending/extracting/review/completed/error
+    status VARCHAR(32) DEFAULT 'pending',
     error_message TEXT,
-    progress INT DEFAULT 0,                      -- 0-100
+    progress INT DEFAULT 0,
     create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     completed_at TIMESTAMP
 );
-
 CREATE INDEX IF NOT EXISTS idx_generations_template ON generations(template_id);
 CREATE INDEX IF NOT EXISTS idx_generations_user ON generations(user_id);
 CREATE INDEX IF NOT EXISTS idx_generations_status ON generations(status);
 CREATE INDEX IF NOT EXISTS idx_generations_create_time ON generations(create_time DESC);
 
-COMMENT ON TABLE generations IS '报告生成任务表';
-COMMENT ON COLUMN generations.source_file_map IS '来源文件映射: {"别名": "文档ID"}';
-COMMENT ON COLUMN generations.variable_values IS '变量提取结果';
-COMMENT ON COLUMN generations.status IS 'pending-待执行, extracting-提取中, review-待确认, completed-已完成, error-错误';
-
--- ============================================
--- 七、触发器函数与触发器
--- ============================================
+-- ============================================ 触发器 ============================================
 
--- 更新时间触发器函数
 CREATE OR REPLACE FUNCTION update_update_time_column()
 RETURNS TRIGGER AS $$
 BEGIN
@@ -682,16 +454,14 @@ BEGIN
 END;
 $$ LANGUAGE plpgsql;
 
--- 为所有需要的表创建触发器
 DO $$
 DECLARE
     tbl TEXT;
     tables TEXT[] := ARRAY[
-        'users', 'documents', 'elements', 'annotations', 'graphs', 
-        'parse_tasks', 'sessions', 'graph_nodes', 'graph_relations',
-        'rules', 'data_sources', 'text_storage', 'text_chunks',
-        'document_blocks', 'document_entities', 'document_elements',
-        'templates', 'variables'
+        'users', 'documents', 'elements', 'annotations', 'graphs',
+        'parse_tasks', 'sessions', 'rules', 'data_sources', 'text_storage',
+        'text_chunks', 'document_blocks', 'document_entities', 'document_elements',
+        'templates', 'source_files', 'variables', 'generations'
     ];
 BEGIN
     FOREACH tbl IN ARRAY tables LOOP
@@ -700,31 +470,18 @@ BEGIN
     END LOOP;
 END $$;
 
--- ============================================
--- 八、辅助函数
--- ============================================
+-- ============================================ 向量检索函数 ============================================
 
--- 向量相似度检索(按文档)
 CREATE OR REPLACE FUNCTION search_similar_chunks(
     query_embedding vector(768),
     target_document_id VARCHAR(36),
     result_limit INTEGER DEFAULT 3
 )
-RETURNS TABLE (
-    chunk_id VARCHAR(36),
-    document_id VARCHAR(36),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
+RETURNS TABLE (chunk_id VARCHAR(36), document_id VARCHAR(36), content TEXT, chunk_index INTEGER, similarity FLOAT) AS $$
 BEGIN
     RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
+    SELECT tc.id, tc.document_id, tc.content, tc.chunk_index,
+           1 - (ve.embedding <=> query_embedding) AS similarity
     FROM text_chunks tc
     JOIN vector_embeddings ve ON tc.id = ve.chunk_id
     WHERE tc.document_id = target_document_id
@@ -733,26 +490,15 @@ BEGIN
 END;
 $$ LANGUAGE plpgsql;
 
--- 向量相似度检索(全局)
 CREATE OR REPLACE FUNCTION search_similar_chunks_global(
     query_embedding vector(768),
     result_limit INTEGER DEFAULT 5
 )
-RETURNS TABLE (
-    chunk_id VARCHAR(36),
-    document_id VARCHAR(36),
-    content TEXT,
-    chunk_index INTEGER,
-    similarity FLOAT
-) AS $$
+RETURNS TABLE (chunk_id VARCHAR(36), document_id VARCHAR(36), content TEXT, chunk_index INTEGER, similarity FLOAT) AS $$
 BEGIN
     RETURN QUERY
-    SELECT 
-        tc.id AS chunk_id,
-        tc.document_id,
-        tc.content,
-        tc.chunk_index,
-        1 - (ve.embedding <=> query_embedding) AS similarity
+    SELECT tc.id, tc.document_id, tc.content, tc.chunk_index,
+           1 - (ve.embedding <=> query_embedding) AS similarity
     FROM text_chunks tc
     JOIN vector_embeddings ve ON tc.id = ve.chunk_id
     ORDER BY ve.embedding <=> query_embedding
@@ -760,8 +506,4 @@ BEGIN
 END;
 $$ LANGUAGE plpgsql;
 
--- ============================================
--- 完成
--- ============================================
-SELECT '灵越智报 v2.0 数据库表创建完成' AS result;
-SELECT COUNT(*) AS "表总数" FROM pg_tables WHERE schemaname = 'public';
+SELECT '灵越智报 v2.0 数据库初始化完成' AS result;

+ 0 - 57
database/migrations/V2026_01_21_02__add_document_elements.sql

@@ -1,57 +0,0 @@
--- 文档结构化元素表
--- 存储从 Word/PDF 等文档中提取的结构化内容(段落、图片、表格)
-
-CREATE TABLE IF NOT EXISTS document_elements (
-    id VARCHAR(64) PRIMARY KEY,
-    document_id VARCHAR(64) NOT NULL,
-    element_index INT NOT NULL,              -- 元素在文档中的顺序
-    element_type VARCHAR(32) NOT NULL,       -- paragraph/heading/heading1-9/list_item/image/table/title/toc
-    content TEXT,                            -- 文本内容(文本类型)
-    style JSONB,                             -- 样式信息(字体、对齐、缩进等)
-    
-    -- 图片相关
-    image_url VARCHAR(500),                  -- 图片访问URL
-    image_path VARCHAR(500),                 -- 图片存储路径
-    image_alt VARCHAR(255),                  -- 图片描述
-    image_width INT,                         -- 图片宽度(像素)
-    image_height INT,                        -- 图片高度(像素)
-    image_format VARCHAR(16),                -- 图片格式
-    
-    -- 表格相关
-    table_index INT,                         -- 表格索引
-    table_data JSONB,                        -- 表格数据(行列内容)
-    table_row_count INT,                     -- 行数
-    table_col_count INT,                     -- 列数
-    table_text TEXT,                         -- 表格文本(用于搜索)
-    
-    -- 审计字段
-    create_by VARCHAR(64),
-    create_by_name VARCHAR(128),
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_by_name VARCHAR(128),
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- 索引
-CREATE INDEX IF NOT EXISTS idx_document_elements_document_id ON document_elements(document_id);
-CREATE INDEX IF NOT EXISTS idx_document_elements_type ON document_elements(element_type);
-CREATE INDEX IF NOT EXISTS idx_document_elements_order ON document_elements(document_id, element_index);
-
--- 注释
-COMMENT ON TABLE document_elements IS '文档结构化元素表';
-COMMENT ON COLUMN document_elements.element_index IS '元素在文档中的顺序索引';
-COMMENT ON COLUMN document_elements.element_type IS '元素类型:paragraph/heading/image/table等';
-COMMENT ON COLUMN document_elements.style IS '样式信息JSON:{alignment, fontSize, bold, color等}';
-COMMENT ON COLUMN document_elements.table_data IS '表格数据JSON:[[{row,col,text,colSpan},...],...]';
-
--- 更新 documents 表,添加结构化解析状态
-ALTER TABLE documents ADD COLUMN IF NOT EXISTS structured_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE documents ADD COLUMN IF NOT EXISTS image_count INT DEFAULT 0;
-ALTER TABLE documents ADD COLUMN IF NOT EXISTS table_count INT DEFAULT 0;
-ALTER TABLE documents ADD COLUMN IF NOT EXISTS element_count INT DEFAULT 0;
-
-COMMENT ON COLUMN documents.structured_status IS '结构化解析状态:pending/completed/failed';
-COMMENT ON COLUMN documents.image_count IS '文档中的图片数量';
-COMMENT ON COLUMN documents.table_count IS '文档中的表格数量';
-COMMENT ON COLUMN documents.element_count IS '文档中的元素总数';

+ 0 - 50
database/migrations/V2026_01_21_03__enhance_data_sources.sql

@@ -1,50 +0,0 @@
--- 数据源表增强
--- 新增 value_type, aggregate_type, separator 字段
--- 修改 node_ids 为 JSONB 类型以支持混合节点引用
-
--- 添加新字段
-ALTER TABLE data_sources 
-ADD COLUMN IF NOT EXISTS value_type VARCHAR(20) DEFAULT 'text';
-
-ALTER TABLE data_sources 
-ADD COLUMN IF NOT EXISTS aggregate_type VARCHAR(20) DEFAULT 'first';
-
-ALTER TABLE data_sources 
-ADD COLUMN IF NOT EXISTS separator VARCHAR(50) DEFAULT '';
-
--- 添加字段注释
-COMMENT ON COLUMN data_sources.value_type IS '值类型: text/image/table/mixed';
-COMMENT ON COLUMN data_sources.aggregate_type IS '聚合方式: first/last/concat/sum/avg/list';
-COMMENT ON COLUMN data_sources.separator IS '聚合分隔符(concat时使用)';
-
--- 检查并修改 node_ids 类型(如果是 TEXT[] 则转为 JSONB)
--- 注意:如果已经是 JSONB 类型,此语句会失败,这是预期行为
-DO $$
-BEGIN
-    -- 检查当前类型
-    IF EXISTS (
-        SELECT 1 
-        FROM information_schema.columns 
-        WHERE table_name = 'data_sources' 
-        AND column_name = 'node_ids' 
-        AND data_type = 'ARRAY'
-    ) THEN
-        -- 如果是数组类型,转换为 JSONB
-        ALTER TABLE data_sources 
-        ALTER COLUMN node_ids TYPE JSONB 
-        USING CASE 
-            WHEN node_ids IS NULL THEN '{"refs": []}'::JSONB
-            ELSE jsonb_build_object('refs', 
-                (SELECT jsonb_agg(jsonb_build_object('type', 'graph_node', 'id', elem))
-                 FROM unnest(node_ids::text[]) AS elem)
-            )
-        END;
-        RAISE NOTICE 'Converted node_ids from TEXT[] to JSONB';
-    ELSE
-        RAISE NOTICE 'node_ids is already JSONB or compatible type';
-    END IF;
-END $$;
-
--- 设置默认值
-ALTER TABLE data_sources 
-ALTER COLUMN node_ids SET DEFAULT '{"refs": []}'::JSONB;

+ 0 - 69
database/migrations/V2026_01_21__add_document_blocks_and_entities.sql

@@ -1,69 +0,0 @@
--- 文档块表:存储文档的结构化内容(参考飞书Block设计)
--- 核心设计:块内容由 elements 数组(TextElement)组成,实体作为元素嵌入
-CREATE TABLE IF NOT EXISTS document_blocks (
-    id VARCHAR(64) PRIMARY KEY,
-    document_id VARCHAR(64) NOT NULL,
-    parent_id VARCHAR(64),
-    children JSONB,
-    block_index INTEGER NOT NULL,
-    block_type VARCHAR(32) NOT NULL,
-    elements JSONB,
-    style JSONB,
-    metadata JSONB,
-    create_by VARCHAR(64),
-    create_by_name VARCHAR(128),
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_by_name VARCHAR(128),
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- 索引
-CREATE INDEX IF NOT EXISTS idx_document_blocks_document_id ON document_blocks(document_id);
-CREATE INDEX IF NOT EXISTS idx_document_blocks_parent_id ON document_blocks(parent_id);
-CREATE INDEX IF NOT EXISTS idx_document_blocks_block_type ON document_blocks(block_type);
--- 全文搜索索引(对elements中的文本内容)
-CREATE INDEX IF NOT EXISTS idx_document_blocks_elements_gin ON document_blocks USING GIN (elements jsonb_path_ops);
-
--- 文档实体标注表:存储文档中的实体/要素标记
-CREATE TABLE IF NOT EXISTS document_entities (
-    id VARCHAR(64) PRIMARY KEY,
-    document_id VARCHAR(64) NOT NULL,
-    block_id VARCHAR(64),
-    name VARCHAR(512) NOT NULL,
-    entity_type VARCHAR(32) NOT NULL,
-    value TEXT,
-    block_char_start INTEGER,
-    block_char_end INTEGER,
-    global_char_start INTEGER,
-    global_char_end INTEGER,
-    anchor_before VARCHAR(100),
-    anchor_after VARCHAR(100),
-    source VARCHAR(16) DEFAULT 'auto',
-    confidence DECIMAL(5,4),
-    confirmed BOOLEAN DEFAULT FALSE,
-    graph_node_id VARCHAR(64),
-    metadata JSONB,
-    create_by VARCHAR(64),
-    create_by_name VARCHAR(128),
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_by_name VARCHAR(128),
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- 索引
-CREATE INDEX IF NOT EXISTS idx_document_entities_document_id ON document_entities(document_id);
-CREATE INDEX IF NOT EXISTS idx_document_entities_block_id ON document_entities(block_id);
-CREATE INDEX IF NOT EXISTS idx_document_entities_type ON document_entities(entity_type);
-CREATE INDEX IF NOT EXISTS idx_document_entities_name ON document_entities(name);
-CREATE INDEX IF NOT EXISTS idx_document_entities_global_char ON document_entities(document_id, global_char_start, global_char_end);
-CREATE INDEX IF NOT EXISTS idx_document_entities_graph_node ON document_entities(graph_node_id);
-
--- 注释
-COMMENT ON TABLE document_blocks IS '文档块 - 存储文档的结构化内容块';
-COMMENT ON TABLE document_entities IS '文档实体 - 存储文档中标记的实体/要素';
-
-COMMENT ON COLUMN document_blocks.block_type IS '块类型: heading1/heading2/heading3/paragraph/table/list/image/quote';
-COMMENT ON COLUMN document_entities.entity_type IS '实体类型: PERSON/ORG/LOC/DATE/NUMBER/MONEY/CONCEPT/DATA/DEVICE/TERM';
-COMMENT ON COLUMN document_entities.source IS '来源: auto=自动识别, manual=手动标注';

+ 0 - 36
database/migrations/V2026_01_22_01__enhance_parse_tasks_stages.sql

@@ -1,36 +0,0 @@
--- ============================================
--- 扩展 parse_tasks 表,支持多阶段进度跟踪
--- ============================================
-
--- 添加各阶段状态字段
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS parse_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS parse_progress INTEGER DEFAULT 0;
-
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS rag_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS rag_progress INTEGER DEFAULT 0;
-
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS structured_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS structured_progress INTEGER DEFAULT 0;
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS structured_element_count INTEGER;
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS structured_image_count INTEGER;
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS structured_table_count INTEGER;
-
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_progress INTEGER DEFAULT 0;
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_task_id VARCHAR(64);
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_entity_count INTEGER;
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_relation_count INTEGER;
-
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS graph_status VARCHAR(20) DEFAULT 'pending';
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS graph_progress INTEGER DEFAULT 0;
-
--- 添加索引
-CREATE INDEX IF NOT EXISTS idx_parse_tasks_ner_task_id ON parse_tasks(ner_task_id);
-
--- 添加注释
-COMMENT ON COLUMN parse_tasks.parse_status IS '解析阶段状态: pending/processing/completed/failed';
-COMMENT ON COLUMN parse_tasks.rag_status IS 'RAG向量化阶段状态';
-COMMENT ON COLUMN parse_tasks.structured_status IS '结构化解析阶段状态';
-COMMENT ON COLUMN parse_tasks.ner_status IS 'NER实体提取阶段状态';
-COMMENT ON COLUMN parse_tasks.graph_status IS '图构建阶段状态';
-COMMENT ON COLUMN parse_tasks.ner_task_id IS 'NER Python服务任务ID';

+ 0 - 200
database/migrations/V2026_01_22_02__create_extract_tables.sql

@@ -1,200 +0,0 @@
--- ============================================
--- 数据提取规则系统核心表
--- extract-service 模块
--- ============================================
-
--- ============================================
--- 1. 项目表 (projects)
--- ============================================
-CREATE TABLE IF NOT EXISTS extract_projects (
-    id VARCHAR(32) PRIMARY KEY,
-    user_id VARCHAR(32) NOT NULL,
-    name VARCHAR(255) NOT NULL,
-    description TEXT,
-    status VARCHAR(32) DEFAULT 'draft',  -- draft/extracting/completed/archived
-    config JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_extract_projects_user_id ON extract_projects(user_id);
-CREATE INDEX IF NOT EXISTS idx_extract_projects_status ON extract_projects(status);
-
-COMMENT ON TABLE extract_projects IS '数据提取项目';
-COMMENT ON COLUMN extract_projects.status IS '状态: draft-草稿, extracting-提取中, completed-已完成, archived-已归档';
-COMMENT ON COLUMN extract_projects.config IS '项目配置: outputFormat, autoExtract, notifyOnComplete, aiModel';
-
--- ============================================
--- 2. 来源文档表 (source_documents)
--- ============================================
-CREATE TABLE IF NOT EXISTS extract_source_documents (
-    id VARCHAR(32) PRIMARY KEY,
-    project_id VARCHAR(32) NOT NULL REFERENCES extract_projects(id) ON DELETE CASCADE,
-    document_id VARCHAR(36) NOT NULL,  -- 关联 documents 表
-    alias VARCHAR(128) NOT NULL,       -- 文档别名,如"可研批复"
-    doc_type VARCHAR(32) NOT NULL,     -- pdf/docx/xlsx
-    display_order INT DEFAULT 0,
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uq_source_doc_alias UNIQUE (project_id, alias)
-);
-
-CREATE INDEX IF NOT EXISTS idx_source_docs_project_id ON extract_source_documents(project_id);
-CREATE INDEX IF NOT EXISTS idx_source_docs_document_id ON extract_source_documents(document_id);
-
-COMMENT ON TABLE extract_source_documents IS '项目来源文档';
-COMMENT ON COLUMN extract_source_documents.alias IS '文档别名,如"可研批复"、"可行性研究报告"';
-COMMENT ON COLUMN extract_source_documents.metadata IS '元数据: fileName, fileSize, pageCount, parseStatus';
-
--- ============================================
--- 3. 提取规则表 (extract_rules)
--- ============================================
-CREATE TABLE IF NOT EXISTS extract_rules (
-    id VARCHAR(32) PRIMARY KEY,
-    project_id VARCHAR(32) NOT NULL REFERENCES extract_projects(id) ON DELETE CASCADE,
-    source_doc_id VARCHAR(32),  -- 可为空,表示引用/固定/手动类型
-    
-    -- 目标字段
-    target_field_key VARCHAR(128) NOT NULL,   -- 字段Key(程序用)
-    target_field_name VARCHAR(255) NOT NULL,  -- 字段名称(显示用)
-    target_field_group VARCHAR(128),          -- 字段分组
-    rule_index INT NOT NULL DEFAULT 0,        -- 规则顺序
-    
-    -- 来源配置
-    source_type VARCHAR(32) NOT NULL,  -- document/self_reference/fixed/manual
-    source_config JSONB NOT NULL,      -- 来源配置详情
-    
-    -- 提取配置
-    extract_type VARCHAR(32) NOT NULL, -- direct/ai_extract/ai_summarize/ocr
-    extract_config JSONB,              -- 提取配置详情
-    
-    -- 结果
-    status VARCHAR(32) DEFAULT 'pending',  -- pending/extracting/extracted/confirmed/error
-    extracted_value TEXT,
-    value_type VARCHAR(32) DEFAULT 'text', -- text/table/image/list
-    error_message TEXT,
-    
-    -- 元数据
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uq_rule_field_key UNIQUE (project_id, target_field_key)
-);
-
-CREATE INDEX IF NOT EXISTS idx_extract_rules_project_id ON extract_rules(project_id);
-CREATE INDEX IF NOT EXISTS idx_extract_rules_source_doc_id ON extract_rules(source_doc_id);
-CREATE INDEX IF NOT EXISTS idx_extract_rules_status ON extract_rules(status);
-CREATE INDEX IF NOT EXISTS idx_extract_rules_rule_index ON extract_rules(project_id, rule_index);
-
-COMMENT ON TABLE extract_rules IS '数据提取规则';
-COMMENT ON COLUMN extract_rules.source_type IS '来源类型: document-文档, self_reference-引用已提取字段, fixed-固定值, manual-手动输入';
-COMMENT ON COLUMN extract_rules.extract_type IS '提取类型: direct-直接提取, ai_extract-AI字段提取, ai_summarize-AI总结, ocr-OCR识别';
-COMMENT ON COLUMN extract_rules.source_config IS '来源配置: location, paragraphKeyword, referenceFieldKeys等';
-COMMENT ON COLUMN extract_rules.extract_config IS '提取配置: targetDescription, expectedFormat, summarizePrompt等';
-
--- ============================================
--- 4. 提取结果表 (extract_results)
--- ============================================
-CREATE TABLE IF NOT EXISTS extract_results (
-    id VARCHAR(32) PRIMARY KEY,
-    rule_id VARCHAR(32) NOT NULL REFERENCES extract_rules(id) ON DELETE CASCADE,
-    project_id VARCHAR(32) NOT NULL REFERENCES extract_projects(id) ON DELETE CASCADE,
-    
-    -- 提取结果
-    extracted_value TEXT NOT NULL,
-    value_type VARCHAR(32) DEFAULT 'text',
-    
-    -- 来源追溯
-    source_content TEXT,        -- 来源原文内容
-    source_location JSONB,      -- 来源位置信息
-    
-    -- 质量评估
-    confidence DECIMAL(5,4),    -- AI提取置信度 0-1
-    
-    -- 状态
-    status VARCHAR(32) DEFAULT 'extracted',  -- extracted/confirmed/rejected/modified
-    
-    -- 人工处理
-    modified_value TEXT,        -- 人工修正后的值
-    confirmed_at TIMESTAMP,
-    confirmed_by VARCHAR(32),
-    reject_reason TEXT,         -- 拒绝原因
-    
-    -- 元数据
-    metadata JSONB DEFAULT '{}',
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_extract_results_rule_id ON extract_results(rule_id);
-CREATE INDEX IF NOT EXISTS idx_extract_results_project_id ON extract_results(project_id);
-CREATE INDEX IF NOT EXISTS idx_extract_results_status ON extract_results(status);
-
-COMMENT ON TABLE extract_results IS '提取结果历史';
-COMMENT ON COLUMN extract_results.source_location IS '来源位置: documentId, documentAlias, locationType, pageStart, pageEnd, elementIds, chapterPath';
-COMMENT ON COLUMN extract_results.status IS '状态: extracted-已提取, confirmed-已确认, rejected-已拒绝, modified-已修正';
-
--- ============================================
--- 5. 规则模板表 (rule_templates)
--- ============================================
-CREATE TABLE IF NOT EXISTS extract_rule_templates (
-    id VARCHAR(32) PRIMARY KEY,
-    user_id VARCHAR(32) NOT NULL,
-    name VARCHAR(255) NOT NULL,
-    description TEXT,
-    
-    -- 模板内容
-    rules_snapshot JSONB NOT NULL,     -- 规则配置快照
-    doc_type_pattern JSONB,            -- 适用的文档类型模式
-    
-    -- 可见性
-    is_public BOOLEAN DEFAULT FALSE,
-    
-    -- 统计
-    use_count INT DEFAULT 0,
-    
-    -- 元数据
-    metadata JSONB DEFAULT '{}',
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100),
-    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX IF NOT EXISTS idx_rule_templates_user_id ON extract_rule_templates(user_id);
-CREATE INDEX IF NOT EXISTS idx_rule_templates_is_public ON extract_rule_templates(is_public);
-
-COMMENT ON TABLE extract_rule_templates IS '规则模板';
-COMMENT ON COLUMN extract_rule_templates.rules_snapshot IS '规则配置快照(不含项目特定信息)';
-COMMENT ON COLUMN extract_rule_templates.doc_type_pattern IS '适用的文档类型模式,用于自动匹配';
-
--- ============================================
--- 更新时间触发器
--- ============================================
-DROP TRIGGER IF EXISTS update_extract_projects_update_time ON extract_projects;
-CREATE TRIGGER update_extract_projects_update_time BEFORE UPDATE ON extract_projects
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_extract_rules_update_time ON extract_rules;
-CREATE TRIGGER update_extract_rules_update_time BEFORE UPDATE ON extract_rules
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
-DROP TRIGGER IF EXISTS update_rule_templates_update_time ON extract_rule_templates;
-CREATE TRIGGER update_rule_templates_update_time BEFORE UPDATE ON extract_rule_templates
-    FOR EACH ROW EXECUTE FUNCTION update_update_time_column();
-
--- ============================================
--- 显示创建结果
--- ============================================
-SELECT 'Extract Service 数据表创建成功' AS result;

+ 0 - 159
database/migrations/V2026_01_23_01__refactor_extract_to_template.sql

@@ -1,159 +0,0 @@
--- =====================================================
--- 数据提取规则系统重构 v2.0
--- 从「规则配置驱动」改为「示例文档驱动」
--- =====================================================
-
--- 1. 创建 templates 表(替代 extract_projects)
-CREATE TABLE IF NOT EXISTS templates (
-    id VARCHAR(36) PRIMARY KEY,
-    user_id VARCHAR(36) NOT NULL,
-    name VARCHAR(255) NOT NULL,
-    description TEXT,
-    base_document_id VARCHAR(36) NOT NULL COMMENT '示例报告文档ID',
-    status VARCHAR(32) DEFAULT 'draft' COMMENT '状态: draft/published/archived',
-    config JSONB COMMENT '模板配置',
-    is_public BOOLEAN DEFAULT FALSE COMMENT '是否公开',
-    use_count INT DEFAULT 0 COMMENT '使用次数',
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    create_by VARCHAR(36),
-    create_by_name VARCHAR(100),
-    update_by VARCHAR(36),
-    update_by_name VARCHAR(100)
-);
-
-CREATE INDEX IF NOT EXISTS idx_templates_user_id ON templates(user_id);
-CREATE INDEX IF NOT EXISTS idx_templates_status ON templates(status);
-CREATE INDEX IF NOT EXISTS idx_templates_is_public ON templates(is_public);
-
-COMMENT ON TABLE templates IS '报告模板';
-COMMENT ON COLUMN templates.base_document_id IS '示例报告文档ID,关联 documents 表';
-COMMENT ON COLUMN templates.status IS 'draft-草稿, published-已发布, archived-已归档';
-COMMENT ON COLUMN templates.config IS '模板配置,如默认AI模型等';
-
--- 2. 创建 source_files 表(来源文件定义,替代 extract_source_documents)
-CREATE TABLE IF NOT EXISTS source_files (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    alias VARCHAR(100) NOT NULL COMMENT '文件别名,如"可研批复"',
-    description TEXT COMMENT '文件说明',
-    file_types JSONB DEFAULT '["pdf", "docx"]' COMMENT '允许的文件类型',
-    required BOOLEAN DEFAULT TRUE COMMENT '是否必须',
-    example_document_id VARCHAR(36) COMMENT '创建模板时使用的示例文件',
-    display_order INT DEFAULT 0 COMMENT '显示顺序',
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uk_source_files_alias UNIQUE (template_id, alias)
-);
-
-CREATE INDEX IF NOT EXISTS idx_source_files_template ON source_files(template_id);
-
-COMMENT ON TABLE source_files IS '来源文件定义';
-COMMENT ON COLUMN source_files.alias IS '用户自定义的别名,用于引用';
-COMMENT ON COLUMN source_files.file_types IS '允许上传的文件类型列表';
-COMMENT ON COLUMN source_files.example_document_id IS '创建模板时使用的示例文件,用于预览';
-
--- 3. 创建 variables 表(替代 extract_rules)
-CREATE TABLE IF NOT EXISTS variables (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
-    
-    -- 变量标识
-    name VARCHAR(100) NOT NULL COMMENT '变量名(程序用)',
-    display_name VARCHAR(200) NOT NULL COMMENT '显示名称',
-    variable_group VARCHAR(100) COMMENT '变量分组',
-    
-    -- 在示例报告中的位置
-    location JSONB NOT NULL COMMENT '文档中的位置',
-    
-    -- 示例值
-    example_value TEXT COMMENT '示例值(原文档中的值)',
-    value_type VARCHAR(32) DEFAULT 'text' COMMENT '值类型: text/date/number/table',
-    
-    -- 数据来源
-    source_file_alias VARCHAR(100) COMMENT '来源文件别名',
-    source_type VARCHAR(32) NOT NULL COMMENT '来源类型: document/manual/reference/fixed',
-    source_config JSONB COMMENT '来源配置',
-    
-    -- 提取方式
-    extract_type VARCHAR(32) COMMENT '提取类型: direct/ai_extract/ai_summarize',
-    extract_config JSONB COMMENT '提取配置',
-    
-    display_order INT DEFAULT 0 COMMENT '显示顺序',
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    
-    CONSTRAINT uk_variables_name UNIQUE (template_id, name)
-);
-
-CREATE INDEX IF NOT EXISTS idx_variables_template ON variables(template_id);
-CREATE INDEX IF NOT EXISTS idx_variables_source_alias ON variables(source_file_alias);
-
-COMMENT ON TABLE variables IS '模板变量';
-COMMENT ON COLUMN variables.name IS '变量名,模板内唯一,用于程序引用';
-COMMENT ON COLUMN variables.location IS '变量在文档中的位置,包含 element_id、偏移量等';
-COMMENT ON COLUMN variables.source_type IS 'document-从来源文件提取, manual-手动输入, reference-引用其他变量, fixed-固定值';
-COMMENT ON COLUMN variables.extract_type IS 'direct-直接提取, ai_extract-AI字段提取, ai_summarize-AI总结';
-
--- 4. 创建 generations 表(生成任务)
-CREATE TABLE IF NOT EXISTS generations (
-    id VARCHAR(36) PRIMARY KEY,
-    template_id VARCHAR(36) NOT NULL REFERENCES templates(id),
-    user_id VARCHAR(36) NOT NULL,
-    
-    name VARCHAR(255) COMMENT '任务名称',
-    
-    -- 来源文件映射:别名 → 文档ID
-    source_file_map JSONB NOT NULL COMMENT '来源文件映射',
-    
-    -- 变量提取结果
-    variable_values JSONB COMMENT '变量值',
-    
-    -- 生成的文档
-    output_document_id VARCHAR(36) COMMENT '输出文档ID',
-    output_file_path VARCHAR(500) COMMENT '输出文件路径',
-    
-    status VARCHAR(32) DEFAULT 'pending' COMMENT '状态',
-    error_message TEXT COMMENT '错误信息',
-    progress INT DEFAULT 0 COMMENT '进度百分比',
-    
-    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    completed_at TIMESTAMP COMMENT '完成时间'
-);
-
-CREATE INDEX IF NOT EXISTS idx_generations_template ON generations(template_id);
-CREATE INDEX IF NOT EXISTS idx_generations_user ON generations(user_id);
-CREATE INDEX IF NOT EXISTS idx_generations_status ON generations(status);
-
-COMMENT ON TABLE generations IS '报告生成任务';
-COMMENT ON COLUMN generations.source_file_map IS '来源文件映射,如 {"可研批复": "doc_123"}';
-COMMENT ON COLUMN generations.variable_values IS '变量提取结果,包含值、置信度、状态等';
-COMMENT ON COLUMN generations.status IS 'pending-待执行, extracting-提取中, review-待确认, completed-已完成, error-错误';
-
--- 5. 添加 update_time 触发器
-CREATE OR REPLACE FUNCTION update_timestamp()
-RETURNS TRIGGER AS $$
-BEGIN
-    NEW.update_time = CURRENT_TIMESTAMP;
-    RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-DROP TRIGGER IF EXISTS trigger_templates_update_time ON templates;
-CREATE TRIGGER trigger_templates_update_time
-    BEFORE UPDATE ON templates
-    FOR EACH ROW
-    EXECUTE FUNCTION update_timestamp();
-
-DROP TRIGGER IF EXISTS trigger_variables_update_time ON variables;
-CREATE TRIGGER trigger_variables_update_time
-    BEFORE UPDATE ON variables
-    FOR EACH ROW
-    EXECUTE FUNCTION update_timestamp();
-
--- 6. 注释:旧表可以保留用于数据迁移,或在确认无需后删除
--- DROP TABLE IF EXISTS extract_rule_templates CASCADE;
--- DROP TABLE IF EXISTS extract_results CASCADE;
--- DROP TABLE IF EXISTS extract_rules CASCADE;
--- DROP TABLE IF EXISTS extract_source_documents CASCADE;
--- DROP TABLE IF EXISTS extract_projects CASCADE;

+ 0 - 34
database/migrations/V2026_01_24_01__add_variable_category.sql

@@ -1,34 +0,0 @@
--- =====================================================
--- V2026_01_24_01__add_variable_category.sql
--- 
--- 为 variables 表添加 category 字段
--- 用于前端显示分类(与原型 HTML 适配)
--- 
--- 变量类别:
---   entity   - 核心实体(蓝色)
---   concept  - 概念/技术(紫色)
---   data     - 数据/指标(绿色)
---   location - 地点/组织(橙色)
---   asset    - 资源模板(粉色)
--- =====================================================
-
--- 添加 category 列
-ALTER TABLE variables ADD COLUMN IF NOT EXISTS category VARCHAR(32);
-
--- 创建索引
-CREATE INDEX IF NOT EXISTS idx_variables_category ON variables(category);
-
--- 添加注释
-COMMENT ON COLUMN variables.category IS '变量类别(前端显示用): entity-核心实体, concept-概念/技术, data-数据/指标, location-地点/组织, asset-资源模板';
-
--- 基于现有数据自动推断 category(可选)
--- 根据 value_type 和 source_type 推断
-UPDATE variables 
-SET category = CASE 
-    WHEN value_type = 'number' THEN 'data'
-    WHEN value_type = 'date' THEN 'data'
-    WHEN source_type = 'fixed' THEN 'asset'
-    WHEN source_type = 'reference' THEN 'concept'
-    ELSE 'entity'
-END
-WHERE category IS NULL;

+ 0 - 15
database/migrations/V2026_01_27_01__make_base_document_id_nullable.sql

@@ -1,15 +0,0 @@
--- 使 templates 表的 base_document_id 字段可为空
--- 支持创建空白模板(不需要基础文档)
-
--- 移除外键约束
-ALTER TABLE templates DROP CONSTRAINT IF EXISTS templates_base_document_id_fkey;
-
--- 修改列为可空
-ALTER TABLE templates ALTER COLUMN base_document_id DROP NOT NULL;
-
--- 重新添加外键约束(允许 NULL)
-ALTER TABLE templates ADD CONSTRAINT templates_base_document_id_fkey 
-    FOREIGN KEY (base_document_id) REFERENCES documents(id) ON DELETE SET NULL;
-
--- 更新注释
-COMMENT ON COLUMN templates.base_document_id IS '示例报告文档ID(可选,空白模板可为空)';

+ 0 - 4
database/migrations/V2026_01_27_02__add_ner_message_field.sql

@@ -1,4 +0,0 @@
--- 添加 NER 消息字段,用于存储分块处理进度等信息
-ALTER TABLE parse_tasks ADD COLUMN IF NOT EXISTS ner_message VARCHAR(255);
-
-COMMENT ON COLUMN parse_tasks.ner_message IS 'NER阶段消息(如分块进度信息)';

+ 0 - 8
database/migrations/V2026_01_28_01__add_document_elements_runs.sql

@@ -1,8 +0,0 @@
--- 为 document_elements 表添加 runs 列
--- 用于存储段落内不同格式的文本片段(TextRun)
-
-ALTER TABLE document_elements 
-ADD COLUMN IF NOT EXISTS runs JSONB;
-
--- 添加注释
-COMMENT ON COLUMN document_elements.runs IS '文本片段列表(JSON),保留段落内不同格式的文本片段: [{text, fontFamily, fontSize, bold, italic, underline, color, strikeThrough, verticalAlign, highlightColor}, ...]';

+ 0 - 7
database/migrations/V2026_01_29_01__add_template_rating.sql

@@ -1,7 +0,0 @@
--- 添加模板评分字段
-ALTER TABLE templates ADD COLUMN IF NOT EXISTS rating DECIMAL(2,1) DEFAULT 0.0;
-
-COMMENT ON COLUMN templates.rating IS '模板评分 (0.0-5.0)';
-
--- 为现有模板设置默认评分
-UPDATE templates SET rating = 4.5 WHERE rating IS NULL OR rating = 0;

+ 0 - 362
docs/neo4j-local-install.md

@@ -1,362 +0,0 @@
-# Neo4j 本地部署指南
-
-## 1. 系统要求
-
-### 硬件要求
-- **CPU**: Intel/AMD x86-64 或 ARM
-- **内存**: 最低 2GB,推荐 16GB+
-- **存储**: 最低 10GB,推荐 SSD
-
-### 软件要求
-- **操作系统**: Ubuntu 22.04/24.04, Debian 11-13, RHEL 8.x/9.x, CentOS 等
-- **Java**: Java 21 或更高版本
-
----
-
-## 2. 安装方法
-
-### 方法 A: Debian/Ubuntu 系统(推荐)
-
-#### 步骤 1: 安装 Java 21
-
-```bash
-# 检查是否已安装 Java
-java -version
-
-# 如果没有,安装 OpenJDK 21
-sudo apt update
-sudo apt install openjdk-21-jdk -y
-
-# 验证安装
-java -version
-```
-
-#### 步骤 2: 添加 Neo4j 仓库
-
-```bash
-# 创建密钥目录
-sudo mkdir -p /etc/apt/keyrings
-
-# 添加 GPG 密钥
-wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/neotechnology.gpg
-sudo chmod a+r /etc/apt/keyrings/neotechnology.gpg
-
-# 添加仓库
-echo 'deb [signed-by=/etc/apt/keyrings/neotechnology.gpg] https://debian.neo4j.com stable latest' | sudo tee /etc/apt/sources.list.d/neo4j.list
-
-# 更新包列表
-sudo apt update
-```
-
-#### 步骤 3: 安装 Neo4j
-
-```bash
-# 安装 Neo4j Community Edition(免费)
-sudo apt install neo4j -y
-
-# 或者指定版本
-# sudo apt install neo4j=1:5.15.0 -y
-```
-
-#### 步骤 4: 配置并启动服务
-
-```bash
-# 设置初始密码(替换 your-password)
-sudo neo4j-admin dbms set-initial-password your-password
-
-# 启用开机自启
-sudo systemctl enable neo4j
-
-# 启动服务
-sudo systemctl start neo4j
-
-# 查看状态
-sudo systemctl status neo4j
-```
-
----
-
-### 方法 B: Tarball 安装(更灵活)
-
-#### 步骤 1: 下载并解压
-
-```bash
-# 下载 Neo4j Community Edition
-wget https://neo4j.com/artifact.php?name=neo4j-community-5.15.0-unix.tar.gz -O neo4j-community-5.15.0-unix.tar.gz
-
-# 解压
-tar -xzf neo4j-community-5.15.0-unix.tar.gz
-
-# 移动到 /opt
-sudo mv neo4j-community-5.15.0 /opt/neo4j
-```
-
-#### 步骤 2: 创建用户和设置权限
-
-```bash
-# 创建 neo4j 用户
-sudo useradd -r -s /bin/false neo4j
-
-# 设置目录权限
-sudo chown -R neo4j:neo4j /opt/neo4j
-```
-
-#### 步骤 3: 配置环境变量
-
-```bash
-# 添加到 ~/.bashrc 或 /etc/profile
-export NEO4J_HOME=/opt/neo4j
-export PATH=$NEO4J_HOME/bin:$PATH
-```
-
-#### 步骤 4: 创建 systemd 服务
-
-```bash
-sudo tee /etc/systemd/system/neo4j.service > /dev/null <<EOF
-[Unit]
-Description=Neo4j Graph Database
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-ExecStart=/opt/neo4j/bin/neo4j console
-Restart=on-abnormal
-User=neo4j
-Group=neo4j
-Environment="NEO4J_CONF=/opt/neo4j/conf"
-Environment="NEO4J_HOME=/opt/neo4j"
-LimitNOFILE=60000
-TimeoutSec=120
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-# 重新加载 systemd
-sudo systemctl daemon-reload
-
-# 设置初始密码
-sudo -u neo4j /opt/neo4j/bin/neo4j-admin dbms set-initial-password your-password
-
-# 启用并启动
-sudo systemctl enable neo4j
-sudo systemctl start neo4j
-```
-
----
-
-## 3. 配置 Neo4j
-
-配置文件位置: `/etc/neo4j/neo4j.conf` 或 `/opt/neo4j/conf/neo4j.conf`
-
-### 3.1 允许远程连接
-
-```bash
-sudo nano /etc/neo4j/neo4j.conf
-```
-
-找到并取消注释以下行:
-
-```properties
-# 监听所有接口(生产环境请谨慎)
-server.default_listen_address=0.0.0.0
-
-# HTTP 端口(浏览器访问)
-server.http.listen_address=:7474
-
-# Bolt 端口(应用连接)
-server.bolt.listen_address=:7687
-```
-
-### 3.2 内存配置(可选)
-
-```properties
-# 堆内存设置
-server.memory.heap.initial_size=512m
-server.memory.heap.max_size=2g
-
-# 页面缓存(建议设置为可用内存的 50%)
-server.memory.pagecache.size=1g
-```
-
-### 3.3 重启服务
-
-```bash
-sudo systemctl restart neo4j
-```
-
----
-
-## 4. 验证安装
-
-### 4.1 浏览器访问
-
-打开浏览器访问: http://localhost:7474
-
-- 用户名: `neo4j`
-- 密码: 你设置的密码
-
-### 4.2 命令行测试
-
-```bash
-# 使用 cypher-shell 连接
-cypher-shell -u neo4j -p your-password
-
-# 运行测试查询
-RETURN "Hello Neo4j!" AS message;
-
-# 退出
-:exit
-```
-
-### 4.3 查看日志
-
-```bash
-# 查看日志
-sudo journalctl -u neo4j -f
-
-# 或者
-tail -f /var/log/neo4j/neo4j.log
-```
-
----
-
-## 5. 灵越智报项目配置
-
-### 5.1 修改环境变量
-
-创建或编辑 `.env` 文件:
-
-```bash
-# Neo4j 配置
-NEO4J_ENABLED=true
-NEO4J_URI=bolt://localhost:7687
-NEO4J_USERNAME=neo4j
-NEO4J_PASSWORD=your-password
-NEO4J_DATABASE=neo4j
-```
-
-### 5.2 或直接修改 application.properties
-
-```properties
-# Neo4j 图数据库配置
-neo4j.enabled=true
-neo4j.uri=bolt://localhost:7687
-neo4j.username=neo4j
-neo4j.password=your-password
-neo4j.database=neo4j
-neo4j.connection-timeout-seconds=30
-neo4j.max-connection-pool-size=50
-```
-
-### 5.3 启动项目后初始化索引
-
-```bash
-# 调用 API 初始化 Neo4j 索引
-curl -X POST http://localhost:5232/api/v1/neo4j/init-indexes
-```
-
----
-
-## 6. 常用命令
-
-```bash
-# 启动 Neo4j
-sudo systemctl start neo4j
-
-# 停止 Neo4j
-sudo systemctl stop neo4j
-
-# 重启 Neo4j
-sudo systemctl restart neo4j
-
-# 查看状态
-sudo systemctl status neo4j
-
-# 查看日志
-sudo journalctl -u neo4j -f
-
-# 进入 Cypher Shell
-cypher-shell -u neo4j -p your-password
-```
-
----
-
-## 7. 常用 Cypher 查询
-
-```cypher
--- 查看所有节点数量
-MATCH (n) RETURN count(n);
-
--- 查看所有关系数量
-MATCH ()-[r]->() RETURN count(r);
-
--- 查看节点标签统计
-MATCH (n) RETURN labels(n), count(*) ORDER BY count(*) DESC;
-
--- 查看关系类型统计
-MATCH ()-[r]->() RETURN type(r), count(*) ORDER BY count(*) DESC;
-
--- 删除所有数据(谨慎使用!)
-MATCH (n) DETACH DELETE n;
-
--- 查看某文档的所有节点
-MATCH (n {documentId: 'your-doc-id'}) RETURN n;
-
--- 查看某节点的所有关系
-MATCH (n {id: 'your-node-id'})-[r]-(m) RETURN n, r, m;
-```
-
----
-
-## 8. 故障排查
-
-### 问题 1: 服务启动失败
-
-```bash
-# 查看详细错误
-sudo journalctl -u neo4j --no-pager -n 50
-
-# 检查端口占用
-sudo netstat -tlnp | grep 7687
-sudo netstat -tlnp | grep 7474
-```
-
-### 问题 2: 连接被拒绝
-
-1. 检查服务是否运行: `sudo systemctl status neo4j`
-2. 检查防火墙: `sudo ufw status`
-3. 检查配置文件中的 `listen_address`
-
-### 问题 3: 内存不足
-
-修改 `neo4j.conf` 降低内存配置:
-
-```properties
-server.memory.heap.initial_size=256m
-server.memory.heap.max_size=512m
-server.memory.pagecache.size=256m
-```
-
-### 问题 4: 忘记密码
-
-```bash
-# 停止服务
-sudo systemctl stop neo4j
-
-# 重置密码
-sudo neo4j-admin dbms set-initial-password new-password --require-password-change=false
-
-# 启动服务
-sudo systemctl start neo4j
-```
-
----
-
-## 9. 生产环境建议
-
-1. **使用强密码**: 不要使用默认密码
-2. **限制访问**: 配置防火墙只允许应用服务器访问
-3. **定期备份**: 使用 `neo4j-admin database dump` 备份数据
-4. **监控**: 配置 Prometheus + Grafana 监控
-5. **日志轮转**: 配置日志轮转避免磁盘占满

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

@@ -402,11 +402,6 @@ export const documentApi = {
     return api.get(`/documents/${documentId}/tables`)
   },
 
-  // 重新生成并保存文档块结构
-  regenerateBlocks(documentId) {
-    return api.post(`/ner/documents/${documentId}/regenerate-blocks`)
-  },
-
   // ==================== 块操作 ====================
 
   // 更新块元素

+ 1 - 43
frontend/vue-demo/src/views/Editor.vue

@@ -223,15 +223,6 @@
             
             <!-- 右侧:操作按钮 -->
             <div class="toolbar-actions">
-              <el-button 
-                size="small"
-                :icon="Refresh" 
-                :loading="regenerating"
-                @click="handleRegenerateBlocks"
-                title="重新生成文档结构"
-              >
-                重新生成
-              </el-button>
               <el-button size="small" :icon="Clock" title="版本历史">版本</el-button>
               <el-button size="small" :icon="Share" circle @click="showGraphModal = true" title="知识图谱" />
               <el-divider direction="vertical" />
@@ -853,7 +844,7 @@
 import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import {
-  ArrowLeft, Clock, Share, Check, Plus, Delete, Connection, Refresh, Search, Loading, Upload
+  ArrowLeft, Clock, Share, Check, Plus, Delete, Connection, Search, Loading, Upload
 } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useTemplateStore } from '@/stores/template'
@@ -893,7 +884,6 @@ const saved = ref(true)
 const editorRef = ref(null)
 const editorContentRef = ref(null)
 const loading = ref(false)
-const regenerating = ref(false)
 
 // 面板宽度(可拖拽调整)
 const leftPanelWidth = ref(300)
@@ -2618,38 +2608,6 @@ function handleSave() {
   ElMessage.success('保存成功')
 }
 
-// 重新生成文档块结构
-async function handleRegenerateBlocks() {
-  const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
-  if (!baseDocumentId) {
-    ElMessage.warning('没有关联的示例文档')
-    return
-  }
-
-  regenerating.value = true
-  try {
-    const result = await documentApi.regenerateBlocks(baseDocumentId)
-    ElMessage.success(`重新生成成功: ${result.blockCount} 个文档块, ${result.entityCount} 个实体`)
-    
-    // 重新加载文档内容
-    const structuredDoc = await documentApi.getStructured(baseDocumentId)
-    if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
-      blocks.value = structuredDoc.blocks
-      documentParagraphs.value = structuredDoc.paragraphs || []
-      documentImages.value = structuredDoc.images || []
-      documentTables.value = structuredDoc.tables || []
-      const extractedEntities = extractEntitiesFromBlocks(structuredDoc.blocks)
-      entities.value = extractedEntities
-      documentContent.value = renderStructuredDocument({ ...structuredDoc, entities: extractedEntities })
-    }
-  } catch (error) {
-    console.error('重新生成失败:', error)
-    ElMessage.error('重新生成失败: ' + (error.message || '未知错误'))
-  } finally {
-    regenerating.value = false
-  }
-}
-
 function getFileIcon(file) {
   return '📄'
 }

+ 4 - 16
server-deploy.sh

@@ -372,22 +372,10 @@ init_database_tables() {
     
     cd ${PROJECT_DIR}
     
-    # 执行初始化脚本
-    if [ -f "backend/sql/init.sql" ]; then
-        PGPASSWORD=${DB_PASS} psql -U ${DB_USER} -d ${DB_NAME} -h ${DB_HOST} -f backend/sql/init.sql 2>/dev/null || true
-        log_info "init.sql 执行完成"
-    fi
-    
-    # 执行 RAG 表脚本
-    if [ -f "backend/sql/rag_tables.sql" ]; then
-        PGPASSWORD=${DB_PASS} psql -U ${DB_USER} -d ${DB_NAME} -h ${DB_HOST} -f backend/sql/rag_tables.sql 2>/dev/null || true
-        log_info "rag_tables.sql 执行完成"
-    fi
-    
-    # 执行补充表脚本
-    if [ -f "backend/sql/supplement_tables.sql" ]; then
-        PGPASSWORD=${DB_PASS} psql -U ${DB_USER} -d ${DB_NAME} -h ${DB_HOST} -f backend/sql/supplement_tables.sql 2>/dev/null || true
-        log_info "supplement_tables.sql 执行完成"
+    # 执行初始化脚本(单文件)
+    if [ -f "database/init.sql" ]; then
+        PGPASSWORD=${DB_PASS} psql -U ${DB_USER} -d ${DB_NAME} -h ${DB_HOST} -f database/init.sql 2>/dev/null || true
+        log_info "database/init.sql 执行完成"
     fi
 }
 

+ 5 - 20
start.sh

@@ -191,31 +191,16 @@ init_db() {
     
     cd "$PROJECT_DIR"
     
-    log_info "执行基础表初始化..."
-    psql -U lingyue -d lingyue_zhibao -f backend/sql/init.sql
-    
-    if [ -f backend/sql/supplement_tables.sql ]; then
-        log_info "执行补充表初始化..."
-        psql -U lingyue -d lingyue_zhibao -f backend/sql/supplement_tables.sql
-    fi
-    
+    log_info "执行数据库初始化 (database/init.sql)..."
+    psql -U lingyue -d lingyue_zhibao -f database/init.sql
     log_info "数据库初始化完成 ✓"
 }
 
-# 初始化 RAG 表
+# 初始化 RAG 表(已合并到 database/init.sql,执行完整 init 即可)
 init_rag() {
     log_title "初始化 RAG 表"
-    
-    cd "$PROJECT_DIR"
-    
-    # 检查 pgvector
-    if psql -U lingyue -d lingyue_zhibao -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null; then
-        log_info "pgvector 扩展已启用 ✓"
-        psql -U lingyue -d lingyue_zhibao -f backend/sql/rag_tables.sql
-        log_info "RAG 表初始化完成 ✓"
-    else
-        log_error "pgvector 未安装,请先安装: yay -S postgresql-pgvector"
-    fi
+    log_info "RAG 表已包含在 database/init.sql 中,请执行: ./start.sh init-db"
+    init_db
 }
 
 # 显示帮助