精通 Salesforce 批准流程与 Apex:开发者深度指南

背景与应用场景

我是一名 Salesforce 开发人员。在我们的日常工作中,处理业务流程自动化是核心任务之一。Salesforce 平台提供了一个强大且高度可配置的声明式工具——Approval Process (批准流程),用于自动化组织内记录的审批步骤。典型的应用场景包括:销售团队提交超过特定折扣阈值的“商机 (Opportunity)”进行审批、员工提交费用报销申请、或者 HR 部门处理休假请求等。

虽然标准的 Approval Process 功能非常强大,可以通过 UI 配置入口条件、审批步骤、审批人和最终批准/拒绝操作,但在某些复杂的业务场景下,仅靠声明式配置是不足够的。这时,作为开发人员,我们就需要介入,利用 Apex 代码来扩展和增强批准流程的自动化能力。这些场景可能包括:

  • 批量提交记录:当业务需要根据特定逻辑,一次性将数百条符合条件的记录(例如,所有上个季度创建的、金额超过 100 万的合同)提交审批时,手动操作是不现实的。
  • 复杂的入口条件:当记录是否需要进入审批流程的判断逻辑,依赖于对其他多个相关对象的查询和计算,超出了公式字段的能力范围时,需要通过 Apex 触发器 (Trigger) 或批处理 (Batch Apex) 来启动。
  • 自定义用户界面:企业可能希望在 Lightning Web Component (LWC) 或自定义页面中构建审批界面,而不是使用标准的 Salesforce UI。这就需要我们通过 Apex Controller 来处理后端的提交、批准或拒绝逻辑。
  • 外部系统集成:当审批的触发或决策需要与外部系统(如 ERP 或财务系统)进行数据交互时,我们需要通过 Apex Callout 获取外部数据,然后根据返回结果来驱动批准流程。

因此,深入理解如何通过 Apex 与 Approval Process 交互,是我们作为 Salesforce 开发人员必备的核心技能之一。它让我们能够将标准的业务流程自动化提升到一个全新的高度,满足企业最独特、最复杂的业务需求。


原理说明

要通过代码与 Salesforce 的 Approval Process 交互,我们必须首先理解其背后的数据模型和核心 Apex 类。整个过程主要围绕几个关键的 sObject 和一个专门的 Apex 系统类展开。

关键 sObject

  • ProcessDefinition: 这个对象存储了您在“设置”中创建的所有批准流程的“定义”或模板。当我们想要通过代码提交某个特定流程时,我们首先需要查询这个对象,根据我们定义的批准流程的名称(Name)或唯一名称(DeveloperName)来获取其 ID。
  • ProcessInstance: 当一条记录(例如,一个 Opportunity)被提交到某个 Approval Process 时,系统会为它创建一个 ProcessInstance 记录。这条记录代表了该审批流程的一个完整实例,跟踪着它的整个生命周期,例如当前状态(Pending, Approved, Rejected)以及提交者是谁。它的 TargetObjectId 字段关联到被审批的记录。
  • ProcessInstanceStep: 一个 ProcessInstance 包含一个或多个步骤。ProcessInstanceStep 对象就代表了其中的某一个具体步骤,无论是已经完成的还是当前正在进行的。它记录了该步骤的状态、进入和离开该步骤的时间等信息。
  • ProcessInstanceWorkitem: 这是我们进行程序化审批或拒绝时最常交互的对象。当一个审批请求被分配给一个用户或一个队列 (Queue) 时,系统会创建一个 ProcessInstanceWorkitem 记录。这实际上就是用户在主页“待审批项目 (Items to Approve)”列表中看到的那个待办事项。我们的 Apex 代码需要找到这个 Workitem 的 ID,才能对其执行批准或拒绝操作。

核心 Apex 类:Approval Class

Salesforce 提供了一个专门的系统命名空间 Approval,其中包含了一系列静态方法和内部类,用于以编程方式处理批准流程。这是我们所有操作的入口点。

  • Approval.ProcessSubmitRequest: 这是一个内部类,用于构建一个“提交审批”的请求。在提交记录之前,我们需要创建一个该类的实例,并设置关键属性,如:
    • setContextId(Id): 设置要提交审批的记录 ID。
    • setComments(String): 设置提交时的备注。
    • setSubmitterId(Id): 指定提交者。如果忽略,则默认为当前运行代码的用户。
    • setProcessDefinitionNameOrId(String): 指定要提交到哪个批准流程。强烈建议使用唯一名称 (DeveloperName),而不是硬编码 ID,以保证代码在不同环境间的可移植性。
  • Approval.ProcessWorkitemRequest: 这是一个内部类,用于构建一个“处理待办事项”的请求,即批准或拒绝。关键属性包括:
    • setWorkitemId(Id): 设置要处理的 ProcessInstanceWorkitem 的 ID。这是必须的。
    • setAction(String): 指定要执行的操作。通常是 'Approve' 或 'Reject'。
    • setComments(String): 设置批准或拒绝时的备注。
  • Approval.process(request): 这是核心的静态方法,它接收一个 ProcessSubmitRequestProcessWorkitemRequest 对象(或它们的列表),并执行相应的操作。它返回一个 Approval.ProcessResult 对象,我们可以通过它来判断操作是否成功。
  • Approval.lock(recordId) / Approval.unlock(recordId): 这两个方法用于手动锁定和解锁正在审批的记录。通常情况下,我们不需要手动调用它们,因为当记录进入审批流程时,系统会自动根据流程定义中的设置来锁定记录。

总而言之,开发的逻辑流程通常是:
提交:创建 Approval.ProcessSubmitRequest -> 设置记录 ID 和流程定义 -> 调用 Approval.process()
处理:通过 SOQL 查询找到目标记录关联的 ProcessInstanceWorkitem -> 获取其 ID -> 创建 Approval.ProcessWorkitemRequest -> 设置 Workitem ID 和操作(Approve/Reject)-> 调用 Approval.process()


示例代码

以下代码示例严格基于 Salesforce Developer 官方文档,展示了如何通过 Apex 提交记录进行审批,以及如何自动批准一个待审批请求。

示例一:提交一个客户 (Account) 记录进行审批

此示例假设我们已经创建了一个针对 Account 对象的、唯一名称为 “Account_Approval_Process” 的批准流程。

// 假设我们有一个需要提交审批的客户记录
// 在实际代码中, 这个 ID 会动态获取, 例如从 LWC 控制器参数传入
Id accountId = '001xx000003DHPwAAO';

// 1. 创建一个审批提交请求对象
Approval.ProcessSubmitRequest req1 = new Approval.ProcessSubmitRequest();

// 2. 设置需要审批的记录 ID
// 这是必须的, 告诉系统我们要对哪条记录进行操作
req1.setContextId(accountId);

// 3. 设置提交备注, 这会显示在审批历史中
req1.setComments('Submitting account for approval via Apex.');

// 4. 指定要使用的批准流程
// 使用唯一名称 (DeveloperName) 而不是硬编码 ID 是最佳实践
// 这使得代码在不同 Salesforce 环境中(如从 Sandbox 到 Production)无需修改即可运行
req1.setProcessDefinitionNameOrId('Account_Approval_Process');

// 5. (可选) 指定下一个审批人
// 如果批准流程的第一个步骤允许提交者选择审批人, 可以使用此方法
// List<Id> nextApproverIds = new List<Id>();
// nextApproverIds.add('005xx000001SvDDAA0'); // 一个具体的用户 ID
// req1.setNextApproverIds(nextApproverIds);

try {
    // 6. 提交请求
    // Approval.process() 方法可以接受单个请求或一个请求列表
    Approval.ProcessResult result = Approval.process(req1);

    // 7. 检查操作结果
    if (result.isSuccess()) {
        System.debug('Record submitted successfully. New ProcessInstance ID: ' + result.getInstanceIds()[0]);
    } else {
        // 8. 如果失败, 遍历并输出错误信息
        for (Approval.ProcessResult.Error error : result.getErrors()) {
            System.debug('An error occurred: ' + error.getStatusCode() + ':' + error.getMessage());
            // 可以在这里添加更复杂的错误处理逻辑, 例如向用户显示错误信息
        }
    }
} catch (System.DmlException e) {
    // 捕获可能发生的 DML 异常
    System.debug('A DML exception has occurred: ' + e.getMessage());
}

示例二:自动批准一个待审批请求

此示例演示了如何查询待审批项 (Workitem) 并自动批准它。这在需要基于系统逻辑(例如,如果某个条件满足,则由系统自动批准)的场景中非常有用。

// 假设我们要批准上面示例中提交的客户记录
Id targetRecordId = '001xx000003DHPwAAO'; 

// 1. 查找与该记录关联的、处于 "Pending" 状态的待审批项 (ProcessInstanceWorkitem)
// 我们需要 ProcessInstanceWorkitem 的 ID 来执行批准/拒绝操作
ProcessInstanceWorkitem workitem = [SELECT Id 
                                    FROM ProcessInstanceWorkitem 
                                    WHERE ProcessInstance.TargetObjectId = :targetRecordId 
                                    AND ProcessInstance.Status = 'Pending' 
                                    LIMIT 1];

if (workitem != null) {
    // 2. 创建一个审批工作项请求对象
    Approval.ProcessWorkitemRequest req2 = new Approval.ProcessWorkitemRequest();

    // 3. 设置要执行的操作, 'Approve', 'Reject', 或 'Removed' (撤回)
    req2.setAction('Approve');
    
    // 4. 设置要处理的工作项 ID, 这是必须的
    req2.setWorkitemId(workitem.Id);

    // 5. 设置批准备注
    req2.setComments('Auto-approved by system process.');

    // 6. (可选) 如果需要, 可以将请求重新分配给下一个审批人
    // List<Id> nextApproverIds = new List<Id>();
    // nextApproverIds.add('005xx000001SvDDAA0');
    // req2.setNextApproverIds(nextApproverIds);
    
    try {
        // 7. 处理该工作项
        Approval.ProcessResult result = Approval.process(req2);

        // 8. 检查结果
        if (result.isSuccess()) {
            System.debug('Workitem processed successfully. Record is approved.');
        } else {
            // 9. 处理错误
            for(Approval.ProcessResult.Error error : result.getErrors()) {
                System.debug('An error occurred while processing workitem: ' + error.getStatusCode() + ':' + error.getMessage());
            }
        }
    } catch (System.DmlException e) {
        System.debug('A DML exception has occurred: ' + e.getMessage());
    }
} else {
    System.debug('No pending workitem found for record ID: ' + targetRecordId);
}

注意事项

  • 权限 (Permissions):执行提交操作的用户需要对记录拥有读取权限,并且其简档 (Profile) 或权限集 (Permission Set) 中需要有“提交以供批准”的权限。执行批准或拒绝操作的用户必须是当前步骤的指定审批人,或者是在批准流程中指定的代理审批人,同时需要对记录拥有编辑权限。
  • API 限制 (API Limits)Approval.process() 调用会消耗 DML 语句的 Governor Limit。每次调用,无论处理多少个请求(最多100个),都只算作一次 DML 操作。因此,在进行批量处理时,务必将所有请求添加到一个 List 中,然后进行单次调用,以遵循 Apex 的批量化最佳实践。
  • 混合 DML 操作错误 (Mixed DML Operation Error)Approval.process() 方法执行的是对非设置对象(如 Account, Opportunity)的 DML 操作。如果在同一个事务 (Transaction) 中,您还对设置对象(如 User, Profile, Group)进行了 DML 操作,就会触发“Mixed DML Operation”错误。解决方案是将这些操作分离到不同的事务中,例如使用 @future 方法来异步执行其中一个操作。
  • 记录锁定 (Record Locking):一旦记录提交审批,它通常会被锁定,以防止在审批过程中被修改。这个行为是在 Approval Process 的设置中配置的。作为开发人员,你需要意识到这一点。任何试图在记录锁定时对其进行编辑的 DML 操作都会失败。如果必须在审批中更新字段,应该通过批准流程中的“字段更新 (Field Update)”操作来完成。
  • 错误处理 (Error Handling):永远不要想当然地认为 Approval.process() 调用会成功。务必检查返回的 Approval.ProcessResult 对象的 isSuccess() 方法。如果返回 false,请务必通过 getErrors() 方法获取并记录详细的错误信息,这对于调试至关重要。例如,提交可能会因为不满足入口条件而失败,或者批准可能会因为用户权限不足而失败。
  • 测试覆盖率 (Test Coverage):在测试类中,对涉及 Approval Process 的代码进行测试是完全可行的。您需要在测试方法中创建测试数据,然后调用您的业务逻辑(例如提交审批的方法)。在调用之后,您可以通过 SOQL 查询 ProcessInstanceProcessInstanceWorkitem 对象来断言 (Assert) 流程是否已按预期启动和分配。您甚至可以模拟批准或拒绝的完整流程,以确保后续逻辑(如批准后的字段更新)也能被正确测试。

总结与最佳实践

将 Apex 与 Salesforce Approval Process 相结合,为我们开发人员提供了强大的能力,以应对标准声明式工具无法满足的复杂自动化需求。通过掌握 Approval 类及其相关对象,我们可以无缝地将自定义逻辑、批量处理和外部系统集成到核心业务审批流程中。

为了确保您的实现是健壮、可扩展和可维护的,请遵循以下最佳实践:

  1. 声明式优先 (Declarative First):在编写任何 Apex 代码之前,请始终充分利用 Approval Process 的标准配置功能。只有当需求(如复杂的入口条件、批量处理)明确无法通过点击式配置实现时,才应诉诸代码。Apex 应该是对标准功能的扩展,而不是替代。
  2. 代码批量化 (Bulkify Your Code):这是 Salesforce 开发的黄金法则。如果您需要处理多条记录,请将它们对应的请求对象(ProcessSubmitRequestProcessWorkitemRequest)收集到一个 List 中,然后通过一次 Approval.process() 调用来处理整个列表。这能极大地提高性能并避免触及 Governor Limits。
  3. 避免硬编码 ID (Avoid Hardcoding IDs):在代码中,绝对不要硬编码记录 ID、用户 ID 或批准流程的 ID。对于批准流程,应使用其唯一的 DeveloperName 通过 SOQL 查询 ProcessDefinition 来动态获取 ID。这保证了您的代码在不同环境之间的可移植性。
  4. 实现健壮的错误处理 (Implement Robust Error Handling):始终假定操作可能会失败。仔细检查 ProcessResult,并为用户提供清晰、有意义的错误反馈。在后台进程中,确保将错误信息记录到自定义日志对象或平台事件 (Platform Event) 中,以便管理员进行监控和故障排除。
  5. 考虑安全上下文 (Consider Security Context):Apex 代码在特定的共享上下文(with sharingwithout sharing)中运行。要仔细考虑您的代码需要以何种权限上下文来执行审批操作。大多数情况下,应遵循运行用户的权限模型(默认或 with sharing),但某些系统级别的自动化流程可能需要使用 without sharing 来确保其顺利执行,但务必谨慎使用并充分理解其安全影响。

通过遵循这些原则,您不仅可以成功地实现功能,还能构建出优雅、高效且经得起时间考验的 Salesforce 解决方案。

评论

此博客中的热门博文

Salesforce Einstein AI 编程实践:开发者视角下的智能预测

Salesforce 登录取证:深入解析用户访问监控与安全

Salesforce Experience Cloud 技术深度解析:构建社区站点 (Community Sites)