40 KiB
40 KiB
设计文档 - Apex 代码编译和执行
元数据
- 需求编号:002-02
- 创建时间:2026-02-02
- 创建人:AI Assistant
- 状态:进行中
设计概述
本设计文档描述了 Salesforce Apex 代码编译和执行功能的完整技术方案。该功能允许用户通过 REST API 接口编译 Apex 类、编译 Apex 触发器、编译并运行测试、执行匿名 Apex 代码,并支持查询编译历史、测试结果和代码覆盖率。
核心设计原则:
- 复用已有的 ApexConnectionFactory 连接管理机制
- 直接使用 apex.jar 中的原生类,避免不必要的二次封装
- 将编译和测试结果持久化到数据库,支持历史查询
- 使用 ReentrantLock 保证多线程安全
- 固定使用 source org 类型
架构设计
系统架构
┌─────────────────────────────────────────────────────────────┐
│ 前端应用层 │
│ (Vue 3 + Element Plus) │
└───────────────────────┬─────────────────────────────────────┘
│ HTTP/REST
┌───────────────────────▼─────────────────────────────────────┐
│ Controller 层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ApexCompile │ │ApexExecute │ │ApexQuery │ │
│ │Controller │ │Controller │ │Controller │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼────────────────┼────────────────┼────────────────┘
│ │ │
┌─────────▼────────────────▼────────────────▼────────────────┐
│ Service 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IApexCompileService │ │
│ │ - compileClasses(String[]) │ │
│ │ - compileTriggers(String[]) │ │
│ │ - compileAndTest(CompileAndTestRequest) │ │
│ │ - executeAnonymous(String) │ │
│ │ - queryCompileHistory(...) │ │
│ │ - queryTestResults(...) │ │
│ │ - queryCodeCoverage(...) │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼─────────────────────────────────┐ │
│ │ ApexCompileServiceImpl │ │
│ │ - 实现所有编译和执行逻辑 │ │
│ │ - 集成 ApexConnectionFactory │ │
│ │ - 异常处理和结果转换 │ │
│ │ - 数据库持久化 │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
┌─────────▼──────────────────────────────────────────────────┐
│ Factory 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ApexConnectionFactory │ │
│ │ - 获取 SoapConnection 实例 │ │
│ │ - 连接缓存管理 │ │
│ │ - 会话管理 │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
┌─────────▼──────────────────────────────────────────────────┐
│ Salesforce API │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SoapConnection │ │
│ │ - compileClasses() │ │
│ │ - compileTriggers() │ │
│ │ - compileAndTest() │ │
│ │ - executeAnonymous() │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
┌─────────▼──────────────────────────────────────────────────┐
│ 数据持久化层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ApexCompile │ │ApexTest │ │ApexCode │ │
│ │History │ │Result │ │Coverage │ │
│ │Mapper │ │Mapper │ │Mapper │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
模块架构
模块: datai-salesforce-apex
包结构:
com.datai.apex
├── controller
│ ├── ApexCompileController.java # 编译相关接口
│ ├── ApexExecuteController.java # 执行相关接口
│ └── ApexQueryController.java # 查询相关接口
├── service
│ ├── IApexCompileService.java # 服务接口
│ └── impl
│ └── ApexCompileServiceImpl.java # 服务实现
├── mapper
│ ├── ApexCompileHistoryMapper.java # 编译历史 Mapper
│ ├── ApexTestResultMapper.java # 测试结果 Mapper
│ ├── ApexTestFailureMapper.java # 测试失败 Mapper
│ └── ApexCodeCoverageMapper.java # 代码覆盖率 Mapper
├── domain
│ ├── ApexCompileHistory.java # 编译历史实体
│ ├── ApexTestResult.java # 测试结果实体
│ ├── ApexTestFailure.java # 测试失败实体
│ └── ApexCodeCoverage.java # 代码覆盖率实体
├── model
│ ├── dto
│ │ ├── CompileClassesDto.java # 编译类请求 DTO
│ │ ├── CompileTriggersDto.java # 编译触发器请求 DTO
│ │ ├── CompileAndTestDto.java # 编译并测试请求 DTO
│ │ ├── ExecuteAnonymousDto.java # 执行匿名代码请求 DTO
│ │ └── RunTestsRequestDto.java # 运行测试请求 DTO
│ └── vo
│ ├── CompileClassResultVo.java # 编译类结果 VO
│ ├── CompileTriggerResultVo.java # 编译触发器结果 VO
│ ├── CompileAndTestResultVo.java # 编译并测试结果 VO
│ ├── ExecuteAnonymousResultVo.java # 执行匿名代码结果 VO
│ └── RunTestsResultVo.java # 运行测试结果 VO
└── factory
└── ApexConnectionFactory.java # 连接工厂(已存在)
数据流设计
1. 编译 Apex 类数据流
用户请求
│
▼
┌─────────────────┐
│ ApexCompile │
│ Controller │
└────────┬────────┘
│ 1. 接收请求参数
│ 2. 参数校验
▼
┌─────────────────┐
│ IApexCompile │
│ Service │
└────────┬────────┘
│ 3. 调用 compileClasses()
▼
┌─────────────────┐
│ ApexConnection │
│ Factory │
└────────┬────────┘
│ 4. 获取 SoapConnection
▼
┌─────────────────┐
│ SoapConnection │
│ compileClasses()│
└────────┬────────┘
│ 5. 调用 Salesforce API
▼
┌─────────────────┐
│ Salesforce │
│ 编译服务 │
└────────┬────────┘
│ 6. 返回编译结果
▼
┌─────────────────┐
│ 结果转换 │
│ 和持久化 │
└────────┬────────┘
│ 7. 保存到数据库
▼
┌─────────────────┐
│ 返回响应给前端 │
└─────────────────┘
2. 执行匿名 Apex 代码数据流
用户请求
│
▼
┌─────────────────┐
│ ApexExecute │
│ Controller │
└────────┬────────┘
│ 1. 接收请求参数
│ 2. 参数校验
▼
┌─────────────────┐
│ IApexCompile │
│ Service │
└────────┬────────┘
│ 3. 调用 executeAnonymous()
▼
┌─────────────────┐
│ ApexConnection │
│ Factory │
└────────┬────────┘
│ 4. 获取 SoapConnection
▼
┌─────────────────┐
│ SoapConnection │
│ executeAnonymous│
└────────┬────────┘
│ 5. 调用 Salesforce API
▼
┌─────────────────┐
│ Salesforce │
│ 执行服务 │
└────────┬────────┘
│ 6. 返回执行结果
▼
┌─────────────────┐
│ 结果转换 │
│ 和持久化 │
└────────┬────────┘
│ 7. 保存到数据库
▼
┌─────────────────┐
│ 返回响应给前端 │
└─────────────────┘
技术方案
技术选型
| 技术组件 | 版本/说明 | 选择理由 |
|---|---|---|
| Spring Boot | 3.x | 应用框架,提供依赖注入、AOP、事务管理等功能 |
| Spring Security | 6.x | 权限控制,支持方法级权限控制 |
| MyBatis Plus | 3.5.x | ORM 框架,简化数据库操作 |
| MySQL | 8.x | 关系型数据库,存储编译和测试结果 |
| apex.jar | Salesforce WSC | Salesforce Apex API Java 客户端 |
| Lombok | 1.18.x | 简化 Java 代码,减少样板代码 |
| Swagger | 3.x | API 文档生成 |
| ReentrantLock | Java 并发包 | 保证多线程安全 |
核心算法设计
1. 编译结果处理算法
public CompileClassResultVo[] processCompileResults(CompileClassResult[] results) {
List<CompileClassResultVo> voList = new ArrayList<>();
for (CompileClassResult result : results) {
CompileClassResultVo vo = new CompileClassResultVo();
vo.setSuccess(result.isSuccess());
vo.setId(result.getId());
vo.setName(result.getName());
vo.setProblem(result.getProblem());
vo.setLine(result.getLine());
vo.setColumn(result.getColumn());
vo.setBodyCrc(result.getBodyCrc());
// 处理编译问题数组
if (result.getProblems() != null) {
List<CompileIssueVo> issues = new ArrayList<>();
for (CompileIssue issue : result.getProblems()) {
CompileIssueVo issueVo = new CompileIssueVo();
issueVo.setMessage(issue.getMessage());
issueVo.setLine(issue.getLine());
issueVo.setColumn(issue.getColumn());
issues.add(issueVo);
}
vo.setProblems(issues);
}
// 处理编译警告数组
if (result.getWarnings() != null) {
List<CompileIssueVo> warnings = new ArrayList<>();
for (CompileIssue warning : result.getWarnings()) {
CompileIssueVo warningVo = new CompileIssueVo();
warningVo.setMessage(warning.getMessage());
warningVo.setLine(warning.getLine());
warningVo.setColumn(warning.getColumn());
warnings.add(warningVo);
}
vo.setWarnings(warnings);
}
voList.add(vo);
}
return voList.toArray(new CompileClassResultVo[0]);
}
2. 代码覆盖率计算算法
public double calculateCoveragePercent(CodeCoverageResult coverage) {
int totalLocations = coverage.getNumLocations();
int notCoveredLocations = coverage.getNumLocationsNotCovered();
if (totalLocations == 0) {
return 0.0;
}
double coveragePercent = (1.0 - (double) notCoveredLocations / totalLocations) * 100;
return Math.round(coveragePercent * 100.0) / 100.0; // 保留两位小数
}
3. 测试结果处理算法
public RunTestsResultVo processRunTestsResult(RunTestsResult result) {
RunTestsResultVo vo = new RunTestsResultVo();
vo.setNumTestsRun(result.getNumTestsRun());
vo.setNumFailures(result.getNumFailures());
vo.setTotalTime(result.getTotalTime());
vo.setApexLogId(result.getApexLogId());
// 处理成功的测试
if (result.getSuccesses() != null) {
List<RunTestSuccessVo> successes = new ArrayList<>();
for (RunTestSuccess success : result.getSuccesses()) {
RunTestSuccessVo successVo = new RunTestSuccessVo();
successVo.setMethodName(success.getMethodName());
successVo.setName(success.getName());
successVo.setTime(success.getTime());
successVo.setId(success.getId());
successes.add(successVo);
}
vo.setSuccesses(successes);
}
// 处理失败的测试
if (result.getFailures() != null) {
List<RunTestFailureVo> failures = new ArrayList<>();
for (RunTestFailure failure : result.getFailures()) {
RunTestFailureVo failureVo = new RunTestFailureVo();
failureVo.setMethodName(failure.getMethodName());
failureVo.setName(failure.getName());
failureVo.setMessage(failure.getMessage());
failureVo.setStackTrace(failure.getStackTrace());
failureVo.setType(failure.getType());
failures.add(failureVo);
}
vo.setFailures(failures);
}
// 处理代码覆盖率
if (result.getCodeCoverage() != null) {
List<CodeCoverageResultVo> coverages = new ArrayList<>();
for (CodeCoverageResult coverage : result.getCodeCoverage()) {
CodeCoverageResultVo coverageVo = new CodeCoverageResultVo();
coverageVo.setId(coverage.getId());
coverageVo.setName(coverage.getName());
coverageVo.setType(coverage.getType());
coverageVo.setNamespace(coverage.getNamespace());
coverageVo.setNumLocations(coverage.getNumLocations());
coverageVo.setNumLocationsNotCovered(coverage.getNumLocationsNotCovered());
coverageVo.setCoveragePercent(calculateCoveragePercent(coverage));
coverages.add(coverageVo);
}
vo.setCodeCoverage(coverages);
}
return vo;
}
集成方案设计
1. Salesforce API 集成
集成方式: 使用 Salesforce WSC (Web Service Connector) 库
连接获取:
@Autowired
private ApexConnectionFactory apexConnectionFactory;
public SoapConnection getConnection() throws SalesforceAuthException {
return apexConnectionFactory.getConnection("source");
}
API 调用:
// 编译 Apex 类
CompileClassResult[] results = connection.compileClasses(classes);
// 编译 Apex 触发器
CompileTriggerResult[] results = connection.compileTriggers(triggers);
// 编译并测试
CompileAndTestResult result = connection.compileAndTest(request);
// 执行匿名 Apex 代码
ExecuteAnonymousResult result = connection.executeAnonymous(code);
2. 数据库集成
持久化策略:
- 每次编译操作都记录到
ApexCompileHistory表 - 每次测试操作都记录到
ApexTestResult表 - 测试失败详情记录到
ApexTestFailure表 - 代码覆盖率记录到
ApexCodeCoverage表
事务管理:
@Transactional(rollbackFor = Exception.class)
public void saveCompileHistory(CompileClassResultVo[] results, String codeContent) {
// 保存编译历史
ApexCompileHistory history = new ApexCompileHistory();
history.setCompileType("class");
history.setCodeContent(codeContent);
history.setCompileSuccess(isAllSuccess(results));
history.setCompileProblem(getCompileProblems(results));
history.setCompileTime(new Date());
history.setUserId(SecurityUtils.getUserId());
apexCompileHistoryMapper.insert(history);
}
数据模型
数据库表设计
1. Apex 编译历史表 (datai_apex_compile_history)
CREATE TABLE datai_apex_compile_history (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
compile_type VARCHAR(20) NOT NULL COMMENT '编译类型(class/trigger/compile_and_test)',
code_content TEXT COMMENT '代码内容',
compile_success TINYINT(1) DEFAULT 0 COMMENT '编译是否成功',
compile_problem TEXT COMMENT '编译问题描述',
compile_time DATETIME COMMENT '编译时间',
user_id VARCHAR(50) COMMENT '用户ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
INDEX idx_compile_type (compile_type),
INDEX idx_user_id (user_id),
INDEX idx_compile_time (compile_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Apex编译历史表';
2. Apex 测试结果表 (datai_apex_test_result)
CREATE TABLE datai_apex_test_result (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
test_run_id VARCHAR(50) COMMENT '测试运行ID',
num_tests_run INT DEFAULT 0 COMMENT '运行的测试数量',
num_failures INT DEFAULT 0 COMMENT '失败的测试数量',
total_time DOUBLE DEFAULT 0 COMMENT '总执行时间(毫秒)',
apex_log_id VARCHAR(50) COMMENT 'Apex日志ID',
test_time DATETIME COMMENT '测试时间',
user_id VARCHAR(50) COMMENT '用户ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
INDEX idx_test_run_id (test_run_id),
INDEX idx_user_id (user_id),
INDEX idx_test_time (test_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Apex测试结果表';
3. Apex 测试失败详情表 (datai_apex_test_failure)
CREATE TABLE datai_apex_test_failure (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
test_result_id BIGINT NOT NULL COMMENT '测试结果ID(外键)',
class_name VARCHAR(255) COMMENT '类名',
method_name VARCHAR(255) COMMENT '方法名',
message TEXT COMMENT '失败消息',
stack_trace TEXT COMMENT '堆栈跟踪',
type VARCHAR(255) COMMENT '异常类型',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
INDEX idx_test_result_id (test_result_id),
FOREIGN KEY (test_result_id) REFERENCES datai_apex_test_result(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Apex测试失败详情表';
4. Apex 代码覆盖率表 (datai_apex_code_coverage)
CREATE TABLE datai_apex_code_coverage (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
test_result_id BIGINT NOT NULL COMMENT '测试结果ID(外键)',
name VARCHAR(255) COMMENT '类/触发器名称',
type VARCHAR(20) COMMENT '类型(Class/Trigger)',
namespace VARCHAR(255) COMMENT '命名空间',
num_locations INT DEFAULT 0 COMMENT '总位置数',
num_locations_not_covered INT DEFAULT 0 COMMENT '未覆盖的位置数',
coverage_percent DOUBLE DEFAULT 0 COMMENT '覆盖率百分比',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
INDEX idx_test_result_id (test_result_id),
INDEX idx_name (name),
INDEX idx_type (type),
FOREIGN KEY (test_result_id) REFERENCES datai_apex_test_result(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Apex代码覆盖率表';
实体类设计
1. ApexCompileHistory
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_compile_history")
public class ApexCompileHistory extends BaseEntity {
@TableField("compile_type")
private String compileType;
@TableField("code_content")
private String codeContent;
@TableField("compile_success")
private Boolean compileSuccess;
@TableField("compile_problem")
private String compileProblem;
@TableField("compile_time")
private Date compileTime;
@TableField("user_id")
private String userId;
}
2. ApexTestResult
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_test_result")
public class ApexTestResult extends BaseEntity {
@TableField("test_run_id")
private String testRunId;
@TableField("num_tests_run")
private Integer numTestsRun;
@TableField("num_failures")
private Integer numFailures;
@TableField("total_time")
private Double totalTime;
@TableField("apex_log_id")
private String apexLogId;
@TableField("test_time")
private Date testTime;
@TableField("user_id")
private String userId;
}
3. ApexTestFailure
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_test_failure")
public class ApexTestFailure extends BaseEntity {
@TableField("test_result_id")
private Long testResultId;
@TableField("class_name")
private String className;
@TableField("method_name")
private String methodName;
@TableField("message")
private String message;
@TableField("stack_trace")
private String stackTrace;
@TableField("type")
private String type;
}
4. ApexCodeCoverage
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_code_coverage")
public class ApexCodeCoverage extends BaseEntity {
@TableField("test_result_id")
private Long testResultId;
@TableField("name")
private String name;
@TableField("type")
private String type;
@TableField("namespace")
private String namespace;
@TableField("num_locations")
private Integer numLocations;
@TableField("num_locations_not_covered")
private Integer numLocationsNotCovered;
@TableField("coverage_percent")
private Double coveragePercent;
}
5. ApexCodeCoverageWarning 【2026-02-03 补充】
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("datai_apex_code_coverage_warning")
public class ApexCodeCoverageWarning extends BaseEntity {
@TableField("test_result_id")
private Long testResultId;
@TableField("name")
private String name;
@TableField("message")
private String message;
@TableField("coverage_percent")
private Double coveragePercent;
}
接口设计
RESTful API 设计
1. 编译 Apex 类
接口信息:
- 请求方式: POST
- 请求路径:
/api/apex/compile/classes - 权限要求:
apex:compile:classes
请求参数:
{
"classes": [
"public class MyClass { public String name; }",
"public class AnotherClass { public Integer count; }"
]
}
响应参数:
{
"code": 200,
"msg": "编译成功",
"data": {
"results": [
{
"success": true,
"id": "01pxx0000003DHb2AAG",
"name": "MyClass",
"problem": null,
"line": 0,
"column": 0,
"bodyCrc": 123456789,
"problems": [],
"warnings": []
}
],
"success": true
}
}
2. 编译 Apex 触发器
接口信息:
- 请求方式: POST
- 请求路径:
/api/apex/compile/triggers - 权限要求:
apex:compile:triggers
请求参数:
{
"triggers": [
"trigger AccountTrigger on Account (before insert) { System.debug('Account inserted'); }"
]
}
响应参数:
{
"code": 200,
"msg": "编译成功",
"data": {
"results": [
{
"success": true,
"id": "01qxx0000003DHb2AAG",
"name": "AccountTrigger",
"problem": null,
"line": 0,
"column": 0,
"bodyCrc": 123456789,
"problems": [],
"warnings": []
}
],
"success": true
}
}
3. 编译并测试
接口信息:
- 请求方式: POST
- 请求路径:
/api/apex/compile/compile-and-test - 权限要求:
apex:compile:compileAndTest
请求参数:
{
"checkOnly": true,
"classes": [
"public class AccountTest { @isTest static void testCreate() { Account acc = new Account(Name='Test'); insert acc; } }"
],
"triggers": [],
"deleteClasses": [],
"deleteTriggers": [],
"runTestsRequest": {
"allTests": false,
"classes": ["AccountTest"],
"namespace": null,
"packages": [],
"tests": [],
"maxFailedTests": 0,
"skipCodeCoverage": false
}
}
响应参数:
{
"code": 200,
"msg": "编译并测试成功",
"data": {
"compileResults": {
"success": true,
"compileResults": [],
"runTestResult": null,
"checkOnly": true
},
"testResults": {
"numTestsRun": 1,
"numFailures": 0,
"totalTime": 123.45,
"successes": [
{
"methodName": "testCreate",
"name": "AccountTest",
"time": 123.45,
"message": null,
"stackTrace": null,
"id": "01pxx0000003DHb2AAG"
}
],
"failures": [],
"codeCoverage": [
{
"id": "01pxx0000003DHb2AAG",
"name": "AccountTest",
"type": "Class",
"namespace": null,
"numLocations": 10,
"numLocationsNotCovered": 2,
"coveragePercent": 80.0
}
],
"codeCoverageWarnings": [],
"flowCoverage": [],
"flowCoverageWarnings": [],
"apexLogId": "07Lxx0000003DHb2AAG"
},
"success": true
}
}
4. 执行匿名 Apex 代码
接口信息:
- 请求方式: POST
- 请求路径:
/api/apex/execute/anonymous - 权限要求:
apex:execute:anonymous
请求参数:
{
"code": "System.debug('Hello World'); Account acc = new Account(Name='Test Account'); insert acc;"
}
响应参数:
{
"code": 200,
"msg": "执行成功",
"data": {
"result": {
"success": true,
"compiled": true,
"compileProblem": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"line": 0,
"column": 0
},
"success": true,
"compiled": true,
"compileProblem": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"line": 0,
"column": 0
}
}
5. 查询编译历史
接口信息:
- 请求方式: GET
- 请求路径:
/api/apex/compile/history - 权限要求:
apex:compile:history
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| compileType | String | 否 | 编译类型(class/trigger/compile_and_test) |
| startTime | String | 否 | 开始时间(yyyy-MM-dd HH:mm:ss) |
| endTime | String | 否 | 结束时间(yyyy-MM-dd HH:mm:ss) |
| page | Integer | 否 | 页码,默认为 1 |
| pageSize | Integer | 否 | 每页大小,默认为 20 |
响应参数:
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 100,
"page": 1,
"pageSize": 20,
"records": [
{
"id": 1,
"compileType": "class",
"codeContent": "public class MyClass { }",
"compileSuccess": true,
"compileProblem": null,
"compileTime": "2026-01-28 10:00:00",
"userId": "005xx0000001Gw2EAS",
"createTime": "2026-01-28 10:00:00"
}
]
}
}
6. 查询测试结果
接口信息:
- 请求方式: GET
- 请求路径:
/api/apex/test/results - 权限要求:
apex:test:results
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| testRunId | String | 否 | 测试运行 ID |
| startTime | String | 否 | 开始时间(yyyy-MM-dd HH:mm:ss) |
| endTime | String | 否 | 结束时间(yyyy-MM-dd HH:mm:ss) |
| page | Integer | 否 | 页码,默认为 1 |
| pageSize | Integer | 否 | 每页大小,默认为 20 |
响应参数:
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 50,
"page": 1,
"pageSize": 20,
"records": [
{
"id": 1,
"testRunId": "707xx0000003DHb2AAG",
"numTestsRun": 10,
"numFailures": 0,
"totalTime": 1234.56,
"apexLogId": "07Lxx0000003DHb2AAG",
"testTime": "2026-01-28 10:00:00",
"userId": "005xx0000001Gw2EAS",
"createTime": "2026-01-28 10:00:00"
}
]
}
}
7. 查询代码覆盖率
接口信息:
- 请求方式: GET
- 请求路径:
/api/apex/test/coverage - 权限要求:
apex:test:coverage
请求参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| testResultId | Long | 否 | 测试结果 ID |
| name | String | 否 | 类/触发器名称 |
| type | String | 否 | 类型(Class/Trigger) |
| minCoverage | Double | 否 | 最小覆盖率 |
| maxCoverage | Double | 否 | 最大覆盖率 |
| page | Integer | 否 | 页码,默认为 1 |
| pageSize | Integer | 否 | 每页大小,默认为 20 |
响应参数:
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 30,
"page": 1,
"pageSize": 20,
"records": [
{
"id": 1,
"testResultId": 1,
"name": "AccountTest",
"type": "Class",
"namespace": null,
"numLocations": 100,
"numLocationsNotCovered": 20,
"coveragePercent": 80.0,
"createTime": "2026-01-28 10:00:00"
}
]
}
}
接口权限设计
| 接口 | 权限标识 | 说明 |
|---|---|---|
| POST /api/apex/compile/classes | apex:compile:classes | 编译 Apex 类 |
| POST /api/apex/compile/triggers | apex:compile:triggers | 编译 Apex 触发器 |
| POST /api/apex/compile/compile-and-test | apex:compile:compileAndTest | 编译并测试 |
| POST /api/apex/execute/anonymous | apex:execute:anonymous | 执行匿名 Apex 代码 |
| GET /api/apex/compile/history | apex:compile:history | 查询编译历史 |
| GET /api/apex/test/results | apex:test:results | 查询测试结果 |
| GET /api/apex/test/coverage | apex:test:coverage | 查询代码覆盖率 |
实现要点
关键实现逻辑
1. Service 层实现
@Service
@Slf4j
public class ApexCompileServiceImpl implements IApexCompileService {
@Autowired
private ApexConnectionFactory apexConnectionFactory;
@Autowired
private ApexCompileHistoryMapper compileHistoryMapper;
@Autowired
private ApexTestResultMapper testResultMapper;
@Autowired
private ApexTestFailureMapper testFailureMapper;
@Autowired
private ApexCodeCoverageMapper codeCoverageMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public CompileClassResultVo[] compileClasses(String[] classes) throws SalesforceAuthException {
log.info("开始编译 Apex 类,数量: {}", classes.length);
try {
// 获取连接
SoapConnection connection = apexConnectionFactory.getConnection("source");
// 调用 Salesforce API
CompileClassResult[] results = connection.compileClasses(classes);
// 转换结果
CompileClassResultVo[] voResults = processCompileResults(results);
// 保存编译历史
saveCompileHistory(voResults, String.join("\n", classes), "class");
log.info("编译 Apex 类完成,成功: {}", isAllSuccess(voResults));
return voResults;
} catch (ConnectionException e) {
log.error("编译 Apex 类失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("编译失败: " + e.getMessage());
} catch (Exception e) {
log.error("编译 Apex 类失败: {}", e.getMessage(), e);
throw new SalesforceOperationException("编译 Apex 类失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ExecuteAnonymousResultVo executeAnonymous(String code) throws SalesforceAuthException {
log.info("开始执行匿名 Apex 代码");
try {
// 获取连接
SoapConnection connection = apexConnectionFactory.getConnection("source");
// 调用 Salesforce API
ExecuteAnonymousResult result = connection.executeAnonymous(code);
// 转换结果
ExecuteAnonymousResultVo vo = convertToVo(result);
// 保存执行历史
saveExecuteHistory(vo, code);
log.info("执行匿名 Apex 代码完成,成功: {}", vo.getSuccess());
return vo;
} catch (ConnectionException e) {
log.error("执行匿名 Apex 代码失败: {}", e.getMessage(), e);
throw new SalesforceAuthException("执行失败: " + e.getMessage());
} catch (Exception e) {
log.error("执行匿名 Apex 代码失败: {}", e.getMessage(), e);
throw new SalesforceOperationException("执行匿名 Apex 代码失败: " + e.getMessage());
}
}
}
2. Controller 层实现
@RestController
@RequestMapping("/api/apex")
@Tag(name = "Apex编译和执行", description = "Salesforce Apex 代码编译和执行接口")
@Slf4j
public class ApexCompileController {
@Autowired
private IApexCompileService apexCompileService;
@PostMapping("/compile/classes")
@Operation(summary = "编译 Apex 类", description = "编译一个或多个 Apex 类")
@PreAuthorize("@ss.hasPermi('apex:compile:classes')")
public AjaxResult compileClasses(@Valid @RequestBody CompileClassesDto dto) {
log.info("收到编译 Apex 类请求");
try {
CompileClassResultVo[] results = apexCompileService.compileClasses(dto.getClasses());
boolean allSuccess = isAllSuccess(results);
return AjaxResult.success(allSuccess ? "编译成功" : "编译完成,存在错误",
Map.of("results", results, "success", allSuccess));
} catch (SalesforceAuthException e) {
log.error("编译 Apex 类失败: {}", e.getMessage());
throw new ServiceException("编译失败: " + e.getMessage());
} catch (Exception e) {
log.error("编译 Apex 类失败: {}", e.getMessage(), e);
return AjaxResult.error("编译失败: " + e.getMessage());
}
}
}
异常处理设计
1. 异常分类
| 异常类型 | 说明 | 处理方式 |
|---|---|---|
| SalesforceAuthException | 认证异常 | 返回 401 状态码,提示用户重新登录 |
| SalesforceOperationException | 操作异常 | 返回 500 状态码,记录错误日志 |
| ServiceException | 服务异常 | 返回 500 状态码,返回错误信息 |
| ValidationException | 参数校验异常 | 返回 400 状态码,返回校验错误信息 |
2. 异常处理示例
@ExceptionHandler(SalesforceAuthException.class)
public AjaxResult handleSalesforceAuthException(SalesforceAuthException e) {
log.error("Salesforce 认证异常: {}", e.getMessage());
return AjaxResult.error("认证失败: " + e.getMessage());
}
@ExceptionHandler(SalesforceOperationException.class)
public AjaxResult handleSalesforceOperationException(SalesforceOperationException e) {
log.error("Salesforce 操作异常: {}", e.getMessage());
return AjaxResult.error("操作失败: " + e.getMessage());
}
性能优化设计
1. 连接池优化
- 复用 ApexConnectionFactory 的连接缓存机制
- 使用 ConcurrentHashMap + ReentrantLock 保证线程安全
- 连接超时设置为 60 秒,避免长时间占用资源
2. 数据库优化
- 为常用查询字段添加索引(compile_type、user_id、compile_time)
- 使用分页查询,避免一次性返回大量数据
- 使用 MyBatis Plus 的批量插入功能
3. 缓存策略
- 编译结果不缓存(每次编译结果可能不同)
- 查询结果可以使用 Redis 缓存(可选)
安全设计
1. 权限控制
- 使用 Spring Security 进行权限控制
- 使用 @PreAuthorize 注解进行方法级权限控制
- 每个接口都有独立的权限标识
2. 参数校验
- 使用 @Valid 注解进行参数校验
- 使用 @NotBlank、@NotNull 等注解进行字段级校验
- 对代码长度进行限制,防止恶意输入
3. SQL 注入防护
- 使用 MyBatis Plus 的参数化查询
- 不直接拼接 SQL 语句
- 对用户输入进行过滤
4. XSS 防护
- 对用户输入的代码内容进行转义
- 使用前端框架的 XSS 防护机制