Explorar o código

feat(extract-service): Variable 添加 category 字段适配前端原型

- Variable 实体添加 category 字段(entity/concept/data/location/asset)
- 新增按类别分组查询 API: GET /templates/{id}/variables/grouped
- Repository/Service/Controller 层完整支持 category 过滤和分组
- 新增数据库迁移脚本 V2026_01_24_01__add_variable_category.sql
- 更新进度报告,补充模板系统和生成任务 API 清单
何文松 hai 1 mes
pai
achega
ac6b4cf6f9

+ 26 - 1
backend/extract-service/src/main/java/com/lingyue/extract/controller/TemplateController.java

@@ -274,13 +274,16 @@ public class TemplateController {
     public AjaxResult<?> listVariables(
             @Parameter(description = "模板ID") @PathVariable String templateId,
             @Parameter(description = "来源文件别名过滤") @RequestParam(required = false) String sourceFileAlias,
-            @Parameter(description = "变量分组过滤") @RequestParam(required = false) String group) {
+            @Parameter(description = "变量分组过滤") @RequestParam(required = false) String group,
+            @Parameter(description = "变量类别过滤") @RequestParam(required = false) String category) {
         List<Variable> variables;
         
         if (sourceFileAlias != null) {
             variables = variableService.listBySourceFileAlias(templateId, sourceFileAlias);
         } else if (group != null) {
             variables = variableService.listByGroup(templateId, group);
+        } else if (category != null) {
+            variables = variableService.listByCategory(templateId, category);
         } else {
             variables = variableService.listByTemplateId(templateId);
         }
@@ -291,6 +294,28 @@ public class TemplateController {
         return AjaxResult.success(responses);
     }
     
+    @GetMapping("/{templateId}/variables/grouped")
+    @Operation(summary = "获取变量按类别分组", description = "获取模板的所有变量,按 category 分组返回")
+    public AjaxResult<?> listVariablesGrouped(
+            @Parameter(description = "模板ID") @PathVariable String templateId) {
+        java.util.Map<String, List<Variable>> grouped = variableService.listGroupedByCategory(templateId);
+        
+        // 转换为 VariableResponse
+        java.util.Map<String, List<VariableResponse>> result = new java.util.LinkedHashMap<>();
+        // 按固定顺序排列类别
+        String[] categoryOrder = {"entity", "concept", "data", "location", "asset", "other"};
+        for (String cat : categoryOrder) {
+            List<Variable> vars = grouped.get(cat);
+            if (vars != null && !vars.isEmpty()) {
+                result.put(cat, vars.stream()
+                        .map(VariableResponse::fromEntity)
+                        .collect(Collectors.toList()));
+            }
+        }
+        
+        return AjaxResult.success(result);
+    }
+    
     @GetMapping("/variables/{id}")
     @Operation(summary = "获取变量详情", description = "获取变量详情")
     public AjaxResult<?> getVariable(

+ 3 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/AddVariableRequest.java

@@ -29,6 +29,9 @@ public class AddVariableRequest {
     @Schema(description = "变量分组")
     private String variableGroup;
     
+    @Schema(description = "变量类别(前端显示用): entity/concept/data/location/asset")
+    private String category;
+    
     @NotNull(message = "变量位置不能为空")
     @Schema(description = "变量在文档中的位置", required = true)
     private VariableLocation location;

+ 4 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/VariableResponse.java

@@ -33,6 +33,9 @@ public class VariableResponse {
     @Schema(description = "变量分组")
     private String variableGroup;
     
+    @Schema(description = "变量类别(前端显示用): entity/concept/data/location/asset")
+    private String category;
+    
     @Schema(description = "变量位置")
     private VariableLocation location;
     
@@ -76,6 +79,7 @@ public class VariableResponse {
         response.setName(variable.getName());
         response.setDisplayName(variable.getDisplayName());
         response.setVariableGroup(variable.getVariableGroup());
+        response.setCategory(variable.getCategory());
         response.setLocation(variable.getLocation());
         response.setExampleValue(variable.getExampleValue());
         response.setValueType(variable.getValueType());

+ 17 - 0
backend/extract-service/src/main/java/com/lingyue/extract/entity/Variable.java

@@ -48,6 +48,10 @@ public class Variable {
     @TableField("variable_group")
     private String variableGroup;
     
+    @Schema(description = "变量类别(前端显示用): entity-核心实体, concept-概念/技术, data-数据/指标, location-地点/组织, asset-资源模板")
+    @TableField("category")
+    private String category;
+    
     // ==================== 在示例报告中的位置 ====================
     
     @Schema(description = "变量在文档中的位置")
@@ -123,4 +127,17 @@ public class Variable {
     public static final String VALUE_TYPE_DATE = "date";
     public static final String VALUE_TYPE_NUMBER = "number";
     public static final String VALUE_TYPE_TABLE = "table";
+    
+    // ==================== 变量类别常量(前端显示用) ====================
+    
+    /** 核心实体(蓝色) - 智慧园区、公司名等 */
+    public static final String CATEGORY_ENTITY = "entity";
+    /** 概念/技术(紫色) - 产业升级、智能化管理等 */
+    public static final String CATEGORY_CONCEPT = "concept";
+    /** 数据/指标(绿色) - 金额、面积、增长率等 */
+    public static final String CATEGORY_DATA = "data";
+    /** 地点/组织(橙色) - 华南地区、华南事业部等 */
+    public static final String CATEGORY_LOCATION = "location";
+    /** 资源模板(粉色) - 图表、结论模板等 */
+    public static final String CATEGORY_ASSET = "asset";
 }

+ 12 - 0
backend/extract-service/src/main/java/com/lingyue/extract/repository/VariableRepository.java

@@ -49,6 +49,18 @@ public interface VariableRepository extends BaseMapper<Variable> {
     @Select("SELECT * FROM variables WHERE template_id = #{templateId} AND variable_group = #{group} ORDER BY display_order")
     List<Variable> findByTemplateIdAndGroup(@Param("templateId") String templateId, @Param("group") String group);
     
+    /**
+     * 根据变量类别查询
+     */
+    @Select("SELECT * FROM variables WHERE template_id = #{templateId} AND category = #{category} ORDER BY display_order")
+    List<Variable> findByTemplateIdAndCategory(@Param("templateId") String templateId, @Param("category") String category);
+    
+    /**
+     * 按类别统计变量数量
+     */
+    @Select("SELECT category, COUNT(*) as count FROM variables WHERE template_id = #{templateId} GROUP BY category")
+    List<Map<String, Object>> countByTemplateIdGroupByCategory(@Param("templateId") String templateId);
+    
     /**
      * 删除模板的所有变量
      */

+ 23 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/VariableService.java

@@ -43,6 +43,7 @@ public class VariableService {
         variable.setName(request.getName());
         variable.setDisplayName(request.getDisplayName());
         variable.setVariableGroup(request.getVariableGroup());
+        variable.setCategory(request.getCategory());
         variable.setLocation(request.getLocation());
         variable.setExampleValue(request.getExampleValue());
         variable.setValueType(request.getValueType() != null ? request.getValueType() : Variable.VALUE_TYPE_TEXT);
@@ -116,6 +117,25 @@ public class VariableService {
         return variableRepository.findByTemplateIdAndGroup(templateId, group);
     }
     
+    /**
+     * 按类别获取变量
+     */
+    public List<Variable> listByCategory(String templateId, String category) {
+        return variableRepository.findByTemplateIdAndCategory(templateId, category);
+    }
+    
+    /**
+     * 获取变量按类别分组
+     * 返回 Map<category, List<Variable>>
+     */
+    public java.util.Map<String, List<Variable>> listGroupedByCategory(String templateId) {
+        List<Variable> variables = variableRepository.findByTemplateId(templateId);
+        return variables.stream()
+                .collect(java.util.stream.Collectors.groupingBy(
+                        v -> v.getCategory() != null ? v.getCategory() : "other"
+                ));
+    }
+    
     /**
      * 更新变量
      */
@@ -141,6 +161,9 @@ public class VariableService {
         if (request.getVariableGroup() != null) {
             variable.setVariableGroup(request.getVariableGroup());
         }
+        if (request.getCategory() != null) {
+            variable.setCategory(request.getCategory());
+        }
         if (request.getLocation() != null) {
             variable.setLocation(request.getLocation());
         }

+ 3 - 0
backend/sql/all_tables.sql

@@ -607,6 +607,7 @@ CREATE TABLE IF NOT EXISTS variables (
     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}
     -- 示例值
@@ -629,12 +630,14 @@ CREATE TABLE IF NOT EXISTS variables (
 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 (

+ 34 - 0
database/migrations/V2026_01_24_01__add_variable_category.sql

@@ -0,0 +1,34 @@
+-- =====================================================
+-- 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;

+ 49 - 6
进度报告.md

@@ -1,10 +1,10 @@
 # 📊 灵越智报 2.0 - 当前进度总结
 
-**整体进度:88%**  |  **报告日期:2026-01-24**
+**整体进度:90%**  |  **报告日期:2026-01-24**
 
 ---
 
-## ✅ 已完成(88%)
+## ✅ 已完成(90%)
 
 ### 基础设施
 - Spring Boot 3.1.5 单体应用架构(lingyue-starter)
@@ -46,18 +46,18 @@ NER服务       ████████████████████ 100
 前端界面      ████░░░░░░░░░░░░░░░░  20% (HTML原型)
 ```
 
-### 新增功能(2026-01-24)✅ 模板系统完整实现
+### 新增功能(2026-01-24)✅ 模板系统完整实现 + 原型适配
 
-- ✅ **Service 层(5个服务)**
+- ✅ **Service 层(6个服务)**
   - `TemplateService` - 模板 CRUD、发布、归档、复制
   - `SourceFileService` - 来源文件定义管理、重排序
-  - `VariableService` - 变量管理、重排序、预览提取
+  - `VariableService` - 变量管理、重排序、预览提取、**按类别分组查询**
   - `GenerationService` - 生成任务管理、执行、进度
   - `ExtractionService` - 变量提取核心逻辑
   - `DocumentGenerationService` - 文档生成、文件下载
 
 - ✅ **Controller 层(2个控制器)**
-  - `TemplateController` - 模板/来源文件/变量统一管理
+  - `TemplateController` - 模板/来源文件/变量统一管理、**变量分组API**
   - `GenerationController` - 生成任务 CRUD 和执行
 
 - ✅ **DTO(12个)**
@@ -67,6 +67,17 @@ NER服务       ████████████████████ 100
 - ✅ **工具类**
   - `SecurityUtils` - 从 JWT 获取当前用户ID
 
+- ✅ **原型适配 - Variable.category 字段(新增)**
+  - 变量类别用于前端显示分类(与 `灵越智报_完整交互版.html` 原型对齐)
+  - 类别定义:
+    - `entity` - 核心实体(蓝色):智慧园区、公司名等
+    - `concept` - 概念/技术(紫色):产业升级、智能化管理等
+    - `data` - 数据/指标(绿色):金额、面积、增长率等
+    - `location` - 地点/组织(橙色):华南地区、华南事业部等
+    - `asset` - 资源模板(粉色):图表、结论模板等
+  - 新增 API:`GET /api/v1/templates/{templateId}/variables/grouped` - 按类别分组返回变量
+  - 数据库迁移:`V2026_01_24_01__add_variable_category.sql`
+
 ### 新增功能(2026-01-23)✅ 模板系统 v2.0 数据模型重构
 
 - ✅ **设计理念升级**
@@ -496,6 +507,38 @@ database/
 | `/api/v1/datasource/{id}/value` | GET | **获取数据源值** | ✅ |
 | `/api/v1/datasource/batch-value` | POST | **批量获取值** | ✅ |
 
+### 模板系统(extract-service)
+
+| 接口 | 方法 | 说明 | 状态 |
+|------|------|------|------|
+| `/api/v1/templates` | POST | 创建模板 | ✅ |
+| `/api/v1/templates/{id}` | GET | 获取模板详情 | ✅ |
+| `/api/v1/templates` | GET | 模板列表 | ✅ |
+| `/api/v1/templates/{id}` | PUT | 更新模板 | ✅ |
+| `/api/v1/templates/{id}` | DELETE | 删除模板 | ✅ |
+| `/api/v1/templates/{id}/publish` | POST | 发布模板 | ✅ |
+| `/api/v1/templates/{id}/archive` | POST | 归档模板 | ✅ |
+| `/api/v1/templates/{id}/duplicate` | POST | 复制模板 | ✅ |
+| `/api/v1/templates/{id}/source-files` | POST | 添加来源文件定义 | ✅ |
+| `/api/v1/templates/{id}/source-files` | GET | 获取来源文件列表 | ✅ |
+| `/api/v1/templates/{id}/variables` | POST | 添加变量 | ✅ |
+| `/api/v1/templates/{id}/variables` | GET | 获取变量列表 | ✅ |
+| `/api/v1/templates/{id}/variables/grouped` | GET | **变量按类别分组** | ✅ |
+| `/api/v1/templates/variables/{id}/preview` | POST | 预览提取结果 | ✅ |
+
+### 生成任务(extract-service)
+
+| 接口 | 方法 | 说明 | 状态 |
+|------|------|------|------|
+| `/api/v1/generations` | POST | 创建生成任务 | ✅ |
+| `/api/v1/generations/{id}` | GET | 获取任务详情 | ✅ |
+| `/api/v1/generations` | GET | 获取任务列表 | ✅ |
+| `/api/v1/generations/{id}/execute` | POST | 执行变量提取 | ✅ |
+| `/api/v1/generations/{id}/progress` | GET | 获取执行进度 | ✅ |
+| `/api/v1/generations/{id}/variables/{varName}` | PUT | 修改变量值 | ✅ |
+| `/api/v1/generations/{id}/confirm` | POST | 确认并生成文档 | ✅ |
+| `/api/v1/generations/{id}/download` | GET | 下载生成文档 | ✅ |
+
 ---
 
 ## 🗄️ 数据库表清单(21张)