datai/datai-scenes/datai-scene-salesforce/docs/design/2026-01-28-004-03-开发工具功能-设计.md

31 KiB
Raw Permalink Blame History

设计文档

元数据

  • 需求编号004
  • 子需求编号004-03
  • 子需求名称:开发工具功能
  • 阶段:阶段 2方案设计
  • 创建时间2026-01-28
  • 状态:已完成

架构设计

系统架构图

graph TB
    subgraph "客户端层"
        A[前端应用]
    end
    
    subgraph "API 网关层"
        B[API Gateway]
    end
    
    subgraph "应用层"
        C[ToolingDevToolsController]
    end
    
    subgraph "服务层"
        D[IToolingDevToolsService]
        E[ToolingDevToolsServiceImpl]
    end
    
    subgraph "工厂层"
        F[ToolingConnectionFactory]
    end
    
    subgraph "会话管理层"
        G[SessionManager]
    end
    
    subgraph "数据层"
        H[(MySQL)]
    end
    
    subgraph "外部服务"
        I[Salesforce Tooling API]
    end
    
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    E --> H
    F --> I

模块架构图

graph LR
    subgraph "Controller 层"
        A1[ToolingDevToolsController]
    end
    
    subgraph "Service 层"
        B1[IToolingDevToolsService]
        B2[ToolingDevToolsServiceImpl]
    end
    
    subgraph "Factory 层"
        C1[ToolingConnectionFactory]
    end
    
    subgraph "Session 层"
        D1[SessionManager]
    end
    
    subgraph "数据访问层"
        E1[ToolingDevToolsOperationLogMapper]
    end
    
    subgraph "数据层"
        F1[(datai_tooling_devtools_operation_log)]
    end
    
    A1 --> B1
    B1 --> B2
    B2 --> C1
    C1 --> D1
    B2 --> E1
    E1 --> F1

数据流图

sequenceDiagram
    participant Client as 客户端
    participant Controller as ToolingDevToolsController
    participant Service as ToolingDevToolsServiceImpl
    participant Factory as ToolingConnectionFactory
    participant Session as SessionManager
    participant SF as Salesforce Tooling API
    participant DB as MySQL
    
    Client->>Controller: 获取代码覆盖率
    Controller->>Service: getApexCodeCoverage()
    Service->>Factory: getConnection("source")
    Factory->>Session: getSession()
    Session-->>Factory: SessionInfo
    Factory-->>Service: ToolingConnection
    Service->>SF: query(SOQL)
    SF-->>Service: ApexCodeCoverage[]
    Service->>DB: insert(log)
    Service-->>Controller: ApexCodeCoverageResult[]
    Controller-->>Client: JSON 响应

技术方案

技术选型

技术领域 选型 说明
Web 框架 Spring Boot 提供RESTful API 支持
ORM 框架 MyBatis Plus 简化数据库操作
数据库 MySQL 存储操作日志
Salesforce SDK tooling.jar Salesforce 官方 Tooling API 客户端
连接管理 ToolingConnectionFactory 复用子需求 004-01 的连接工厂
会话管理 SessionManager 复用 datai-salesforce-auth 的会话管理
异常处理 SalesforceAuthException 统一异常处理
日志框架 SLF4J + Logback 应用日志记录
文档工具 Swagger/OpenAPI API 文档生成

核心算法

1. 代码覆盖率查询算法

public List<ApexCodeCoverage> getApexCodeCoverage(String apexClassOrTriggerId) {
    ToolingConnection connection = connectionFactory.getConnection("source");
    
    SoqlBuilder builder = SoqlBuilder.newInstance()
        .select("Id, ApexClassOrTriggerId, ApexClassOrTrigger.Name, " +
                "ApexTestClass.Id, ApexTestClass.Name, Coverage, " +
                "NumLinesCovered, NumLinesUncovered, LocationsNotCovered")
        .from("ApexCodeCoverage");
    
    if (StringUtils.isNotBlank(apexClassOrTriggerId)) {
        builder.where("ApexClassOrTriggerId = :id")
               .setParameter("id", apexClassOrTriggerId);
    }
    
    QueryResult result = connection.query(builder.build());
    return convertToApexCodeCoverageList(result.getRecords());
}

2. 测试队列查询算法

public List<ApexTestQueueItem> getApexTestQueueItems(String apexClassId) {
    ToolingConnection connection = connectionFactory.getConnection("source");
    
    SoqlBuilder builder = SoqlBuilder.newInstance()
        .select("Id, ApexClassId, ApexClass.Name, Status, " +
                "ExtendedStatus, MethodNames, CreatedDate, ParentJobId")
        .from("ApexTestQueueItem");
    
    if (StringUtils.isNotBlank(apexClassId)) {
        builder.where("ApexClassId = :id")
               .setParameter("id", apexClassId);
    }
    
    QueryResult result = connection.query(builder.build());
    return convertToApexTestQueueItemList(result.getRecords());
}

3. 成员信息查询算法

public ApexClassMember getApexClassMember(String apexClassId) {
    ToolingConnection connection = connectionFactory.getConnection("source");
    
    SoqlBuilder builder = SoqlBuilder.newInstance()
        .select("Id, Body, BodyCrc, ContentEntityId, ContentType, SymbolTable")
        .from("ApexClassMember")
        .where("ContentEntityId = :id")
        .setParameter("id", apexClassId);
    
    QueryResult result = connection.query(builder.build());
    if (result.getSize() == 0) {
        throw new SalesforceAuthException("TOOLING_DEVTOOLS_005", 
                                      "Apex 类成员不存在");
    }
    
    return convertToApexClassMember(result.getRecords()[0]);
}

4. 异步日志记录算法

private void logOperation(String operationType, String metadataType, 
                      String metadataId, String metadataName,
                      String queryCondition, int resultCount,
                      boolean success, String errorCode, String errorMessage) {
    CompletableFuture.runAsync(() -> {
        ToolingDevToolsOperationLog log = new ToolingDevToolsOperationLog();
        log.setOperationType(operationType);
        log.setMetadataType(metadataType);
        log.setMetadataId(metadataId);
        log.setMetadataName(metadataName);
        log.setQueryCondition(queryCondition);
        log.setResultCount(resultCount);
        log.setStatus(success ? "success" : "failed");
        log.setErrorCode(errorCode);
        log.setErrorMessage(errorMessage);
        log.setOperationTime(LocalDateTime.now());
        log.setUserId(SecurityUtils.getUserId());
        
        logMapper.insert(log);
    }, executor);
}

集成方案

1. 与 ToolingConnectionFactory 集成

@Service
public class ToolingDevToolsServiceImpl implements IToolingDevToolsService {
    
    @Autowired
    private ToolingConnectionFactory connectionFactory;
    
    @Override
    public List<ApexCodeCoverage> getApexCodeCoverage(String apexClassOrTriggerId) {
        ToolingConnection connection = connectionFactory.getConnection("source");
        // 使用连接执行查询
    }
}

2. 与 SessionManager 集成

@Service
public class ToolingDevToolsServiceImpl implements IToolingDevToolsService {
    
    @Autowired
    private SessionManager sessionManager;
    
    @Override
    public List<ApexCodeCoverage> getApexCodeCoverage(String apexClassOrTriggerId) {
        SessionInfo session = sessionManager.getSession();
        if (session == null || session.isExpired()) {
            throw new SalesforceLoginException("TOOLING_DEVTOOLS_006", "用户未登录");
        }
        // 执行查询
    }
}

3. 与 MyBatis Plus 集成

@Mapper
public interface ToolingDevToolsOperationLogMapper extends BaseMapper<ToolingDevToolsOperationLog> {
    
    @Select("SELECT * FROM datai_tooling_devtools_operation_log " +
            "WHERE operation_type = #{operationType} " +
            "AND operation_time >= #{startTime} " +
            "AND operation_time <= #{endTime} " +
            "ORDER BY operation_time DESC " +
            "LIMIT #{offset}, #{pageSize}")
    List<ToolingDevToolsOperationLog> queryLogs(
        @Param("operationType") String operationType,
        @Param("startTime") LocalDateTime startTime,
        @Param("endTime") LocalDateTime endTime,
        @Param("offset") int offset,
        @Param("pageSize") int pageSize
    );
}

数据模型

数据库表设计

datai_tooling_devtools_operation_log开发工具操作日志表

字段名 类型 长度 必填 默认值 说明
id BIGINT - 自增 主键
operation_type VARCHAR 50 - 操作类型get_code_coverage、get_test_queue_items、get_apex_log、get_class_member、get_trigger_member、get_page_member、get_component_member
metadata_type VARCHAR 100 NULL 元数据类型ApexCodeCoverage、ApexTestQueueItem、ApexLog、ApexClassMember、ApexTriggerMember、ApexPageMember、ApexComponentMember
metadata_id VARCHAR 255 NULL 元数据 ID
metadata_name VARCHAR 255 NULL 元数据名称
query_condition TEXT - NULL 查询条件JSON 格式)
result_count INT - 0 结果数量
status VARCHAR 20 - 操作状态success/failed
error_code VARCHAR 50 NULL 错误码(失败时)
error_message TEXT - NULL 错误消息(失败时)
operation_time DATETIME - - 操作时间
user_id VARCHAR 50 NULL 用户 ID
create_time DATETIME - CURRENT_TIMESTAMP 记录创建时间
update_time DATETIME - CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 记录更新时间

索引设计

  • PRIMARY KEY: id
  • INDEX: idx_operation_type (operation_type)
  • INDEX: idx_metadata_type (metadata_type)
  • INDEX: idx_metadata_id (metadata_id)
  • INDEX: idx_metadata_name (metadata_name)
  • INDEX: idx_operation_time (operation_time)
  • INDEX: idx_user_id (user_id)
  • INDEX: idx_status (status)
  • INDEX: idx_metadata_type_operation_time (metadata_type, operation_time)
  • INDEX: idx_operation_type_status (operation_type, status)

SQL 建表语句

CREATE TABLE `datai_tooling_devtools_operation_log` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型',
  `metadata_type` VARCHAR(100) DEFAULT NULL COMMENT '元数据类型',
  `metadata_id` VARCHAR(255) DEFAULT NULL COMMENT '元数据ID',
  `metadata_name` VARCHAR(255) DEFAULT NULL COMMENT '元数据名称',
  `query_condition` TEXT DEFAULT NULL COMMENT '查询条件JSON格式',
  `result_count` INT NOT NULL DEFAULT 0 COMMENT '结果数量',
  `status` VARCHAR(20) NOT NULL COMMENT '操作状态success/failed',
  `error_code` VARCHAR(50) DEFAULT NULL COMMENT '错误码',
  `error_message` TEXT DEFAULT NULL COMMENT '错误消息',
  `operation_time` DATETIME NOT NULL COMMENT '操作时间',
  `user_id` VARCHAR(50) DEFAULT NULL COMMENT '用户ID',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_operation_type` (`operation_type`),
  KEY `idx_metadata_type` (`metadata_type`),
  KEY `idx_metadata_id` (`metadata_id`),
  KEY `idx_metadata_name` (`metadata_name`),
  KEY `idx_operation_time` (`operation_time`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_metadata_type_operation_time` (`metadata_type`, `operation_time`),
  KEY `idx_operation_type_status` (`operation_type`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tooling API 开发工具操作日志表';

实体类设计

1. ToolingDevToolsOperationLog开发工具操作日志实体

@Data
@TableName("datai_tooling_devtools_operation_log")
public class ToolingDevToolsOperationLog {
    
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("operation_type")
    private String operationType;
    
    @TableField("metadata_type")
    private String metadataType;
    
    @TableField("metadata_id")
    private String metadataId;
    
    @TableField("metadata_name")
    private String metadataName;
    
    @TableField("query_condition")
    private String queryCondition;
    
    @TableField("result_count")
    private Integer resultCount;
    
    @TableField("status")
    private String status;
    
    @TableField("error_code")
    private String errorCode;
    
    @TableField("error_message")
    private String errorMessage;
    
    @TableField("operation_time")
    private LocalDateTime operationTime;
    
    @TableField("user_id")
    private String userId;
    
    @TableField("create_time")
    private LocalDateTime createTime;
    
    @TableField("update_time")
    private LocalDateTime updateTime;
}

2. ApexCodeCoverageResult代码覆盖率结果

@Data
public class ApexCodeCoverageResult {
    
    private String apexClassOrTriggerId;
    private String apexClassOrTriggerName;
    private String apexTestClassId;
    private String apexTestClassName;
    private Double coverage;
    private Integer numLinesCovered;
    private Integer numLinesUncovered;
    private List<CodeLocation> locationsNotCovered;
}

3. ApexTestQueueItemResult测试队列项结果

@Data
public class ApexTestQueueItemResult {
    
    private String apexClassId;
    private String apexClassName;
    private String status;
    private String extendedStatus;
    private List<String> methodNames;
    private String createdDate;
    private String parentJobId;
}

4. ApexLogResult日志结果

@Data
public class ApexLogResult {
    
    private String id;
    private String application;
    private Integer durationMilliseconds;
    private String location;
    private Integer logLength;
    private String operation;
    private String startTime;
    private String status;
}

5. ApexMemberResult成员结果

@Data
public class ApexMemberResult {
    
    private String id;
    private String body;
    private Double bodyCrc;
    private String contentEntityId;
    private String contentType;
    private SymbolTable symbolTable;
}

接口设计

RESTful API 接口列表

序号 接口名称 HTTP 方法 路径 功能描述
1 获取 Apex 代码覆盖率 GET /salesforce/tooling/devtools/code-coverage 获取 Apex 代码覆盖率信息
2 获取 Apex 测试队列项 GET /salesforce/tooling/devtools/test-queue-items 获取 Apex 测试队列项
3 获取 Apex 日志 GET /salesforce/tooling/devtools/apex-log 获取 Apex 日志信息
4 获取 Apex 类成员 GET /salesforce/tooling/devtools/class-member 获取 Apex 类成员信息
5 获取 Apex 触发器成员 GET /salesforce/tooling/devtools/trigger-member 获取 Apex 触发器成员信息
6 获取 Visualforce 页面成员 GET /salesforce/tooling/devtools/page-member 获取 Visualforce 页面成员信息
7 获取 Visualforce 组件成员 GET /salesforce/tooling/devtools/component-member 获取 Visualforce 组件成员信息
8 获取开发工具操作日志 GET /salesforce/tooling/devtools/operation-logs 查询开发工具操作日志

接口详细设计

1. 获取 Apex 代码覆盖率

接口: GET /salesforce/tooling/devtools/code-coverage

功能: 获取 Apex 代码覆盖率信息

请求参数:

参数名 类型 必填 说明
apexClassOrTriggerId String Apex 类或触发器 ID

响应示例:

{
  "code": 200,
  "message": "获取代码覆盖率成功",
  "data": {
    "success": true,
    "total": 100,
    "coverageResults": [
      {
        "apexClassOrTriggerId": "01p...",
        "apexClassOrTriggerName": "MyClass",
        "apexTestClassId": "01p...",
        "apexTestClassName": "MyTestClass",
        "coverage": 85.5,
        "numLinesCovered": 171,
        "numLinesUncovered": 29,
        "locationsNotCovered": []
      }
    ]
  }
}

错误码:

  • TOOLING_DEVTOOLS_001: Session 无效或已过期
  • TOOLING_DEVTOOLS_002: 查询失败
  • TOOLING_DEVTOOLS_003: 网络超时
  • TOOLING_DEVTOOLS_004: 权限不足
  • TOOLING_DEVTOOLS_005: 记录不存在
  • TOOLING_DEVTOOLS_006: 用户未登录

2. 获取 Apex 测试队列项

接口: GET /salesforce/tooling/devtools/test-queue-items

功能: 获取 Apex 测试队列项

请求参数:

参数名 类型 必填 说明
apexClassId String Apex 类 ID

响应示例:

{
  "code": 200,
  "message": "获取测试队列项成功",
  "data": {
    "success": true,
    "total": 50,
    "queueItems": [
      {
        "apexClassId": "01p...",
        "apexClassName": "MyClass",
        "status": "Completed",
        "extendedStatus": "Passed",
        "methodNames": ["testMethod1", "testMethod2"],
        "createdDate": "2026-01-28 10:00:00",
        "parentJobId": "707..."
      }
    ]
  }
}

3. 获取 Apex 日志

接口: GET /salesforce/tooling/devtools/apex-log

功能: 获取 Apex 日志信息

请求参数:

参数名 类型 必填 说明
logId String 日志 ID

响应示例:

{
  "code": 200,
  "message": "获取 Apex 日志成功",
  "data": {
    "success": true,
    "logInfo": {
      "id": "07L...",
      "application": "Visualforce",
      "durationMilliseconds": 1234,
      "location": "ApexPage",
      "logLength": 5678,
      "operation": "PageView",
      "startTime": "2026-01-28 10:00:00",
      "status": "Completed"
    }
  }
}

4. 获取 Apex 类成员

接口: GET /salesforce/tooling/devtools/class-member

功能: 获取 Apex 类成员信息

请求参数:

参数名 类型 必填 说明
apexClassId String Apex 类 ID

响应示例:

{
  "code": 200,
  "message": "获取 Apex 类成员成功",
  "data": {
    "success": true,
    "classMember": {
      "id": "01p...",
      "body": "public class MyClass {\n    public void hello() {\n        System.debug('Hello World');\n    }\n}",
      "bodyCrc": 1234567890.0,
      "contentEntityId": "01p...",
      "contentType": "ApexClass",
      "symbolTable": {
        "name": "MyClass",
        "methods": [
          {
            "name": "hello",
            "returnType": "void",
            "parameters": []
          }
        ]
      }
    }
  }
}

5. 获取 Apex 触发器成员

接口: GET /salesforce/tooling/devtools/trigger-member

功能: 获取 Apex 触发器成员信息

请求参数:

参数名 类型 必填 说明
apexTriggerId String Apex 触发器 ID

响应示例:

{
  "code": 200,
  "message": "获取 Apex 触发器成员成功",
  "data": {
    "success": true,
    "triggerMember": {
      "id": "01q...",
      "body": "trigger MyTrigger on Account (before insert) {\n    System.debug('Account created');\n}",
      "bodyCrc": 1234567890.0,
      "contentEntityId": "01q...",
      "contentType": "ApexTrigger",
      "symbolTable": {
        "name": "MyTrigger",
        "tableEnumOrId": "Account"
      }
    }
  }
}

6. 获取 Visualforce 页面成员

接口: GET /salesforce/tooling/devtools/page-member

功能: 获取 Visualforce 页面成员信息

请求参数:

参数名 类型 必填 说明
apexPageId String Visualforce 页面 ID

响应示例:

{
  "code": 200,
  "message": "获取 Visualforce 页面成员成功",
  "data": {
    "success": true,
    "pageMember": {
      "id": "066...",
      "body": "<apex:page>\n    <h1>Hello World</h1>\n</apex:page>",
      "bodyCrc": 1234567890.0,
      "contentEntityId": "066...",
      "contentType": "ApexPage"
    }
  }
}

7. 获取 Visualforce 组件成员

接口: GET /salesforce/tooling/devtools/component-member

功能: 获取 Visualforce 组件成员信息

请求参数:

参数名 类型 必填 说明
apexComponentId String Visualforce 组件 ID

响应示例:

{
  "code": 200,
  "message": "获取 Visualforce 组件成员成功",
  "data": {
    "success": true,
    "componentMember": {
      "id": "06a...",
      "body": "<apex:component>\n    <h1>Hello Component</h1>\n</apex:component>",
      "bodyCrc": 1234567890.0,
      "contentEntityId": "06a...",
      "contentType": "ApexComponent"
    }
  }
}

8. 获取开发工具操作日志

接口: GET /salesforce/tooling/devtools/operation-logs

功能: 查询开发工具操作日志

请求参数:

参数名 类型 必填 说明
operationType String 操作类型
metadataType String 元数据类型
startTime String 开始时间yyyy-MM-dd HH:mm:ss
endTime String 结束时间yyyy-MM-dd HH:mm:ss
pageNum Integer 页码(默认 1
pageSize Integer 每页大小(默认 10

响应示例:

{
  "code": 200,
  "message": "查询操作日志成功",
  "data": {
    "total": 100,
    "pageNum": 1,
    "pageSize": 10,
    "list": [
      {
        "id": 1,
        "operationType": "get_code_coverage",
        "metadataType": "ApexCodeCoverage",
        "metadataId": "01p...",
        "metadataName": "MyClass",
        "queryCondition": "{\"apexClassOrTriggerId\":null}",
        "resultCount": 100,
        "status": "success",
        "operationTime": "2026-01-28 10:00:00",
        "userId": "user001"
      }
    ]
  }
}

实现要点

关键实现逻辑

1. Service 层实现

@Service
@Slf4j
public class ToolingDevToolsServiceImpl implements IToolingDevToolsService {
    
    @Autowired
    private ToolingConnectionFactory connectionFactory;
    
    @Autowired
    private ToolingDevToolsOperationLogMapper logMapper;
    
    @Autowired
    private SessionManager sessionManager;
    
    @Override
    public List<ApexCodeCoverage> getApexCodeCoverage(String apexClassOrTriggerId) {
        try {
            ToolingConnection connection = connectionFactory.getConnection("source");
            
            SoqlBuilder builder = SoqlBuilder.newInstance()
                .select("Id, ApexClassOrTriggerId, ApexClassOrTrigger.Name, " +
                        "ApexTestClass.Id, ApexTestClass.Name, Coverage, " +
                        "NumLinesCovered, NumLinesUncovered, LocationsNotCovered")
                .from("ApexCodeCoverage");
            
            if (StringUtils.isNotBlank(apexClassOrTriggerId)) {
                builder.where("ApexClassOrTriggerId = :id")
                       .setParameter("id", apexClassOrTriggerId);
            }
            
            QueryResult result = connection.query(builder.build());
            
            List<ApexCodeCoverage> coverageList = convertToApexCodeCoverageList(result.getRecords());
            
            logOperation("get_code_coverage", "ApexCodeCoverage", 
                       null, null, 
                       Map.of("apexClassOrTriggerId", apexClassOrTriggerId),
                       coverageList.size(), 
                       true, null, null);
            
            return coverageList;
        } catch (ConnectionException e) {
            log.error("获取代码覆盖率失败", e);
            throw new SalesforceAuthException("TOOLING_DEVTOOLS_002", "获取代码覆盖率失败: " + e.getMessage());
        }
    }
    
    @Override
    public ApexClassMember getApexClassMember(String apexClassId) {
        try {
            ToolingConnection connection = connectionFactory.getConnection("source");
            
            SoqlBuilder builder = SoqlBuilder.newInstance()
                .select("Id, Body, BodyCrc, ContentEntityId, ContentType, SymbolTable")
                .from("ApexClassMember")
                .where("ContentEntityId = :id")
                .setParameter("id", apexClassId);
            
            QueryResult result = connection.query(builder.build());
            
            if (result.getSize() == 0) {
                throw new SalesforceAuthException("TOOLING_DEVTOOLS_005", 
                                              "Apex 类成员不存在");
            }
            
            ApexClassMember member = convertToApexClassMember(result.getRecords()[0]);
            
            logOperation("get_class_member", "ApexClassMember", 
                       apexClassId, null, 
                       Map.of("apexClassId", apexClassId),
                       1, 
                       true, null, null);
            
            return member;
        } catch (ConnectionException e) {
            log.error("获取 Apex 类成员失败", e);
            throw new SalesforceAuthException("TOOLING_DEVTOOLS_002", "获取 Apex 类成员失败: " + e.getMessage());
        }
    }
    
    private void logOperation(String operationType, String metadataType, 
                          String metadataId, String metadataName,
                          Map<String, Object> queryCondition, int resultCount,
                          boolean success, String errorCode, String errorMessage) {
        CompletableFuture.runAsync(() -> {
            try {
                ToolingDevToolsOperationLog log = new ToolingDevToolsOperationLog();
                log.setOperationType(operationType);
                log.setMetadataType(metadataType);
                log.setMetadataId(metadataId);
                log.setMetadataName(metadataName);
                log.setQueryCondition(JsonUtils.toJson(queryCondition));
                log.setResultCount(resultCount);
                log.setStatus(success ? "success" : "failed");
                log.setErrorCode(errorCode);
                log.setErrorMessage(errorMessage);
                log.setOperationTime(LocalDateTime.now());
                log.setUserId(SecurityUtils.getUserId());
                
                logMapper.insert(log);
            } catch (Exception e) {
                log.error("记录操作日志失败", e);
            }
        }, executor);
    }
}

2. Controller 层实现

@RestController
@RequestMapping("/salesforce/tooling/devtools")
@Slf4j
public class ToolingDevToolsController {
    
    @Autowired
    private IToolingDevToolsService toolingDevToolsService;
    
    @GetMapping("/code-coverage")
    public R<ApexCodeCoverageResult> getApexCodeCoverage(
            @RequestParam(required = false) String apexClassOrTriggerId) {
        try {
            List<ApexCodeCoverage> coverageList = 
                toolingDevToolsService.getApexCodeCoverage(apexClassOrTriggerId);
            
            ApexCodeCoverageResult result = new ApexCodeCoverageResult();
            result.setSuccess(true);
            result.setTotal(coverageList.size());
            result.setCoverageResults(convertToResultList(coverageList));
            
            return R.ok(result);
        } catch (SalesforceAuthException e) {
            log.error("获取代码覆盖率失败", e);
            return R.fail(e.getCode(), e.getMessage());
        } catch (SalesforceLoginException e) {
            log.error("用户未登录", e);
            return R.fail(e.getCode(), e.getMessage());
        } catch (Exception e) {
            log.error("获取代码覆盖率失败", e);
            return R.fail("TOOLING_DEVTOOLS_002", "获取代码覆盖率失败: " + e.getMessage());
        }
    }
    
    @GetMapping("/class-member")
    public R<ApexMemberResult> getApexClassMember(
            @RequestParam String apexClassId) {
        try {
            ApexClassMember member = 
                toolingDevToolsService.getApexClassMember(apexClassId);
            
            ApexMemberResult result = new ApexMemberResult();
            result.setSuccess(true);
            result.setClassMember(convertToResult(member));
            
            return R.ok(result);
        } catch (SalesforceAuthException e) {
            log.error("获取 Apex 类成员失败", e);
            return R.fail(e.getCode(), e.getMessage());
        } catch (SalesforceLoginException e) {
            log.error("用户未登录", e);
            return R.fail(e.getCode(), e.getMessage());
        } catch (Exception e) {
            log.error("获取 Apex 类成员失败", e);
            return R.fail("TOOLING_DEVTOOLS_002", "获取 Apex 类成员失败: " + e.getMessage());
        }
    }
}

异常处理设计

异常转换规则

  1. Session 无效异常

    • Salesforce 异常:INVALID_SESSION_ID
    • 转换为:SalesforceAuthException
    • 错误码:TOOLING_DEVTOOLS_001
  2. 查询失败异常

    • Salesforce 异常:QUERY_EXECUTION_ERROR
    • 转换为:SalesforceAuthException
    • 错误码:TOOLING_DEVTOOLS_002
  3. 网络超时异常

    • Salesforce 异常:NETWORK_TIMEOUT
    • 转换为:SalesforceAuthException
    • 错误码:TOOLING_DEVTOOLS_003
  4. 权限不足异常

    • Salesforce 异常:INSUFFICIENT_ACCESS
    • 转换为:SalesforceAuthException
    • 错误码:TOOLING_DEVTOOLS_004
  5. 记录不存在异常

    • Salesforce 异常:RECORD_NOT_FOUND
    • 转换为:SalesforceAuthException
    • 错误码:TOOLING_DEVTOOLS_005
  6. 未登录异常

    • 场景SessionManager 返回 null 或 Session 已过期
    • 转换为:SalesforceLoginException
    • 错误码:TOOLING_DEVTOOLS_006

错误码枚举

public enum ToolingDevToolsErrorCode {
    TOOLING_DEVTOOLS_001("TOOLING_DEVTOOLS_001", "Session 无效或已过期"),
    TOOLING_DEVTOOLS_002("TOOLING_DEVTOOLS_002", "查询失败"),
    TOOLING_DEVTOOLS_003("TOOLING_DEVTOOLS_003", "网络超时"),
    TOOLING_DEVTOOLS_004("TOOLING_DEVTOOLS_004", "权限不足"),
    TOOLING_DEVTOOLS_005("TOOLING_DEVTOOLS_005", "记录不存在"),
    TOOLING_DEVTOOLS_006("TOOLING_DEVTOOLS_006", "用户未登录");

    private final String code;
    private final String message;

    ToolingDevToolsErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

性能优化设计

1. 连接复用

  • 复用 ToolingConnectionFactory 的连接缓存机制
  • 避免重复创建连接,提高性能

2. 异步日志记录

  • 使用 CompletableFuture 异步记录操作日志
  • 不影响主流程性能

3. 查询优化

  • 使用 SoqlBuilder 构建 SOQL 查询
  • 只查询需要的字段,减少数据传输量
  • 合理使用索引,提高查询性能

4. 分页查询

  • 操作日志查询支持分页
  • 避免一次性查询大量数据

安全设计

1. 权限控制

  • 所有接口需要登录认证
  • 可选增加细粒度权限控制

2. 参数校验

  • 使用 Jakarta Validation 对请求参数进行校验
  • 防止 SQL 注入和 XSS 攻击

3. 敏感数据脱敏

  • 日志中不记录敏感信息如密码、Token 等)

4. 异常信息脱敏

  • 返回给前端的异常信息不包含敏感信息

相关文档