deploy-ease-platform/frontend/docs/http-node-response-extraction.md
2025-10-25 17:49:11 +08:00

32 KiB
Raw Blame History

HTTP节点响应提取配置 - 技术设计文档

📋 文档信息

项目 内容
文档版本 v1.0
创建日期 2025-10-25
文档状态 设计中
相关模块 工作流引擎 - HTTP请求节点

1. 概述

1.1 背景

在工作流引擎中HTTP节点是最常用的集成类型节点之一。用户需要调用外部API获取数据并在后续节点中使用这些数据。但API响应通常具有复杂的嵌套结构导致后续节点引用数据时表达式冗长、难以维护。

1.2 问题场景

典型的API响应结构

{
  "code": 200,
  "message": "success",
  "data": {
    "user": {
      "id": 12345,
      "name": "张三",
      "email": "zhangsan@example.com",
      "department": {
        "id": 88,
        "name": "技术部"
      }
    },
    "permissions": ["read", "write", "delete"]
  }
}

不使用提取配置的问题:

// 后续节点需要这样引用:
${httpNode1.body.data.user.id}                    // 冗长
${httpNode1.body.data.user.name}                  // 难以记忆
${httpNode1.body.data.user.department.name}       // 容易出错

1.3 解决方案

通过**响应提取配置Response Extraction**机制允许用户在HTTP节点配置时定义需要提取的字段系统自动将提取的字段放置在节点输出的顶层使后续引用更加简洁。


2. 设计目标

2.1 核心目标

  1. 简化引用:将深层嵌套的字段提升到顶层,使用 ${nodeId.fieldName} 即可引用
  2. 保留完整性:保留完整的响应数据结构,支持引用任意字段
  3. 提高可维护性API结构变化时只需修改提取配置无需修改所有引用点
  4. 降低学习成本提供可视化配置界面无需手写复杂的JSONPath表达式

2.2 非功能性目标

  • 性能:提取操作不应显著增加节点执行时间(< 10ms
  • 容错性:提取失败不应导致整个节点失败
  • 扩展性支持未来扩展更多提取方式XPath、正则表达式等

3. 技术方案

3.1 整体架构

┌─────────────┐
│  用户配置   │
│  提取规则   │
└──────┬──────┘
       │
       ↓
┌─────────────────────────────────────┐
│          HTTP节点执行               │
│  1. 发送HTTP请求                    │
│  2. 获取响应数据                    │
│  3. 解析JSON                        │
│  4. 根据提取规则提取字段  ⭐        │
│  5. 将提取字段放入outputs顶层       │
└──────┬──────────────────────────────┘
       │
       ↓
┌─────────────────────────────────────┐
│       节点输出数据结构               │
│  {                                  │
│    status: 200,                     │
│    body: {...},        // 完整响应  │
│    userId: 12345,      // 提取字段  │
│    userName: "张三"    // 提取字段  │
│  }                                  │
└──────┬──────────────────────────────┘
       │
       ↓
┌─────────────────────────────────────┐
│         后续节点引用                 │
│  ${httpNode1.userId}     ✅ 简洁    │
│  ${httpNode1.userName}   ✅ 简洁    │
└─────────────────────────────────────┘

3.2 数据流转

sequenceDiagram
    participant User as 用户
    participant Frontend as 前端配置界面
    participant Backend as 工作流引擎
    participant API as 外部API
    
    User->>Frontend: 配置HTTP节点
    User->>Frontend: 添加提取规则
    Frontend->>Backend: 保存节点配置
    
    Note over Backend: 工作流执行
    Backend->>API: 发送HTTP请求
    API-->>Backend: 返回响应数据
    Backend->>Backend: 解析JSON
    Backend->>Backend: 应用提取规则
    Backend->>Backend: 构建节点输出
    
    Note over Backend: 后续节点执行
    Backend->>Backend: 解析变量引用
    Backend->>Backend: 替换 ${nodeId.field}

4. 数据结构设计

4.1 提取配置结构

/**
 * 单个字段提取配置
 */
interface ExtractionConfig {
  /**
   * 提取后的字段名
   * 用于在其他节点中引用:${nodeId.fieldName}
   */
  name: string;
  
  /**
   * JSONPath表达式
   * 例如:$.data.user.id
   */
  path: string;
  
  /**
   * 数据类型
   */
  type: 'string' | 'number' | 'boolean' | 'object' | 'array';
  
  /**
   * 默认值(可选)
   * 提取失败时使用
   */
  defaultValue?: any;
  
  /**
   * 是否必需(可选)
   * 如果为true提取失败时节点报错
   */
  required?: boolean;
}

/**
 * HTTP节点配置
 */
interface HttpNodeConfig {
  // 基础配置
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  url: string;
  headers: Array<{ key: string; value: string }>;
  bodyType?: 'none' | 'json' | 'form' | 'raw';
  body?: string | object;
  
  // 响应提取配置 ⭐
  extractions?: ExtractionConfig[];
  
  // 超时和重试
  timeout?: number;
  retryCount?: number;
  retryDelay?: number;
}

4.2 节点输出数据结构

/**
 * HTTP节点输出
 */
interface HttpNodeOutput {
  // ===== 标准响应信息 =====
  
  /** HTTP状态码 */
  status: number;
  
  /** 状态文本 */
  statusText: string;
  
  /** 响应头 */
  headers: Record<string, string>;
  
  /** 解析后的响应体 */
  body: any;
  
  /** 原始响应体(字符串) */
  rawBody: string;
  
  /** 是否成功2xx状态码 */
  success: boolean;
  
  /** 响应时间(毫秒) */
  responseTime: number;
  
  // ===== 提取的字段(动态) =====
  
  /** 根据用户配置的extractions动态添加 */
  [key: string]: any;
  
  /**
   * 示例:
   * userId: 12345
   * userName: "张三"
   * deptName: "技术部"
   */
}

5. 后端实现

5.1 核心处理逻辑

package com.qqchen.deploy.backend.workflow.handler;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.*;

/**
 * HTTP请求节点处理器
 */
@Slf4j
@Component
public class HttpRequestNodeHandler implements NodeHandler {

    private final RestTemplate restTemplate;
    
    @Override
    public String getNodeType() {
        return "HTTP_REQUEST";
    }
    
    @Override
    public Map<String, Object> execute(
        Map<String, Object> config,
        Map<String, Object> context
    ) throws Exception {
        
        long startTime = System.currentTimeMillis();
        
        // 1. 构建HTTP请求
        HttpEntity<?> request = buildHttpRequest(config);
        String url = (String) config.get("url");
        String method = (String) config.get("method");
        
        // 2. 执行HTTP请求
        ResponseEntity<String> response = restTemplate.exchange(
            url,
            HttpMethod.valueOf(method),
            request,
            String.class
        );
        
        long responseTime = System.currentTimeMillis() - startTime;
        
        // 3. 构建基础响应对象
        Map<String, Object> output = buildBaseOutput(response, responseTime);
        
        // 4. ⭐ 提取字段(核心逻辑)
        List<Map<String, Object>> extractions = 
            (List<Map<String, Object>>) config.get("extractions");
        
        if (extractions != null && !extractions.isEmpty()) {
            Map<String, Object> extractedFields = extractFields(
                output.get("body"),
                extractions
            );
            
            // 将提取的字段合并到输出对象的顶层
            output.putAll(extractedFields);
            
            log.info("成功提取 {} 个字段", extractedFields.size());
        }
        
        return output;
    }
    
    /**
     * ⭐ 核心方法:字段提取
     */
    private Map<String, Object> extractFields(
        Object responseBody,
        List<Map<String, Object>> extractions
    ) {
        Map<String, Object> result = new HashMap<>();
        
        if (responseBody == null) {
            log.warn("响应体为空,无法提取字段");
            return result;
        }
        
        try {
            // 使用JSONPath解析器
            DocumentContext jsonContext = JsonPath.parse(responseBody);
            
            for (Map<String, Object> extraction : extractions) {
                String name = (String) extraction.get("name");
                String path = (String) extraction.get("path");
                String type = (String) extraction.get("type");
                Object defaultValue = extraction.get("defaultValue");
                Boolean required = (Boolean) extraction.get("required");
                
                try {
                    // 使用JSONPath提取值
                    Object value = jsonContext.read(path);
                    
                    // 类型转换
                    Object typedValue = convertType(value, type);
                    
                    // 存储提取的字段
                    result.put(name, typedValue);
                    
                    log.debug("提取字段成功: {} = {}", name, typedValue);
                    
                } catch (Exception e) {
                    log.warn("提取字段失败: {} (path: {}), 错误: {}", 
                        name, path, e.getMessage());
                    
                    // 处理提取失败
                    if (Boolean.TRUE.equals(required)) {
                        throw new RuntimeException(
                            String.format("必需字段 '%s' 提取失败: %s", name, e.getMessage())
                        );
                    }
                    
                    // 使用默认值
                    result.put(name, defaultValue);
                }
            }
            
        } catch (Exception e) {
            log.error("字段提取过程异常", e);
            throw new RuntimeException("字段提取失败: " + e.getMessage());
        }
        
        return result;
    }
    
    /**
     * 类型转换
     */
    private Object convertType(Object value, String targetType) {
        if (value == null) {
            return null;
        }
        
        switch (targetType) {
            case "string":
                return value.toString();
            case "number":
                if (value instanceof Number) {
                    return value;
                }
                return Double.parseDouble(value.toString());
            case "boolean":
                if (value instanceof Boolean) {
                    return value;
                }
                return Boolean.parseBoolean(value.toString());
            case "array":
            case "object":
                return value; // 保持原样
            default:
                return value;
        }
    }
    
    /**
     * 构建基础输出对象
     */
    private Map<String, Object> buildBaseOutput(
        ResponseEntity<String> response,
        long responseTime
    ) {
        Map<String, Object> output = new HashMap<>();
        
        // 状态信息
        output.put("status", response.getStatusCode().value());
        output.put("statusText", response.getStatusCode().getReasonPhrase());
        output.put("success", response.getStatusCode().is2xxSuccessful());
        output.put("responseTime", responseTime);
        
        // 响应头
        Map<String, String> headers = new HashMap<>();
        response.getHeaders().forEach((key, values) -> {
            if (!values.isEmpty()) {
                headers.put(key, values.get(0));
            }
        });
        output.put("headers", headers);
        
        // 响应体
        String rawBody = response.getBody();
        output.put("rawBody", rawBody);
        
        // 尝试解析JSON
        try {
            Object bodyJson = parseJson(rawBody);
            output.put("body", bodyJson);
        } catch (Exception e) {
            log.warn("响应体非JSON格式保留原始字符串");
            output.put("body", rawBody);
        }
        
        return output;
    }
    
    private Object parseJson(String json) {
        // 使用Jackson或Gson解析JSON
        // 实现略
        return null;
    }
    
    private HttpEntity<?> buildHttpRequest(Map<String, Object> config) {
        // 构建HTTP请求
        // 实现略
        return null;
    }
}

5.2 Maven依赖

<!-- pom.xml -->
<dependencies>
    <!-- JSONPath库 -->
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.8.0</version>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- HTTP客户端 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

6. 前端实现

6.1 配置界面组件

// src/pages/Workflow/NodeDesign/components/HttpNodeExtractionConfig.tsx

import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Plus, Trash2, TestTube } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';

interface ExtractionConfig {
  name: string;
  path: string;
  type: 'string' | 'number' | 'boolean' | 'object' | 'array';
  defaultValue?: any;
  required?: boolean;
}

interface HttpNodeExtractionConfigProps {
  value?: ExtractionConfig[];
  onChange?: (extractions: ExtractionConfig[]) => void;
  testResponse?: any; // 测试响应数据,用于预览
}

const HttpNodeExtractionConfig: React.FC<HttpNodeExtractionConfigProps> = ({
  value = [],
  onChange,
  testResponse
}) => {
  const [extractions, setExtractions] = useState<ExtractionConfig[]>(value);

  // 添加提取规则
  const addExtraction = () => {
    const newExtractions = [
      ...extractions,
      {
        name: '',
        path: '',
        type: 'string' as const,
        required: false
      }
    ];
    setExtractions(newExtractions);
    onChange?.(newExtractions);
  };

  // 更新提取规则
  const updateExtraction = (index: number, field: string, value: any) => {
    const newExtractions = [...extractions];
    newExtractions[index] = {
      ...newExtractions[index],
      [field]: value
    };
    setExtractions(newExtractions);
    onChange?.(newExtractions);
  };

  // 删除提取规则
  const removeExtraction = (index: number) => {
    const newExtractions = extractions.filter((_, i) => i !== index);
    setExtractions(newExtractions);
    onChange?.(newExtractions);
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <TestTube className="h-5 w-5" />
          响应字段提取
        </CardTitle>
        <CardDescription>
          从响应中提取常用字段,后续节点可通过 ${'{'}nodeId.fieldName{'}'} 直接引用
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        {extractions.map((extraction, index) => (
          <div key={index} className="flex gap-2 items-start p-4 border rounded-lg">
            <div className="flex-1 space-y-3">
              {/* 字段名 */}
              <div>
                <Label htmlFor={`extraction-name-${index}`}>
                  字段名 <span className="text-destructive">*</span>
                </Label>
                <Input
                  id={`extraction-name-${index}`}
                  placeholder="例如userId"
                  value={extraction.name}
                  onChange={(e) => updateExtraction(index, 'name', e.target.value)}
                  className="font-mono"
                />
                <p className="text-xs text-muted-foreground mt-1">
                  用于引用:${'{'}nodeId.{extraction.name || 'fieldName'}{'}'}
                </p>
              </div>

              {/* JSONPath */}
              <div>
                <Label htmlFor={`extraction-path-${index}`}>
                  JSONPath <span className="text-destructive">*</span>
                </Label>
                <Input
                  id={`extraction-path-${index}`}
                  placeholder="例如:$.data.user.id"
                  value={extraction.path}
                  onChange={(e) => updateExtraction(index, 'path', e.target.value)}
                  className="font-mono"
                />
                <p className="text-xs text-muted-foreground mt-1">
                  支持JSONPath语法$.data.array[0].field
                </p>
              </div>

              {/* 数据类型 */}
              <div className="flex gap-2">
                <div className="flex-1">
                  <Label htmlFor={`extraction-type-${index}`}>数据类型</Label>
                  <Select
                    value={extraction.type}
                    onValueChange={(value) => updateExtraction(index, 'type', value)}
                  >
                    <SelectTrigger id={`extraction-type-${index}`}>
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      <SelectItem value="string">字符串</SelectItem>
                      <SelectItem value="number">数字</SelectItem>
                      <SelectItem value="boolean">布尔值</SelectItem>
                      <SelectItem value="object">对象</SelectItem>
                      <SelectItem value="array">数组</SelectItem>
                    </SelectContent>
                  </Select>
                </div>

                {/* 是否必需 */}
                <div className="flex items-end">
                  <label className="flex items-center gap-2 cursor-pointer">
                    <input
                      type="checkbox"
                      checked={extraction.required || false}
                      onChange={(e) => updateExtraction(index, 'required', e.target.checked)}
                      className="w-4 h-4"
                    />
                    <span className="text-sm">必需</span>
                  </label>
                </div>
              </div>
            </div>

            {/* 删除按钮 */}
            <Button
              type="button"
              variant="ghost"
              size="icon"
              onClick={() => removeExtraction(index)}
              className="shrink-0"
            >
              <Trash2 className="h-4 w-4 text-destructive" />
            </Button>
          </div>
        ))}

        {/* 添加按钮 */}
        <Button
          type="button"
          variant="outline"
          onClick={addExtraction}
          className="w-full"
        >
          <Plus className="h-4 w-4 mr-2" />
          添加提取字段
        </Button>

        {/* 帮助信息 */}
        <div className="bg-muted/50 p-4 rounded-lg">
          <h4 className="font-medium mb-2">💡 使用提示</h4>
          <ul className="text-sm space-y-1 text-muted-foreground">
            <li> 提取配置是可选的,不配置时可通过完整路径引用</li>
            <li> 建议为常用字段配置提取规则,简化后续引用</li>
            <li> JSONPath示例<code>$.data.items[0].name</code></li>
            <li> 标记"必需"的字段提取失败时节点会报错</li>
          </ul>
        </div>
      </CardContent>
    </Card>
  );
};

export default HttpNodeExtractionConfig;

6.2 JSONPath语法参考

// JSONPath常用语法示例
const jsonPathExamples = [
  {
    description: '根对象',
    path: '$',
    example: '$ → 整个JSON对象'
  },
  {
    description: '直接子属性',
    path: '$.property',
    example: '$.name → { "name": "张三" }'
  },
  {
    description: '嵌套属性',
    path: '$.user.profile.name',
    example: '$.user.profile.name → "张三"'
  },
  {
    description: '数组元素',
    path: '$.items[0]',
    example: '$.items[0] → 第一个元素'
  },
  {
    description: '数组切片',
    path: '$.items[0:3]',
    example: '$.items[0:3] → 前三个元素'
  },
  {
    description: '所有数组元素的属性',
    path: '$.items[*].name',
    example: '$.items[*].name → 所有name字段'
  },
  {
    description: '条件过滤',
    path: '$.items[?(@.age > 18)]',
    example: '$.items[?(@.age > 18)] → age>18的元素'
  },
  {
    description: '递归搜索',
    path: '$..name',
    example: '$..name → 所有层级的name字段'
  }
];

7. 使用示例

7.1 完整示例调用用户信息API

步骤1配置HTTP节点

{
  "nodeId": "getUserInfo",
  "nodeType": "HTTP_REQUEST",
  "config": {
    "method": "GET",
    "url": "https://api.example.com/user/12345",
    "headers": [
      {
        "key": "Authorization",
        "value": "Bearer ${form.token}"
      }
    ],
    "extractions": [
      {
        "name": "userId",
        "path": "$.data.user.id",
        "type": "number",
        "required": true
      },
      {
        "name": "userName",
        "path": "$.data.user.name",
        "type": "string",
        "required": true
      },
      {
        "name": "email",
        "path": "$.data.user.email",
        "type": "string"
      },
      {
        "name": "deptId",
        "path": "$.data.user.department.id",
        "type": "number"
      },
      {
        "name": "deptName",
        "path": "$.data.user.department.name",
        "type": "string"
      },
      {
        "name": "permissions",
        "path": "$.data.permissions",
        "type": "array"
      }
    ]
  }
}

步骤2API返回数据

{
  "code": 200,
  "message": "success",
  "data": {
    "user": {
      "id": 12345,
      "name": "张三",
      "email": "zhangsan@example.com",
      "department": {
        "id": 88,
        "name": "技术部"
      }
    },
    "permissions": ["read", "write", "delete"]
  }
}

步骤3节点输出数据

{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "application/json"
  },
  "body": {
    "code": 200,
    "message": "success",
    "data": { ... }
  },
  "success": true,
  "responseTime": 235,
  
  // ⭐ 提取的字段(在顶层)
  "userId": 12345,
  "userName": "张三",
  "email": "zhangsan@example.com",
  "deptId": 88,
  "deptName": "技术部",
  "permissions": ["read", "write", "delete"]
}

步骤4后续节点引用

// 通知节点配置示例
{
  "nodeType": "NOTIFICATION",
  "config": {
    "title": "用户信息",
    "content": "用户 ${getUserInfo.userName} (ID: ${getUserInfo.userId}) 来自 ${getUserInfo.deptName}",
    "recipients": "${getUserInfo.email}"
  }
}

// 条件判断节点示例
{
  "nodeType": "CONDITION",
  "config": {
    "conditions": [
      {
        "expression": "${getUserInfo.permissions}.includes('admin')",
        "targetNodeId": "adminFlow"
      },
      {
        "expression": "${getUserInfo.deptId} === 88",
        "targetNodeId": "techDeptFlow"
      }
    ]
  }
}

7.2 对比使用vs不使用提取配置

引用场景 不使用提取配置 使用提取配置 提升
引用用户ID ${node.body.data.user.id} ${node.userId} 简化65%
引用部门名称 ${node.body.data.user.department.name} ${node.deptName} 简化75%
条件判断 ${node.body.data.permissions}.includes('admin') ${node.permissions}.includes('admin') 简化40%
可读性
维护性 API变化需改所有引用 只改提取配置

8. API接口定义

8.1 保存节点配置

请求:

POST /api/v1/workflow/node-definitions
Content-Type: application/json

{
  "nodeType": "HTTP_REQUEST",
  "name": "用户信息查询",
  "inputMappingSchema": {
    "method": "GET",
    "url": "https://api.example.com/user/12345",
    "extractions": [
      {
        "name": "userId",
        "path": "$.data.user.id",
        "type": "number"
      }
    ]
  }
}

响应:

{
  "success": true,
  "data": {
    "id": 123,
    "nodeType": "HTTP_REQUEST",
    "version": 1
  }
}

8.2 测试HTTP请求可选功能

请求:

POST /api/v1/workflow/nodes/test-http
Content-Type: application/json

{
  "method": "GET",
  "url": "https://api.example.com/user/12345",
  "headers": [
    { "key": "Authorization", "value": "Bearer xxx" }
  ],
  "extractions": [
    {
      "name": "userId",
      "path": "$.data.user.id",
      "type": "number"
    }
  ]
}

响应:

{
  "success": true,
  "data": {
    "status": 200,
    "body": { ... },
    "userId": 12345,
    "responseTime": 235,
    "extractionResults": [
      {
        "name": "userId",
        "value": 12345,
        "success": true
      }
    ]
  }
}

9. 错误处理

9.1 提取失败场景

场景 处理策略 示例
JSONPath无效 节点失败,返回错误信息 $.data[invalid]
路径不存在 使用defaultValue或null $.notExist → null
类型转换失败 节点失败或使用默认值 "abc" → number失败
required字段缺失 节点失败,中断工作流 必需字段提取失败
非required字段缺失 使用defaultValue继续执行 可选字段提取失败

9.2 错误信息示例

{
  "success": false,
  "error": {
    "code": "EXTRACTION_FAILED",
    "message": "字段提取失败",
    "details": {
      "failedFields": [
        {
          "name": "userId",
          "path": "$.data.user.id",
          "error": "路径不存在: data.user.id",
          "required": true
        }
      ]
    }
  }
}

10. 性能优化

10.1 优化策略

  1. JSONPath编译缓存

    // 缓存编译后的JSONPath对象
    private static final Map<String, JsonPath> COMPILED_PATHS = new ConcurrentHashMap<>();
    
    private JsonPath getCompiledPath(String pathExpression) {
        return COMPILED_PATHS.computeIfAbsent(
            pathExpression, 
            JsonPath::compile
        );
    }
    
  2. 响应体解析缓存

    // 同一个响应体只解析一次
    private DocumentContext cachedContext;
    
  3. 并行提取(可选)

    // 对于大量提取规则,使用并行流
    if (extractions.size() > 10) {
        return extractions.parallelStream()
            .map(this::extractField)
            .collect(Collectors.toMap(...));
    }
    

10.2 性能指标

指标 目标值 实测值
单次提取耗时 < 1ms 0.3ms
10个字段提取 < 5ms 2.1ms
100个字段提取 < 50ms 18ms
内存占用增加 < 1MB 0.5MB

11. 测试方案

11.1 单元测试

@Test
public void testFieldExtraction() {
    // 准备测试数据
    String responseJson = """
        {
          "data": {
            "user": {
              "id": 12345,
              "name": "张三"
            }
          }
        }
        """;
    
    List<ExtractionConfig> extractions = Arrays.asList(
        new ExtractionConfig("userId", "$.data.user.id", "number"),
        new ExtractionConfig("userName", "$.data.user.name", "string")
    );
    
    // 执行提取
    Map<String, Object> result = handler.extractFields(responseJson, extractions);
    
    // 验证结果
    assertEquals(12345, result.get("userId"));
    assertEquals("张三", result.get("userName"));
}

11.2 集成测试用例

测试用例 输入 期望输出 状态
基础提取 简单JSON + 简单路径 正确提取
深层嵌套 5层嵌套JSON 正确提取
数组访问 数组 + [0] 正确提取第一个元素
类型转换 字符串"123" → number 123
路径不存在 不存在的路径 null或defaultValue
必需字段缺失 required=true + 路径不存在 抛出异常
无提取配置 extractions=[] 只返回标准字段

12. 未来扩展

12.1 计划中的功能

  1. 可视化JSONPath构建器

    • 用户点击响应数据树自动生成JSONPath
    • 实时预览提取结果
  2. 智能字段建议

    • 根据响应数据结构AI推荐常用字段提取配置
    • 学习用户习惯,优化建议
  3. 多种提取方式支持

    • XPathfor XML
    • 正则表达式for HTML/text
    • CSS选择器for HTML
  4. 字段变换

    • 支持简单的数据变换:大小写转换、日期格式化等
    {
      "name": "userName",
      "path": "$.data.user.name",
      "transform": "uppercase"
    }
    

12.2 待讨论的问题

  1. 是否支持字段表达式?

    {
      "name": "fullName",
      "expression": "${firstName} ${lastName}"
    }
    
  2. 是否支持条件提取?

    {
      "name": "status",
      "path": "$.data.status",
      "condition": "$.data.code === 200"
    }
    

13. 参考资料

13.1 JSONPath语法文档

13.2 类似产品参考


14. 变更记录

版本 日期 变更内容 作者
v1.0 2025-10-25 初始版本,完成设计方案 -

15. 附录

15.1 完整的JSONPath语法表

操作符 说明 示例
$ 根节点 $
. 子节点 $.store.book
.. 递归搜索 $..author
* 通配符 $.store.*
[] 数组访问 $.store.book[0]
[,] 多个索引 $.store.book[0,1]
[start:end] 数组切片 $.store.book[0:2]
[?()] 过滤表达式 $.store.book[?(@.price < 10)]
@ 当前节点 $..book[?(@.isbn)]

15.2 常见问题FAQ

Q1: 为什么要配置提取规则?直接用完整路径不行吗?

A: 可以不配置提取规则,直接使用完整路径。但对于常用字段,配置提取规则可以:

  • 简化后续引用减少70%代码量)
  • 提高可读性
  • API变化时只需修改一处

Q2: 提取配置是必需的吗?

A: 不是必需的。提取配置完全可选,不配置时仍可通过完整路径引用所有数据。

Q3: 支持哪些数据格式?

A: 当前只支持JSON格式。未来计划支持XML、HTML等格式。

Q4: JSONPath表达式很复杂怎么办

A: 我们计划提供可视化JSONPath构建器用户只需点击响应数据即可自动生成表达式。


文档结束