Pārlūkot izejas kodu

feat(extract-service): 实现模板系统 Service 和 Controller 层

新增 Service(3个):
- TemplateService: 模板 CRUD、发布、归档、复制
- SourceFileService: 来源文件定义管理
- VariableService: 变量管理

新增 Controller:
- TemplateController: 统一的模板管理 REST API
  - /api/v1/templates - 模板 CRUD
  - /api/v1/templates/{id}/publish - 发布
  - /api/v1/templates/{id}/archive - 归档
  - /api/v1/templates/{id}/duplicate - 复制
  - /api/v1/templates/{templateId}/source-files - 来源文件管理
  - /api/v1/templates/{templateId}/variables - 变量管理

新增 DTO(8个):
- 请求:CreateTemplateRequest, UpdateTemplateRequest,
        AddSourceFileRequest, AddVariableRequest
- 响应:TemplateDetailResponse, TemplateListResponse,
        SourceFileResponse, VariableResponse
何文松 1 mēnesi atpakaļ
vecāks
revīzija
4454793beb

+ 333 - 0
backend/extract-service/src/main/java/com/lingyue/extract/controller/TemplateController.java

@@ -0,0 +1,333 @@
+package com.lingyue.extract.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.lingyue.common.domain.AjaxResult;
+import com.lingyue.extract.dto.request.AddSourceFileRequest;
+import com.lingyue.extract.dto.request.AddVariableRequest;
+import com.lingyue.extract.dto.request.CreateTemplateRequest;
+import com.lingyue.extract.dto.request.UpdateTemplateRequest;
+import com.lingyue.extract.dto.response.SourceFileResponse;
+import com.lingyue.extract.dto.response.TemplateDetailResponse;
+import com.lingyue.extract.dto.response.TemplateListResponse;
+import com.lingyue.extract.dto.response.VariableResponse;
+import com.lingyue.extract.entity.SourceFile;
+import com.lingyue.extract.entity.Template;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.service.SourceFileService;
+import com.lingyue.extract.service.TemplateService;
+import com.lingyue.extract.service.VariableService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 模板管理控制器
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/templates")
+@RequiredArgsConstructor
+@Tag(name = "模板管理", description = "报告模板 CRUD 接口")
+public class TemplateController {
+    
+    private final TemplateService templateService;
+    private final SourceFileService sourceFileService;
+    private final VariableService variableService;
+    
+    // TODO: 从认证上下文获取用户ID
+    private String getCurrentUserId() {
+        return "test-user-001";
+    }
+    
+    // ==================== 模板 CRUD ====================
+    
+    @PostMapping
+    @Operation(summary = "创建模板", description = "创建新的报告模板")
+    public AjaxResult<?> createTemplate(@Valid @RequestBody CreateTemplateRequest request) {
+        try {
+            String userId = getCurrentUserId();
+            Template template = templateService.create(userId, request);
+            return AjaxResult.success("创建成功", TemplateDetailResponse.fromEntity(template));
+        } catch (Exception e) {
+            log.error("创建模板失败", e);
+            return AjaxResult.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{id}")
+    @Operation(summary = "获取模板详情", description = "获取模板详情,包含来源文件和变量列表")
+    public AjaxResult<?> getTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id) {
+        TemplateDetailResponse response = templateService.getTemplateDetail(id);
+        if (response == null) {
+            return AjaxResult.error("模板不存在");
+        }
+        return AjaxResult.success(response);
+    }
+    
+    @GetMapping
+    @Operation(summary = "获取模板列表", description = "分页获取当前用户的模板列表")
+    public AjaxResult<?> listTemplates(
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
+            @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") int size) {
+        String userId = getCurrentUserId();
+        Page<TemplateListResponse> result = templateService.listByUserId(userId, page, size);
+        return AjaxResult.success(result);
+    }
+    
+    @GetMapping("/accessible")
+    @Operation(summary = "获取可访问的模板", description = "获取用户可见的模板(自己的 + 公开的)")
+    public AjaxResult<?> listAccessibleTemplates() {
+        String userId = getCurrentUserId();
+        List<TemplateListResponse> result = templateService.listAccessible(userId);
+        return AjaxResult.success(result);
+    }
+    
+    @GetMapping("/search")
+    @Operation(summary = "搜索模板", description = "按名称搜索模板")
+    public AjaxResult<?> searchTemplates(
+            @Parameter(description = "关键词") @RequestParam String keyword) {
+        String userId = getCurrentUserId();
+        List<TemplateListResponse> result = templateService.search(userId, keyword);
+        return AjaxResult.success(result);
+    }
+    
+    @PutMapping("/{id}")
+    @Operation(summary = "更新模板", description = "更新模板基本信息")
+    public AjaxResult<?> updateTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id,
+            @Valid @RequestBody UpdateTemplateRequest request) {
+        try {
+            Template template = templateService.update(id, request);
+            if (template == null) {
+                return AjaxResult.error("模板不存在");
+            }
+            return AjaxResult.success("更新成功", TemplateDetailResponse.fromEntity(template));
+        } catch (Exception e) {
+            log.error("更新模板失败", e);
+            return AjaxResult.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除模板", description = "删除模板(级联删除来源文件和变量)")
+    public AjaxResult<?> deleteTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id) {
+        try {
+            boolean success = templateService.delete(id);
+            if (!success) {
+                return AjaxResult.error("模板不存在");
+            }
+            return AjaxResult.success("删除成功");
+        } catch (Exception e) {
+            log.error("删除模板失败", e);
+            return AjaxResult.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 模板状态操作 ====================
+    
+    @PostMapping("/{id}/publish")
+    @Operation(summary = "发布模板", description = "将模板状态改为已发布")
+    public AjaxResult<?> publishTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id) {
+        try {
+            Template template = templateService.publish(id);
+            if (template == null) {
+                return AjaxResult.error("模板不存在");
+            }
+            return AjaxResult.success("发布成功", TemplateDetailResponse.fromEntity(template));
+        } catch (Exception e) {
+            log.error("发布模板失败", e);
+            return AjaxResult.error("发布失败: " + e.getMessage());
+        }
+    }
+    
+    @PostMapping("/{id}/archive")
+    @Operation(summary = "归档模板", description = "将模板状态改为已归档")
+    public AjaxResult<?> archiveTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id) {
+        try {
+            Template template = templateService.archive(id);
+            if (template == null) {
+                return AjaxResult.error("模板不存在");
+            }
+            return AjaxResult.success("归档成功", TemplateDetailResponse.fromEntity(template));
+        } catch (Exception e) {
+            log.error("归档模板失败", e);
+            return AjaxResult.error("归档失败: " + e.getMessage());
+        }
+    }
+    
+    @PostMapping("/{id}/duplicate")
+    @Operation(summary = "复制模板", description = "复制模板及其来源文件和变量")
+    public AjaxResult<?> duplicateTemplate(
+            @Parameter(description = "模板ID") @PathVariable String id,
+            @Parameter(description = "新模板名称") @RequestParam(required = false) String newName) {
+        try {
+            String userId = getCurrentUserId();
+            Template template = templateService.duplicate(id, userId, newName);
+            if (template == null) {
+                return AjaxResult.error("模板不存在");
+            }
+            return AjaxResult.success("复制成功", TemplateDetailResponse.fromEntity(template));
+        } catch (Exception e) {
+            log.error("复制模板失败", e);
+            return AjaxResult.error("复制失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 来源文件管理 ====================
+    
+    @PostMapping("/{templateId}/source-files")
+    @Operation(summary = "添加来源文件定义", description = "为模板添加来源文件定义")
+    public AjaxResult<?> addSourceFile(
+            @Parameter(description = "模板ID") @PathVariable String templateId,
+            @Valid @RequestBody AddSourceFileRequest request) {
+        try {
+            SourceFile sourceFile = sourceFileService.add(templateId, request);
+            return AjaxResult.success("添加成功", SourceFileResponse.fromEntity(sourceFile));
+        } catch (Exception e) {
+            log.error("添加来源文件定义失败", e);
+            return AjaxResult.error("添加失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{templateId}/source-files")
+    @Operation(summary = "获取来源文件定义列表", description = "获取模板的所有来源文件定义")
+    public AjaxResult<?> listSourceFiles(
+            @Parameter(description = "模板ID") @PathVariable String templateId) {
+        List<SourceFile> sourceFiles = sourceFileService.listByTemplateId(templateId);
+        List<SourceFileResponse> responses = sourceFiles.stream()
+                .map(SourceFileResponse::fromEntity)
+                .collect(Collectors.toList());
+        return AjaxResult.success(responses);
+    }
+    
+    @PutMapping("/source-files/{id}")
+    @Operation(summary = "更新来源文件定义", description = "更新来源文件定义")
+    public AjaxResult<?> updateSourceFile(
+            @Parameter(description = "来源文件定义ID") @PathVariable String id,
+            @Valid @RequestBody AddSourceFileRequest request) {
+        try {
+            SourceFile sourceFile = sourceFileService.update(id, request);
+            if (sourceFile == null) {
+                return AjaxResult.error("来源文件定义不存在");
+            }
+            return AjaxResult.success("更新成功", SourceFileResponse.fromEntity(sourceFile));
+        } catch (Exception e) {
+            log.error("更新来源文件定义失败", e);
+            return AjaxResult.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    @DeleteMapping("/source-files/{id}")
+    @Operation(summary = "删除来源文件定义", description = "删除来源文件定义")
+    public AjaxResult<?> deleteSourceFile(
+            @Parameter(description = "来源文件定义ID") @PathVariable String id,
+            @Parameter(description = "是否强制删除") @RequestParam(defaultValue = "false") boolean force) {
+        try {
+            boolean success = force ? sourceFileService.forceDelete(id) : sourceFileService.delete(id);
+            if (!success) {
+                return AjaxResult.error("来源文件定义不存在");
+            }
+            return AjaxResult.success("删除成功");
+        } catch (Exception e) {
+            log.error("删除来源文件定义失败", e);
+            return AjaxResult.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 变量管理 ====================
+    
+    @PostMapping("/{templateId}/variables")
+    @Operation(summary = "添加变量", description = "为模板添加变量")
+    public AjaxResult<?> addVariable(
+            @Parameter(description = "模板ID") @PathVariable String templateId,
+            @Valid @RequestBody AddVariableRequest request) {
+        try {
+            Variable variable = variableService.add(templateId, request);
+            return AjaxResult.success("添加成功", VariableResponse.fromEntity(variable));
+        } catch (Exception e) {
+            log.error("添加变量失败", e);
+            return AjaxResult.error("添加失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{templateId}/variables")
+    @Operation(summary = "获取变量列表", description = "获取模板的所有变量")
+    public AjaxResult<?> listVariables(
+            @Parameter(description = "模板ID") @PathVariable String templateId,
+            @Parameter(description = "来源文件别名过滤") @RequestParam(required = false) String sourceFileAlias,
+            @Parameter(description = "变量分组过滤") @RequestParam(required = false) String group) {
+        List<Variable> variables;
+        
+        if (sourceFileAlias != null) {
+            variables = variableService.listBySourceFileAlias(templateId, sourceFileAlias);
+        } else if (group != null) {
+            variables = variableService.listByGroup(templateId, group);
+        } else {
+            variables = variableService.listByTemplateId(templateId);
+        }
+        
+        List<VariableResponse> responses = variables.stream()
+                .map(VariableResponse::fromEntity)
+                .collect(Collectors.toList());
+        return AjaxResult.success(responses);
+    }
+    
+    @GetMapping("/variables/{id}")
+    @Operation(summary = "获取变量详情", description = "获取变量详情")
+    public AjaxResult<?> getVariable(
+            @Parameter(description = "变量ID") @PathVariable String id) {
+        Variable variable = variableService.getById(id);
+        if (variable == null) {
+            return AjaxResult.error("变量不存在");
+        }
+        return AjaxResult.success(VariableResponse.fromEntity(variable));
+    }
+    
+    @PutMapping("/variables/{id}")
+    @Operation(summary = "更新变量", description = "更新变量")
+    public AjaxResult<?> updateVariable(
+            @Parameter(description = "变量ID") @PathVariable String id,
+            @Valid @RequestBody AddVariableRequest request) {
+        try {
+            Variable variable = variableService.update(id, request);
+            if (variable == null) {
+                return AjaxResult.error("变量不存在");
+            }
+            return AjaxResult.success("更新成功", VariableResponse.fromEntity(variable));
+        } catch (Exception e) {
+            log.error("更新变量失败", e);
+            return AjaxResult.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    @DeleteMapping("/variables/{id}")
+    @Operation(summary = "删除变量", description = "删除变量")
+    public AjaxResult<?> deleteVariable(
+            @Parameter(description = "变量ID") @PathVariable String id,
+            @Parameter(description = "是否强制删除") @RequestParam(defaultValue = "false") boolean force) {
+        try {
+            boolean success = force ? variableService.forceDelete(id) : variableService.delete(id);
+            if (!success) {
+                return AjaxResult.error("变量不存在");
+            }
+            return AjaxResult.success("删除成功");
+        } catch (Exception e) {
+            log.error("删除变量失败", e);
+            return AjaxResult.error("删除失败: " + e.getMessage());
+        }
+    }
+}

+ 34 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/AddSourceFileRequest.java

@@ -0,0 +1,34 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 添加来源文件定义请求
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "添加来源文件定义请求")
+public class AddSourceFileRequest {
+    
+    @NotBlank(message = "文件别名不能为空")
+    @Schema(description = "文件别名,如'可研批复'", required = true)
+    private String alias;
+    
+    @Schema(description = "文件说明")
+    private String description;
+    
+    @Schema(description = "允许的文件类型", defaultValue = "[\"pdf\", \"docx\"]")
+    private List<String> fileTypes;
+    
+    @Schema(description = "是否必须", defaultValue = "true")
+    private Boolean required = true;
+    
+    @Schema(description = "示例文件文档ID")
+    private String exampleDocumentId;
+}

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

@@ -0,0 +1,57 @@
+package com.lingyue.extract.dto.request;
+
+import com.lingyue.extract.dto.config.VariableLocation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 添加变量请求
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "添加变量请求")
+public class AddVariableRequest {
+    
+    @NotBlank(message = "变量名不能为空")
+    @Schema(description = "变量名(程序用),模板内唯一", required = true)
+    private String name;
+    
+    @NotBlank(message = "显示名称不能为空")
+    @Schema(description = "显示名称", required = true)
+    private String displayName;
+    
+    @Schema(description = "变量分组")
+    private String variableGroup;
+    
+    @NotNull(message = "变量位置不能为空")
+    @Schema(description = "变量在文档中的位置", required = true)
+    private VariableLocation location;
+    
+    @Schema(description = "示例值(原文档中的值)")
+    private String exampleValue;
+    
+    @Schema(description = "值类型: text/date/number/table", defaultValue = "text")
+    private String valueType = "text";
+    
+    @Schema(description = "来源文件别名")
+    private String sourceFileAlias;
+    
+    @NotBlank(message = "来源类型不能为空")
+    @Schema(description = "来源类型: document/manual/reference/fixed", required = true)
+    private String sourceType;
+    
+    @Schema(description = "来源配置")
+    private Map<String, Object> sourceConfig;
+    
+    @Schema(description = "提取类型: direct/ai_extract/ai_summarize")
+    private String extractType;
+    
+    @Schema(description = "提取配置")
+    private Map<String, Object> extractConfig;
+}

+ 35 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/CreateTemplateRequest.java

@@ -0,0 +1,35 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 创建模板请求
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "创建模板请求")
+public class CreateTemplateRequest {
+    
+    @NotBlank(message = "模板名称不能为空")
+    @Schema(description = "模板名称", required = true)
+    private String name;
+    
+    @Schema(description = "模板描述")
+    private String description;
+    
+    @NotBlank(message = "示例报告文档ID不能为空")
+    @Schema(description = "示例报告文档ID", required = true)
+    private String baseDocumentId;
+    
+    @Schema(description = "模板配置")
+    private Map<String, Object> config;
+    
+    @Schema(description = "是否公开", defaultValue = "false")
+    private Boolean isPublic = false;
+}

+ 29 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/UpdateTemplateRequest.java

@@ -0,0 +1,29 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 更新模板请求
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "更新模板请求")
+public class UpdateTemplateRequest {
+    
+    @Schema(description = "模板名称")
+    private String name;
+    
+    @Schema(description = "模板描述")
+    private String description;
+    
+    @Schema(description = "模板配置")
+    private Map<String, Object> config;
+    
+    @Schema(description = "是否公开")
+    private Boolean isPublic;
+}

+ 68 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/SourceFileResponse.java

@@ -0,0 +1,68 @@
+package com.lingyue.extract.dto.response;
+
+import com.lingyue.extract.entity.SourceFile;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 来源文件响应
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "来源文件响应")
+public class SourceFileResponse {
+    
+    @Schema(description = "ID")
+    private String id;
+    
+    @Schema(description = "模板ID")
+    private String templateId;
+    
+    @Schema(description = "文件别名")
+    private String alias;
+    
+    @Schema(description = "文件说明")
+    private String description;
+    
+    @Schema(description = "允许的文件类型")
+    private List<String> fileTypes;
+    
+    @Schema(description = "是否必须")
+    private Boolean required;
+    
+    @Schema(description = "示例文件文档ID")
+    private String exampleDocumentId;
+    
+    @Schema(description = "显示顺序")
+    private Integer displayOrder;
+    
+    @Schema(description = "创建时间")
+    private Date createTime;
+    
+    // ==================== 统计信息 ====================
+    
+    @Schema(description = "关联的变量数量")
+    private Integer variableCount;
+    
+    /**
+     * 从实体转换
+     */
+    public static SourceFileResponse fromEntity(SourceFile sourceFile) {
+        SourceFileResponse response = new SourceFileResponse();
+        response.setId(sourceFile.getId());
+        response.setTemplateId(sourceFile.getTemplateId());
+        response.setAlias(sourceFile.getAlias());
+        response.setDescription(sourceFile.getDescription());
+        response.setFileTypes(sourceFile.getFileTypes());
+        response.setRequired(sourceFile.getRequired());
+        response.setExampleDocumentId(sourceFile.getExampleDocumentId());
+        response.setDisplayOrder(sourceFile.getDisplayOrder());
+        response.setCreateTime(sourceFile.getCreateTime());
+        return response;
+    }
+}

+ 93 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/TemplateDetailResponse.java

@@ -0,0 +1,93 @@
+package com.lingyue.extract.dto.response;
+
+import com.lingyue.extract.entity.SourceFile;
+import com.lingyue.extract.entity.Template;
+import com.lingyue.extract.entity.Variable;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 模板详情响应
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "模板详情响应")
+public class TemplateDetailResponse {
+    
+    @Schema(description = "模板ID")
+    private String id;
+    
+    @Schema(description = "用户ID")
+    private String userId;
+    
+    @Schema(description = "模板名称")
+    private String name;
+    
+    @Schema(description = "模板描述")
+    private String description;
+    
+    @Schema(description = "示例报告文档ID")
+    private String baseDocumentId;
+    
+    @Schema(description = "状态: draft/published/archived")
+    private String status;
+    
+    @Schema(description = "模板配置")
+    private Map<String, Object> config;
+    
+    @Schema(description = "是否公开")
+    private Boolean isPublic;
+    
+    @Schema(description = "使用次数")
+    private Integer useCount;
+    
+    @Schema(description = "创建时间")
+    private Date createTime;
+    
+    @Schema(description = "更新时间")
+    private Date updateTime;
+    
+    // ==================== 关联数据 ====================
+    
+    @Schema(description = "来源文件定义列表")
+    private List<SourceFile> sourceFiles;
+    
+    @Schema(description = "变量列表")
+    private List<Variable> variables;
+    
+    // ==================== 统计信息 ====================
+    
+    @Schema(description = "来源文件数量")
+    private Integer sourceFileCount;
+    
+    @Schema(description = "变量数量")
+    private Integer variableCount;
+    
+    @Schema(description = "生成次数")
+    private Integer generationCount;
+    
+    /**
+     * 从实体转换
+     */
+    public static TemplateDetailResponse fromEntity(Template template) {
+        TemplateDetailResponse response = new TemplateDetailResponse();
+        response.setId(template.getId());
+        response.setUserId(template.getUserId());
+        response.setName(template.getName());
+        response.setDescription(template.getDescription());
+        response.setBaseDocumentId(template.getBaseDocumentId());
+        response.setStatus(template.getStatus());
+        response.setConfig(template.getConfig());
+        response.setIsPublic(template.getIsPublic());
+        response.setUseCount(template.getUseCount());
+        response.setCreateTime(template.getCreateTime());
+        response.setUpdateTime(template.getUpdateTime());
+        return response;
+    }
+}

+ 64 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/TemplateListResponse.java

@@ -0,0 +1,64 @@
+package com.lingyue.extract.dto.response;
+
+import com.lingyue.extract.entity.Template;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 模板列表项响应
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "模板列表项响应")
+public class TemplateListResponse {
+    
+    @Schema(description = "模板ID")
+    private String id;
+    
+    @Schema(description = "模板名称")
+    private String name;
+    
+    @Schema(description = "模板描述")
+    private String description;
+    
+    @Schema(description = "状态: draft/published/archived")
+    private String status;
+    
+    @Schema(description = "是否公开")
+    private Boolean isPublic;
+    
+    @Schema(description = "使用次数")
+    private Integer useCount;
+    
+    @Schema(description = "来源文件数量")
+    private Integer sourceFileCount;
+    
+    @Schema(description = "变量数量")
+    private Integer variableCount;
+    
+    @Schema(description = "创建时间")
+    private Date createTime;
+    
+    @Schema(description = "更新时间")
+    private Date updateTime;
+    
+    /**
+     * 从实体转换
+     */
+    public static TemplateListResponse fromEntity(Template template) {
+        TemplateListResponse response = new TemplateListResponse();
+        response.setId(template.getId());
+        response.setName(template.getName());
+        response.setDescription(template.getDescription());
+        response.setStatus(template.getStatus());
+        response.setIsPublic(template.getIsPublic());
+        response.setUseCount(template.getUseCount());
+        response.setCreateTime(template.getCreateTime());
+        response.setUpdateTime(template.getUpdateTime());
+        return response;
+    }
+}

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

@@ -0,0 +1,92 @@
+package com.lingyue.extract.dto.response;
+
+import com.lingyue.extract.dto.config.VariableLocation;
+import com.lingyue.extract.entity.Variable;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 变量响应
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Data
+@Schema(description = "变量响应")
+public class VariableResponse {
+    
+    @Schema(description = "ID")
+    private String id;
+    
+    @Schema(description = "模板ID")
+    private String templateId;
+    
+    @Schema(description = "变量名")
+    private String name;
+    
+    @Schema(description = "显示名称")
+    private String displayName;
+    
+    @Schema(description = "变量分组")
+    private String variableGroup;
+    
+    @Schema(description = "变量位置")
+    private VariableLocation location;
+    
+    @Schema(description = "示例值")
+    private String exampleValue;
+    
+    @Schema(description = "值类型")
+    private String valueType;
+    
+    @Schema(description = "来源文件别名")
+    private String sourceFileAlias;
+    
+    @Schema(description = "来源类型")
+    private String sourceType;
+    
+    @Schema(description = "来源配置")
+    private Map<String, Object> sourceConfig;
+    
+    @Schema(description = "提取类型")
+    private String extractType;
+    
+    @Schema(description = "提取配置")
+    private Map<String, Object> extractConfig;
+    
+    @Schema(description = "显示顺序")
+    private Integer displayOrder;
+    
+    @Schema(description = "创建时间")
+    private Date createTime;
+    
+    @Schema(description = "更新时间")
+    private Date updateTime;
+    
+    /**
+     * 从实体转换
+     */
+    public static VariableResponse fromEntity(Variable variable) {
+        VariableResponse response = new VariableResponse();
+        response.setId(variable.getId());
+        response.setTemplateId(variable.getTemplateId());
+        response.setName(variable.getName());
+        response.setDisplayName(variable.getDisplayName());
+        response.setVariableGroup(variable.getVariableGroup());
+        response.setLocation(variable.getLocation());
+        response.setExampleValue(variable.getExampleValue());
+        response.setValueType(variable.getValueType());
+        response.setSourceFileAlias(variable.getSourceFileAlias());
+        response.setSourceType(variable.getSourceType());
+        response.setSourceConfig(variable.getSourceConfig());
+        response.setExtractType(variable.getExtractType());
+        response.setExtractConfig(variable.getExtractConfig());
+        response.setDisplayOrder(variable.getDisplayOrder());
+        response.setCreateTime(variable.getCreateTime());
+        response.setUpdateTime(variable.getUpdateTime());
+        return response;
+    }
+}

+ 203 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/SourceFileService.java

@@ -0,0 +1,203 @@
+package com.lingyue.extract.service;
+
+import com.lingyue.extract.dto.request.AddSourceFileRequest;
+import com.lingyue.extract.entity.SourceFile;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.SourceFileRepository;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 来源文件定义服务
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SourceFileService {
+    
+    private final SourceFileRepository sourceFileRepository;
+    private final VariableRepository variableRepository;
+    
+    /**
+     * 添加来源文件定义
+     */
+    @Transactional
+    public SourceFile add(String templateId, AddSourceFileRequest request) {
+        // 检查别名是否重复
+        SourceFile existing = sourceFileRepository.findByTemplateIdAndAlias(templateId, request.getAlias());
+        if (existing != null) {
+            throw new RuntimeException("来源文件别名已存在: " + request.getAlias());
+        }
+        
+        SourceFile sourceFile = new SourceFile();
+        sourceFile.setId(UUID.randomUUID().toString().replace("-", ""));
+        sourceFile.setTemplateId(templateId);
+        sourceFile.setAlias(request.getAlias());
+        sourceFile.setDescription(request.getDescription());
+        sourceFile.setFileTypes(request.getFileTypes() != null ? request.getFileTypes() : Arrays.asList("pdf", "docx"));
+        sourceFile.setRequired(request.getRequired() != null ? request.getRequired() : true);
+        sourceFile.setExampleDocumentId(request.getExampleDocumentId());
+        
+        // 设置显示顺序
+        int maxOrder = sourceFileRepository.getMaxDisplayOrder(templateId);
+        sourceFile.setDisplayOrder(maxOrder + 1);
+        sourceFile.setCreateTime(new Date());
+        
+        sourceFileRepository.insert(sourceFile);
+        log.info("添加来源文件定义成功: templateId={}, alias={}", templateId, request.getAlias());
+        
+        return sourceFile;
+    }
+    
+    /**
+     * 批量添加来源文件定义
+     */
+    @Transactional
+    public List<SourceFile> batchAdd(String templateId, List<AddSourceFileRequest> requests) {
+        return requests.stream()
+                .map(request -> add(templateId, request))
+                .toList();
+    }
+    
+    /**
+     * 根据ID获取
+     */
+    public SourceFile getById(String id) {
+        return sourceFileRepository.selectById(id);
+    }
+    
+    /**
+     * 获取模板的所有来源文件定义
+     */
+    public List<SourceFile> listByTemplateId(String templateId) {
+        return sourceFileRepository.findByTemplateId(templateId);
+    }
+    
+    /**
+     * 根据别名获取
+     */
+    public SourceFile getByAlias(String templateId, String alias) {
+        return sourceFileRepository.findByTemplateIdAndAlias(templateId, alias);
+    }
+    
+    /**
+     * 更新来源文件定义
+     */
+    @Transactional
+    public SourceFile update(String id, AddSourceFileRequest request) {
+        SourceFile sourceFile = sourceFileRepository.selectById(id);
+        if (sourceFile == null) {
+            return null;
+        }
+        
+        // 检查别名是否重复(排除自己)
+        if (request.getAlias() != null && !request.getAlias().equals(sourceFile.getAlias())) {
+            SourceFile existing = sourceFileRepository.findByTemplateIdAndAlias(sourceFile.getTemplateId(), request.getAlias());
+            if (existing != null) {
+                throw new RuntimeException("来源文件别名已存在: " + request.getAlias());
+            }
+            
+            // 更新关联变量的别名
+            List<Variable> variables = variableRepository.findByTemplateIdAndSourceFileAlias(
+                    sourceFile.getTemplateId(), sourceFile.getAlias());
+            for (Variable v : variables) {
+                v.setSourceFileAlias(request.getAlias());
+                v.setUpdateTime(new Date());
+                variableRepository.updateById(v);
+            }
+            
+            sourceFile.setAlias(request.getAlias());
+        }
+        
+        if (request.getDescription() != null) {
+            sourceFile.setDescription(request.getDescription());
+        }
+        if (request.getFileTypes() != null) {
+            sourceFile.setFileTypes(request.getFileTypes());
+        }
+        if (request.getRequired() != null) {
+            sourceFile.setRequired(request.getRequired());
+        }
+        if (request.getExampleDocumentId() != null) {
+            sourceFile.setExampleDocumentId(request.getExampleDocumentId());
+        }
+        
+        sourceFileRepository.updateById(sourceFile);
+        log.info("更新来源文件定义成功: id={}", id);
+        
+        return sourceFile;
+    }
+    
+    /**
+     * 删除来源文件定义
+     */
+    @Transactional
+    public boolean delete(String id) {
+        SourceFile sourceFile = sourceFileRepository.selectById(id);
+        if (sourceFile == null) {
+            return false;
+        }
+        
+        // 检查是否有变量引用
+        List<Variable> variables = variableRepository.findByTemplateIdAndSourceFileAlias(
+                sourceFile.getTemplateId(), sourceFile.getAlias());
+        if (!variables.isEmpty()) {
+            throw new RuntimeException("来源文件被 " + variables.size() + " 个变量引用,无法删除");
+        }
+        
+        sourceFileRepository.deleteById(id);
+        log.info("删除来源文件定义成功: id={}", id);
+        
+        return true;
+    }
+    
+    /**
+     * 强制删除(同时清除变量的引用)
+     */
+    @Transactional
+    public boolean forceDelete(String id) {
+        SourceFile sourceFile = sourceFileRepository.selectById(id);
+        if (sourceFile == null) {
+            return false;
+        }
+        
+        // 清除变量的来源文件引用
+        List<Variable> variables = variableRepository.findByTemplateIdAndSourceFileAlias(
+                sourceFile.getTemplateId(), sourceFile.getAlias());
+        for (Variable v : variables) {
+            v.setSourceFileAlias(null);
+            v.setUpdateTime(new Date());
+            variableRepository.updateById(v);
+        }
+        
+        sourceFileRepository.deleteById(id);
+        log.info("强制删除来源文件定义成功: id={}, clearedVariables={}", id, variables.size());
+        
+        return true;
+    }
+    
+    /**
+     * 获取必须的来源文件定义
+     */
+    public List<SourceFile> listRequired(String templateId) {
+        return sourceFileRepository.findRequiredByTemplateId(templateId);
+    }
+    
+    /**
+     * 统计数量
+     */
+    public int count(String templateId) {
+        return sourceFileRepository.countByTemplateId(templateId);
+    }
+}

+ 345 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/TemplateService.java

@@ -0,0 +1,345 @@
+package com.lingyue.extract.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.lingyue.extract.dto.request.CreateTemplateRequest;
+import com.lingyue.extract.dto.request.UpdateTemplateRequest;
+import com.lingyue.extract.dto.response.TemplateDetailResponse;
+import com.lingyue.extract.dto.response.TemplateListResponse;
+import com.lingyue.extract.entity.Generation;
+import com.lingyue.extract.entity.SourceFile;
+import com.lingyue.extract.entity.Template;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.GenerationRepository;
+import com.lingyue.extract.repository.SourceFileRepository;
+import com.lingyue.extract.repository.TemplateRepository;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * 模板服务
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TemplateService {
+    
+    private final TemplateRepository templateRepository;
+    private final SourceFileRepository sourceFileRepository;
+    private final VariableRepository variableRepository;
+    private final GenerationRepository generationRepository;
+    
+    /**
+     * 创建模板
+     */
+    @Transactional
+    public Template create(String userId, CreateTemplateRequest request) {
+        Template template = new Template();
+        template.setId(UUID.randomUUID().toString().replace("-", ""));
+        template.setUserId(userId);
+        template.setName(request.getName());
+        template.setDescription(request.getDescription());
+        template.setBaseDocumentId(request.getBaseDocumentId());
+        template.setStatus(Template.STATUS_DRAFT);
+        template.setConfig(request.getConfig());
+        template.setIsPublic(request.getIsPublic() != null ? request.getIsPublic() : false);
+        template.setUseCount(0);
+        template.setCreateTime(new Date());
+        template.setUpdateTime(new Date());
+        
+        templateRepository.insert(template);
+        log.info("创建模板成功: id={}, name={}, userId={}", template.getId(), template.getName(), userId);
+        
+        return template;
+    }
+    
+    /**
+     * 根据ID获取模板
+     */
+    public Template getById(String id) {
+        return templateRepository.selectById(id);
+    }
+    
+    /**
+     * 获取模板详情(包含来源文件和变量)
+     */
+    public TemplateDetailResponse getTemplateDetail(String id) {
+        Template template = templateRepository.selectById(id);
+        if (template == null) {
+            return null;
+        }
+        
+        TemplateDetailResponse response = TemplateDetailResponse.fromEntity(template);
+        
+        // 获取来源文件列表
+        List<SourceFile> sourceFiles = sourceFileRepository.findByTemplateId(id);
+        response.setSourceFiles(sourceFiles);
+        response.setSourceFileCount(sourceFiles.size());
+        
+        // 获取变量列表
+        List<Variable> variables = variableRepository.findByTemplateId(id);
+        response.setVariables(variables);
+        response.setVariableCount(variables.size());
+        
+        // 获取生成次数
+        int generationCount = generationRepository.countByTemplateId(id);
+        response.setGenerationCount(generationCount);
+        
+        return response;
+    }
+    
+    /**
+     * 分页查询用户的模板
+     */
+    public Page<TemplateListResponse> listByUserId(String userId, int page, int size) {
+        Page<Template> pageRequest = new Page<>(page, size);
+        
+        LambdaQueryWrapper<Template> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Template::getUserId, userId)
+               .orderByDesc(Template::getCreateTime);
+        
+        Page<Template> templatePage = templateRepository.selectPage(pageRequest, wrapper);
+        
+        // 转换为响应对象
+        Page<TemplateListResponse> responsePage = new Page<>(page, size, templatePage.getTotal());
+        List<TemplateListResponse> records = templatePage.getRecords().stream()
+                .map(template -> {
+                    TemplateListResponse response = TemplateListResponse.fromEntity(template);
+                    // 填充统计信息
+                    response.setSourceFileCount(sourceFileRepository.countByTemplateId(template.getId()));
+                    response.setVariableCount(variableRepository.countByTemplateId(template.getId()));
+                    return response;
+                })
+                .collect(Collectors.toList());
+        responsePage.setRecords(records);
+        
+        return responsePage;
+    }
+    
+    /**
+     * 查询用户可见的模板(自己的 + 公开的)
+     */
+    public List<TemplateListResponse> listAccessible(String userId) {
+        List<Template> templates = templateRepository.findAccessibleByUserId(userId);
+        return templates.stream()
+                .map(template -> {
+                    TemplateListResponse response = TemplateListResponse.fromEntity(template);
+                    response.setSourceFileCount(sourceFileRepository.countByTemplateId(template.getId()));
+                    response.setVariableCount(variableRepository.countByTemplateId(template.getId()));
+                    return response;
+                })
+                .collect(Collectors.toList());
+    }
+    
+    /**
+     * 搜索模板
+     */
+    public List<TemplateListResponse> search(String userId, String keyword) {
+        List<Template> templates = templateRepository.searchByName(userId, keyword);
+        return templates.stream()
+                .map(TemplateListResponse::fromEntity)
+                .collect(Collectors.toList());
+    }
+    
+    /**
+     * 更新模板
+     */
+    @Transactional
+    public Template update(String id, UpdateTemplateRequest request) {
+        Template template = templateRepository.selectById(id);
+        if (template == null) {
+            return null;
+        }
+        
+        if (request.getName() != null) {
+            template.setName(request.getName());
+        }
+        if (request.getDescription() != null) {
+            template.setDescription(request.getDescription());
+        }
+        if (request.getConfig() != null) {
+            template.setConfig(request.getConfig());
+        }
+        if (request.getIsPublic() != null) {
+            template.setIsPublic(request.getIsPublic());
+        }
+        template.setUpdateTime(new Date());
+        
+        templateRepository.updateById(template);
+        log.info("更新模板成功: id={}", id);
+        
+        return template;
+    }
+    
+    /**
+     * 删除模板(级联删除来源文件、变量)
+     */
+    @Transactional
+    public boolean delete(String id) {
+        Template template = templateRepository.selectById(id);
+        if (template == null) {
+            return false;
+        }
+        
+        // 检查是否有生成任务
+        int generationCount = generationRepository.countByTemplateId(id);
+        if (generationCount > 0) {
+            log.warn("模板有关联的生成任务,无法删除: id={}, generationCount={}", id, generationCount);
+            throw new RuntimeException("模板有关联的生成任务,无法删除。如需删除,请先删除相关生成任务。");
+        }
+        
+        // 级联删除变量
+        variableRepository.deleteByTemplateId(id);
+        
+        // 级联删除来源文件
+        sourceFileRepository.deleteByTemplateId(id);
+        
+        // 删除模板
+        templateRepository.deleteById(id);
+        log.info("删除模板成功(级联删除): id={}", id);
+        
+        return true;
+    }
+    
+    /**
+     * 发布模板
+     */
+    @Transactional
+    public Template publish(String id) {
+        Template template = templateRepository.selectById(id);
+        if (template == null) {
+            return null;
+        }
+        
+        // 检查是否有变量
+        int variableCount = variableRepository.countByTemplateId(id);
+        if (variableCount == 0) {
+            throw new RuntimeException("模板没有定义变量,无法发布");
+        }
+        
+        template.setStatus(Template.STATUS_PUBLISHED);
+        template.setUpdateTime(new Date());
+        templateRepository.updateById(template);
+        
+        log.info("发布模板成功: id={}", id);
+        return template;
+    }
+    
+    /**
+     * 归档模板
+     */
+    @Transactional
+    public Template archive(String id) {
+        Template template = templateRepository.selectById(id);
+        if (template == null) {
+            return null;
+        }
+        
+        template.setStatus(Template.STATUS_ARCHIVED);
+        template.setUpdateTime(new Date());
+        templateRepository.updateById(template);
+        
+        log.info("归档模板成功: id={}", id);
+        return template;
+    }
+    
+    /**
+     * 复制模板
+     */
+    @Transactional
+    public Template duplicate(String id, String userId, String newName) {
+        Template source = templateRepository.selectById(id);
+        if (source == null) {
+            return null;
+        }
+        
+        // 创建新模板
+        Template newTemplate = new Template();
+        newTemplate.setId(UUID.randomUUID().toString().replace("-", ""));
+        newTemplate.setUserId(userId);
+        newTemplate.setName(newName != null ? newName : source.getName() + " (副本)");
+        newTemplate.setDescription(source.getDescription());
+        newTemplate.setBaseDocumentId(source.getBaseDocumentId());
+        newTemplate.setStatus(Template.STATUS_DRAFT);
+        newTemplate.setConfig(source.getConfig());
+        newTemplate.setIsPublic(false);
+        newTemplate.setUseCount(0);
+        newTemplate.setCreateTime(new Date());
+        newTemplate.setUpdateTime(new Date());
+        
+        templateRepository.insert(newTemplate);
+        
+        // 复制来源文件
+        List<SourceFile> sourceFiles = sourceFileRepository.findByTemplateId(id);
+        for (SourceFile sf : sourceFiles) {
+            SourceFile newSf = new SourceFile();
+            newSf.setId(UUID.randomUUID().toString().replace("-", ""));
+            newSf.setTemplateId(newTemplate.getId());
+            newSf.setAlias(sf.getAlias());
+            newSf.setDescription(sf.getDescription());
+            newSf.setFileTypes(sf.getFileTypes());
+            newSf.setRequired(sf.getRequired());
+            newSf.setExampleDocumentId(sf.getExampleDocumentId());
+            newSf.setDisplayOrder(sf.getDisplayOrder());
+            newSf.setCreateTime(new Date());
+            sourceFileRepository.insert(newSf);
+        }
+        
+        // 复制变量
+        List<Variable> variables = variableRepository.findByTemplateId(id);
+        for (Variable v : variables) {
+            Variable newV = new Variable();
+            newV.setId(UUID.randomUUID().toString().replace("-", ""));
+            newV.setTemplateId(newTemplate.getId());
+            newV.setName(v.getName());
+            newV.setDisplayName(v.getDisplayName());
+            newV.setVariableGroup(v.getVariableGroup());
+            newV.setLocation(v.getLocation());
+            newV.setExampleValue(v.getExampleValue());
+            newV.setValueType(v.getValueType());
+            newV.setSourceFileAlias(v.getSourceFileAlias());
+            newV.setSourceType(v.getSourceType());
+            newV.setSourceConfig(v.getSourceConfig());
+            newV.setExtractType(v.getExtractType());
+            newV.setExtractConfig(v.getExtractConfig());
+            newV.setDisplayOrder(v.getDisplayOrder());
+            newV.setCreateTime(new Date());
+            newV.setUpdateTime(new Date());
+            variableRepository.insert(newV);
+        }
+        
+        log.info("复制模板成功: sourceId={}, newId={}", id, newTemplate.getId());
+        return newTemplate;
+    }
+    
+    /**
+     * 增加使用次数
+     */
+    public void incrementUseCount(String id) {
+        templateRepository.incrementUseCount(id);
+    }
+    
+    /**
+     * 检查用户是否有访问权限
+     */
+    public boolean hasAccess(String templateId, String userId) {
+        Template template = templateRepository.selectById(templateId);
+        if (template == null) {
+            return false;
+        }
+        // 自己的模板或公开的已发布模板
+        return template.getUserId().equals(userId) || 
+               (Boolean.TRUE.equals(template.getIsPublic()) && Template.STATUS_PUBLISHED.equals(template.getStatus()));
+    }
+}

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

@@ -0,0 +1,227 @@
+package com.lingyue.extract.service;
+
+import com.lingyue.extract.dto.request.AddVariableRequest;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 变量服务
+ * 
+ * @author lingyue
+ * @since 2026-01-23
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class VariableService {
+    
+    private final VariableRepository variableRepository;
+    
+    /**
+     * 添加变量
+     */
+    @Transactional
+    public Variable add(String templateId, AddVariableRequest request) {
+        // 检查变量名是否重复
+        Variable existing = variableRepository.findByTemplateIdAndName(templateId, request.getName());
+        if (existing != null) {
+            throw new RuntimeException("变量名已存在: " + request.getName());
+        }
+        
+        Variable variable = new Variable();
+        variable.setId(UUID.randomUUID().toString().replace("-", ""));
+        variable.setTemplateId(templateId);
+        variable.setName(request.getName());
+        variable.setDisplayName(request.getDisplayName());
+        variable.setVariableGroup(request.getVariableGroup());
+        variable.setLocation(request.getLocation());
+        variable.setExampleValue(request.getExampleValue());
+        variable.setValueType(request.getValueType() != null ? request.getValueType() : Variable.VALUE_TYPE_TEXT);
+        variable.setSourceFileAlias(request.getSourceFileAlias());
+        variable.setSourceType(request.getSourceType());
+        variable.setSourceConfig(request.getSourceConfig());
+        variable.setExtractType(request.getExtractType());
+        variable.setExtractConfig(request.getExtractConfig());
+        
+        // 设置显示顺序
+        int maxOrder = variableRepository.getMaxDisplayOrder(templateId);
+        variable.setDisplayOrder(maxOrder + 1);
+        variable.setCreateTime(new Date());
+        variable.setUpdateTime(new Date());
+        
+        variableRepository.insert(variable);
+        log.info("添加变量成功: templateId={}, name={}", templateId, request.getName());
+        
+        return variable;
+    }
+    
+    /**
+     * 批量添加变量
+     */
+    @Transactional
+    public List<Variable> batchAdd(String templateId, List<AddVariableRequest> requests) {
+        return requests.stream()
+                .map(request -> add(templateId, request))
+                .toList();
+    }
+    
+    /**
+     * 根据ID获取
+     */
+    public Variable getById(String id) {
+        return variableRepository.selectById(id);
+    }
+    
+    /**
+     * 根据变量名获取
+     */
+    public Variable getByName(String templateId, String name) {
+        return variableRepository.findByTemplateIdAndName(templateId, name);
+    }
+    
+    /**
+     * 获取模板的所有变量
+     */
+    public List<Variable> listByTemplateId(String templateId) {
+        return variableRepository.findByTemplateId(templateId);
+    }
+    
+    /**
+     * 按来源文件别名获取变量
+     */
+    public List<Variable> listBySourceFileAlias(String templateId, String alias) {
+        return variableRepository.findByTemplateIdAndSourceFileAlias(templateId, alias);
+    }
+    
+    /**
+     * 按来源类型获取变量
+     */
+    public List<Variable> listBySourceType(String templateId, String sourceType) {
+        return variableRepository.findByTemplateIdAndSourceType(templateId, sourceType);
+    }
+    
+    /**
+     * 按分组获取变量
+     */
+    public List<Variable> listByGroup(String templateId, String group) {
+        return variableRepository.findByTemplateIdAndGroup(templateId, group);
+    }
+    
+    /**
+     * 更新变量
+     */
+    @Transactional
+    public Variable update(String id, AddVariableRequest request) {
+        Variable variable = variableRepository.selectById(id);
+        if (variable == null) {
+            return null;
+        }
+        
+        // 检查变量名是否重复(排除自己)
+        if (request.getName() != null && !request.getName().equals(variable.getName())) {
+            Variable existing = variableRepository.findByTemplateIdAndName(variable.getTemplateId(), request.getName());
+            if (existing != null) {
+                throw new RuntimeException("变量名已存在: " + request.getName());
+            }
+            variable.setName(request.getName());
+        }
+        
+        if (request.getDisplayName() != null) {
+            variable.setDisplayName(request.getDisplayName());
+        }
+        if (request.getVariableGroup() != null) {
+            variable.setVariableGroup(request.getVariableGroup());
+        }
+        if (request.getLocation() != null) {
+            variable.setLocation(request.getLocation());
+        }
+        if (request.getExampleValue() != null) {
+            variable.setExampleValue(request.getExampleValue());
+        }
+        if (request.getValueType() != null) {
+            variable.setValueType(request.getValueType());
+        }
+        if (request.getSourceFileAlias() != null) {
+            variable.setSourceFileAlias(request.getSourceFileAlias());
+        }
+        if (request.getSourceType() != null) {
+            variable.setSourceType(request.getSourceType());
+        }
+        if (request.getSourceConfig() != null) {
+            variable.setSourceConfig(request.getSourceConfig());
+        }
+        if (request.getExtractType() != null) {
+            variable.setExtractType(request.getExtractType());
+        }
+        if (request.getExtractConfig() != null) {
+            variable.setExtractConfig(request.getExtractConfig());
+        }
+        
+        variable.setUpdateTime(new Date());
+        variableRepository.updateById(variable);
+        log.info("更新变量成功: id={}", id);
+        
+        return variable;
+    }
+    
+    /**
+     * 删除变量
+     */
+    @Transactional
+    public boolean delete(String id) {
+        Variable variable = variableRepository.selectById(id);
+        if (variable == null) {
+            return false;
+        }
+        
+        // 检查是否有其他变量引用此变量
+        List<Variable> referencing = variableRepository.findVariablesReferencingVariable(
+                variable.getTemplateId(), variable.getName());
+        if (!referencing.isEmpty()) {
+            throw new RuntimeException("变量被 " + referencing.size() + " 个其他变量引用,无法删除");
+        }
+        
+        variableRepository.deleteById(id);
+        log.info("删除变量成功: id={}", id);
+        
+        return true;
+    }
+    
+    /**
+     * 强制删除(忽略引用检查)
+     */
+    @Transactional
+    public boolean forceDelete(String id) {
+        Variable variable = variableRepository.selectById(id);
+        if (variable == null) {
+            return false;
+        }
+        
+        variableRepository.deleteById(id);
+        log.info("强制删除变量成功: id={}", id);
+        
+        return true;
+    }
+    
+    /**
+     * 统计数量
+     */
+    public int count(String templateId) {
+        return variableRepository.countByTemplateId(templateId);
+    }
+    
+    /**
+     * 获取引用指定变量的变量列表
+     */
+    public List<Variable> findReferencingVariables(String templateId, String variableName) {
+        return variableRepository.findVariablesReferencingVariable(templateId, variableName);
+    }
+}