25 KiB
25 KiB
提示词文档
元数据
- 需求编号:004
- 子需求编号:004-03
- 子需求名称:开发工具功能
- 阶段:阶段 5:提示词生成
- 创建时间:2026-01-28
- 状态:已完成
引用真源
1. 需求文档
- 需求文档
- 核心功能点:
- 诊断和调试:ApexCodeCoverage、ApexTestQueueItem、ApexLog
- 查询和搜索:ApexClassMember、ApexTriggerMember、ApexPageMember、ApexComponentMember
- 依赖项:
- 子需求 004-01:连接管理
- SessionManager、ToolingConnectionFactory、AbstractConnectionFactory
- datai-salesforce-common 模块的异常类
- tooling.jar 中的 ToolingConnection、ApexCodeCoverage、ApexTestQueueItem、ApexLog、ApexClassMember 等
2. 设计文档
- 设计文档
- 架构设计:
- 系统架构图:Controller → Service → Factory → Session → Salesforce Tooling API
- 模块架构图:ToolingDevToolsController → IToolingDevToolsService → ToolingDevToolsServiceImpl
- 数据流图:完整的请求-响应流程
- 技术方案:
- 技术选型:Spring Boot、MyBatis Plus、MySQL、tooling.jar
- 核心算法:代码覆盖率查询、测试队列查询、成员信息查询、异步日志记录
- 集成方案:与 ToolingConnectionFactory、SessionManager、MyBatis Plus 集成
- 数据模型:
- 数据库表:datai_tooling_devtools_operation_log
- 实体类:ToolingDevToolsOperationLog、ApexCodeCoverageResult、ApexTestQueueItemResult、ApexLogResult、ApexMemberResult
- 接口设计:
- 8 个 RESTful API 接口
- 获取代码覆盖率、获取测试队列项、获取 Apex 日志、获取类成员、获取触发器成员、获取页面成员、获取组件成员、获取操作日志
- 实现要点:
- 关键实现逻辑:Service 层和 Controller 层实现
- 异常处理:统一异常处理,错误码枚举
- 性能优化:连接复用、异步日志记录、查询优化
- 安全设计:权限控制、参数校验、敏感数据脱敏
3. 决策记录
- 决策记录
- 决策内容:
- 选择方案 1:Service + ToolingConnectionFactory + 异步日志记录
- 复用现有基础设施,开发效率高
- 异步日志记录,不影响主流程性能
- 与项目现有架构保持一致
- 替代方案:
- 方案 2:同步日志记录
- 方案 3:仅记录失败日志
- 方案 4:使用消息队列记录日志
- 方案 5:文件日志记录
需求描述
核心任务
实现 Tooling API 开发工具功能,包括以下功能:
-
诊断和调试功能:
- 获取 Apex 代码覆盖率
- 获取 Apex 测试队列项
- 获取 Apex 日志
-
查询和搜索功能:
- 获取 Apex 类成员
- 获取 Apex 触发器成员
- 获取 Visualforce 页面成员
- 获取 Visualforce 组件成员
-
操作日志查询:
- 查询开发工具操作日志
技术要求
- 复用子需求 004-01 的 ToolingConnectionFactory 获取连接
- 使用 SessionManager 管理会话
- 使用 SoqlBuilder 构建 SOQL 查询
- 使用 MyBatis Plus 进行数据库操作
- 异步记录操作日志,不影响主流程性能
- 统一异常处理,使用 datai-salesforce-common 模块的异常类
- 提供 RESTful API 接口
设计方案
架构设计
- Controller 层:ToolingDevToolsController
- Service 层:IToolingDevToolsService、ToolingDevToolsServiceImpl
- Factory 层:ToolingConnectionFactory(复用子需求 004-01)
- Session 层:SessionManager(复用 datai-salesforce-auth)
- 数据访问层:ToolingDevToolsOperationLogMapper
- 数据层:MySQL(datai_tooling_devtools_operation_log 表)
核心算法
-
代码覆盖率查询算法:
- 使用 SoqlBuilder 构建 SOQL 查询
- 支持可选的 apexClassOrTriggerId 过滤条件
- 返回 ApexCodeCoverage 列表
-
测试队列查询算法:
- 使用 SoqlBuilder 构建 SOQL 查询
- 支持可选的 apexClassId 过滤条件
- 返回 ApexTestQueueItem 列表
-
成员信息查询算法:
- 使用 SoqlBuilder 构建 SOQL 查询
- 根据 ID 查询单个成员
- 返回 ApexClassMember、ApexTriggerMember、ApexPageMember、ApexComponentMember
-
异步日志记录算法:
- 使用 CompletableFuture 异步记录操作日志
- 不影响主流程性能
- 记录操作类型、元数据类型、查询条件、结果数量、状态、错误信息
数据模型
- 数据库表:datai_tooling_devtools_operation_log
- 实体类:
- ToolingDevToolsOperationLog(开发工具操作日志实体)
- ApexCodeCoverageResult(代码覆盖率结果)
- ApexTestQueueItemResult(测试队列项结果)
- ApexLogResult(日志结果)
- ApexMemberResult(成员结果)
接口设计
- RESTful API 接口:
- GET /salesforce/tooling/devtools/code-coverage(获取代码覆盖率)
- GET /salesforce/tooling/devtools/test-queue-items(获取测试队列项)
- GET /salesforce/tooling/devtools/apex-log(获取 Apex 日志)
- GET /salesforce/tooling/devtools/class-member(获取 Apex 类成员)
- GET /salesforce/tooling/devtools/trigger-member(获取 Apex 触发器成员)
- GET /salesforce/tooling/devtools/page-member(获取 Visualforce 页面成员)
- GET /salesforce/tooling/devtools/component-member(获取 Visualforce 组件成员)
- GET /salesforce/tooling/devtools/operation-logs(查询操作日志)
异常处理
- 异常转换规则:
- Session 无效 → SalesforceAuthException(TOOLING_DEVTOOLS_001)
- 查询失败 → SalesforceAuthException(TOOLING_DEVTOOLS_002)
- 网络超时 → SalesforceAuthException(TOOLING_DEVTOOLS_003)
- 权限不足 → SalesforceAuthException(TOOLING_DEVTOOLS_004)
- 记录不存在 → SalesforceAuthException(TOOLING_DEVTOOLS_005)
- 未登录 → SalesforceLoginException(TOOLING_DEVTOOLS_006)
性能优化
- 连接复用:复用 ToolingConnectionFactory 的连接缓存
- 异步日志记录:使用 CompletableFuture 异步记录操作日志
- 查询优化:使用 SoqlBuilder 构建 SOQL,只查询需要的字段
- 分页查询:操作日志查询支持分页
安全设计
- 权限控制:所有接口需要登录认证
- 参数校验:使用 Jakarta Validation 对请求参数进行校验
- 敏感数据脱敏:日志中不记录敏感信息
- 异常信息脱敏:返回给前端的异常信息不包含敏感信息
输出格式
1. Service 接口
package com.datai.tooling.service;
import java.util.List;
import com.datai.tooling.model.domain.ApexCodeCoverage;
import com.datai.tooling.model.domain.ApexTestQueueItem;
import com.datai.tooling.model.domain.ApexLog;
import com.datai.tooling.model.domain.ApexClassMember;
import com.datai.tooling.model.domain.ApexTriggerMember;
import com.datai.tooling.model.domain.ApexPageMember;
import com.datai.tooling.model.domain.ApexComponentMember;
public interface IToolingDevToolsService {
List<ApexCodeCoverage> getApexCodeCoverage(String apexClassOrTriggerId);
List<ApexTestQueueItem> getApexTestQueueItems(String apexClassId);
ApexLog getApexLog(String logId);
ApexClassMember getApexClassMember(String apexClassId);
ApexTriggerMember getApexTriggerMember(String apexTriggerId);
ApexPageMember getApexPageMember(String apexPageId);
ApexComponentMember getApexComponentMember(String apexComponentId);
}
2. Service 实现类
package com.datai.tooling.service.impl;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.datai.tooling.factory.ToolingConnectionFactory;
import com.datai.tooling.mapper.ToolingDevToolsOperationLogMapper;
import com.datai.tooling.model.domain.ToolingDevToolsOperationLog;
import com.datai.tooling.service.IToolingDevToolsService;
import com.datai.salesforce.auth.exception.SalesforceAuthException;
import com.datai.salesforce.auth.exception.SalesforceLoginException;
import com.datai.salesforce.auth.manager.SessionManager;
import com.datai.salesforce.auth.model.SessionInfo;
import com.datai.salesforce.common.exception.SalesforceException;
import com.datai.salesforce.common.util.JsonUtils;
import com.datai.salesforce.common.util.SecurityUtils;
import com.datai.salesforce.common.util.SoqlBuilder;
import com.sforce.soap.tooling.ToolingConnection;
import com.sforce.soap.tooling.QueryResult;
import com.sforce.soap.tooling.ApexCodeCoverage;
import com.sforce.soap.tooling.ApexTestQueueItem;
import com.sforce.soap.tooling.ApexLog;
import com.sforce.soap.tooling.ApexClassMember;
import com.sforce.soap.tooling.ApexTriggerMember;
import com.sforce.soap.tooling.ApexPageMember;
import com.sforce.soap.tooling.ApexComponentMember;
import com.sforce.ws.ConnectionException;
@Service
public class ToolingDevToolsServiceImpl implements IToolingDevToolsService {
private static final Logger log = LoggerFactory.getLogger(ToolingDevToolsServiceImpl.class);
@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);
}
}
3. Controller 类
package com.datai.tooling.controller;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.datai.common.core.domain.R;
import com.datai.tooling.model.domain.ApexCodeCoverage;
import com.datai.tooling.model.domain.ApexTestQueueItem;
import com.datai.tooling.model.domain.ApexLog;
import com.datai.tooling.model.domain.ApexClassMember;
import com.datai.tooling.model.domain.ApexTriggerMember;
import com.datai.tooling.model.domain.ApexPageMember;
import com.datai.tooling.model.domain.ApexComponentMember;
import com.datai.tooling.model.domain.result.ApexCodeCoverageResult;
import com.datai.tooling.model.domain.result.ApexTestQueueItemResult;
import com.datai.tooling.model.domain.result.ApexLogResult;
import com.datai.tooling.model.domain.result.ApexMemberResult;
import com.datai.tooling.service.IToolingDevToolsService;
import com.datai.salesforce.auth.exception.SalesforceAuthException;
import com.datai.salesforce.auth.exception.SalesforceLoginException;
@RestController
@RequestMapping("/salesforce/tooling/devtools")
public class ToolingDevToolsController {
private static final Logger log = LoggerFactory.getLogger(ToolingDevToolsController.class);
@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(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(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());
}
}
}
4. DTO 类
package com.datai.tooling.model.dto;
import lombok.Data;
@Data
public class ApexCodeCoverageRequest {
private String apexClassOrTriggerId;
}
@Data
public class ApexClassMemberRequest {
private String apexClassId;
}
@Data
public class ApexTriggerMemberRequest {
private String apexTriggerId;
}
@Data
public class ApexPageMemberRequest {
private String apexPageId;
}
@Data
public class ApexComponentMemberRequest {
private String apexComponentId;
}
5. VO 类
package com.datai.tooling.model.domain.result;
import java.util.List;
import lombok.Data;
@Data
public class ApexCodeCoverageResult {
private boolean success;
private int total;
private List<ApexCodeCoverage> coverageResults;
}
@Data
public class ApexTestQueueItemResult {
private boolean success;
private int total;
private List<ApexTestQueueItem> queueItems;
}
@Data
public class ApexLogResult {
private boolean success;
private ApexLog logInfo;
}
@Data
public class ApexMemberResult {
private boolean success;
private ApexClassMember classMember;
private ApexTriggerMember triggerMember;
private ApexPageMember pageMember;
private ApexComponentMember componentMember;
}
6. Enums 类
package com.datai.tooling.enums;
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;
}
}
7. Mapper 接口
package com.datai.tooling.mapper;
import java.util.List;
import java.time.LocalDateTime;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.datai.tooling.model.domain.ToolingDevToolsOperationLog;
@Mapper
public interface ToolingDevToolsOperationLogMapper extends BaseMapper<ToolingDevToolsOperationLog> {
List<ToolingDevToolsOperationLog> queryLogs(
@Param("operationType") String operationType,
@Param("metadataType") String metadataType,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("offset") int offset,
@Param("pageSize") int pageSize
);
}
代码规范
命名规范
- 类名:使用 PascalCase,如 ToolingDevToolsController
- 方法名:使用 camelCase,如 getApexCodeCoverage
- 变量名:使用 camelCase,如 apexClassOrTriggerId
- 常量名:使用 UPPER_SNAKE_CASE,如 OPERATION_TYPE_GET_CODE_COVERAGE
- 包名:使用小写字母,如 com.datai.tooling.service
注释规范
- 类注释:使用 Javadoc 格式,说明类的用途
- 方法注释:使用 Javadoc 格式,说明方法的参数、返回值、异常
- 字段注释:使用 Javadoc 格式,说明字段的用途
- 行内注释:使用 // 注释,说明复杂的逻辑
导入规范
- 按类型分组:先导入 java.,再导入 javax.,再导入 org.*,最后导入项目包
- 避免通配符导入:避免使用 * 通配符导入
- 移除未使用的导入:及时移除未使用的导入
包结构
com.datai.tooling
├── controller
│ └── ToolingDevToolsController.java
├── service
│ ├── IToolingDevToolsService.java
│ └── impl
│ └── ToolingDevToolsServiceImpl.java
├── mapper
│ └── ToolingDevToolsOperationLogMapper.java
├── model
│ ├── domain
│ │ ├── ToolingDevToolsOperationLog.java
│ │ ├── ApexCodeCoverageResult.java
│ │ ├── ApexTestQueueItemResult.java
│ │ ├── ApexLogResult.java
│ │ └── ApexMemberResult.java
│ └── dto
│ ├── ApexCodeCoverageRequest.java
│ ├── ApexClassMemberRequest.java
│ ├── ApexTriggerMemberRequest.java
│ ├── ApexPageMemberRequest.java
│ └── ApexComponentMemberRequest.java
└── enums
└── ToolingDevToolsErrorCode.java
测试要求
单元测试
- 覆盖率要求:代码覆盖率至少达到 80%
- 测试框架:使用 JUnit 5 + Mockito
- 测试场景:
- 测试获取代码覆盖率成功场景
- 测试获取代码覆盖率失败场景(Session 无效)
- 测试获取测试队列项成功场景
- 测试获取测试队列项失败场景(记录不存在)
- 测试获取 Apex 日志成功场景
- 测试获取 Apex 日志失败场景(记录不存在)
- 测试获取类成员成功场景
- 测试获取类成员失败场景(记录不存在)
- 测试获取触发器成员成功场景
- 测试获取触发器成员失败场景(记录不存在)
- 测试获取页面成员成功场景
- 测试获取页面成员失败场景(记录不存在)
- 测试获取组件成员成功场景
- 测试获取组件成员失败场景(记录不存在)
- 测试查询操作日志成功场景
- 测试异步日志记录场景
- 测试异常处理场景
集成测试
- 测试框架:使用 Spring Boot Test
- 测试场景:
- 测试完整的请求-响应流程
- 测试数据库操作
- 测试与 Salesforce Tooling API 的集成
- 测试异常处理
注意事项
-
复用现有基础设施:
- 必须复用子需求 004-01 的 ToolingConnectionFactory 获取连接
- 必须使用 SessionManager 管理会话
- 必须使用 SoqlBuilder 构建 SOQL 查询
-
异步日志记录:
- 必须使用 CompletableFuture 异步记录操作日志
- 异步日志记录不能影响主流程性能
-
异常处理:
- 必须使用统一的异常处理
- 必须使用 ToolingDevToolsErrorCode 枚举定义错误码
- 必须正确处理各种异常情况
-
参数校验:
- 必须使用 Jakarta Validation 对请求参数进行校验
- 必须防止 SQL 注入和 XSS 攻击
-
敏感数据脱敏:
- 日志中不记录敏感信息(如密码、Token 等)
- 返回给前端的异常信息不包含敏感信息
-
代码规范:
- 必须遵循项目的命名规范、注释规范、导入规范、包结构
- 必须使用 Lombok 注解简化代码
-
测试要求:
- 必须编写单元测试和集成测试
- 代码覆盖率必须达到 80%
- 必须测试各种场景(成功、失败、异常)
-
性能优化:
- 必须复用连接缓存,避免重复创建连接
- 必须使用异步日志记录,不影响主流程性能
- 必须优化查询,只查询需要的字段
-
安全设计:
- 所有接口必须需要登录认证
- 必须进行参数校验
- 必须进行敏感数据脱敏
-
数据库操作:
- 必须使用 MyBatis Plus 进行数据库操作
- 所有开发工具操作必须记录到 datai_tooling_devtools_operation_log 表
- 必须正确处理数据库异常