以 Apex 掌控 Salesforce 审批流程:开发者深度指南

背景与应用场景

在任何企业的运营中,审批都是一个不可或缺的环节。从销售折扣、费用报销到合同签署,都需要一个结构化、可追溯的流程来确保合规性和效率。Salesforce 提供了强大的 Approval Process (审批流程) 功能,这是一个声明式的自动化工具,允许管理员通过点击式配置来定义多步骤的记录审批工作流。

对于大部分标准业务场景,例如“当机会折扣超过 20% 时,需要销售经理批准”,标准的 Approval Process 配置已经足够。然而,作为一名 Salesforce Developer (Salesforce 开发人员),我们经常会遇到更复杂的需求,这些需求超出了声明式工具的能力范围:

  • 复杂的提交逻辑: 记录是否需要提交审批,可能取决于多个相关对象的数据,或者需要复杂的计算逻辑来判断。
  • 动态审批人指派: 审批人可能不是一个固定的用户或角色,而是需要通过复杂的 Apex 逻辑查询,甚至调用外部系统 API 来确定。
  • 自定义用户界面 (UI): 企业可能希望通过 Lightning Web Component (LWC) 或 Aura Component 构建一个高度定制化的界面来提交审批、展示审批历史,而不是使用标准的 Salesforce 界面。
  • 批量处理: 需要通过一个批处理任务,将数千条符合条件的记录一次性提交审批。
  • 外部系统集成: 审批的触发或响应(批准/拒绝)需要与外部系统(如 ERP、财务系统)进行实时同步。

在这些场景下,我们就需要利用 Apex 语言,以编程方式与 Approval Process 进行交互。这不仅扩展了审批流程的灵活性和能力,也为我们开发者提供了精细化控制工作流的手段。


原理说明

要通过 Apex 与审批流程进行交互,我们必须首先理解其核心对象模型和相关的 Apex 类。整个流程的核心是 Salesforce 提供的 Process 命名空间。

1. 声明式基础

在编写任何代码之前,一个定义好的、激活 (Active) 的 Approval Process 必须存在于 Salesforce 中。Apex 代码本身并不能创建或修改审批流程的结构(例如审批步骤、准入条件),它只能与一个已经存在的流程定义进行交互。一个标准的 Approval Process 包含以下关键组件:

  • Entry Criteria (进入条件): 定义了什么样的记录可以被提交审批。
  • Initial Submission Actions (初始提交操作): 记录被提交时触发的动作,例如锁定记录、更新字段。
  • Approval Steps (审批步骤): 定义了审批的每一个阶段,包括该步骤的审批人、准入条件和拒绝行为。
  • Final Approval/Rejection Actions (最终批准/拒绝操作): 当整个流程完成(被批准或被拒绝)时触发的动作。

2. Apex 编程接口

Salesforce 在 Apex 中提供了专门的类来让我们以编程方式执行审批相关的操作。核心是 Process 类中的两个主要方法:

  • Process.SubmitRequest: 这是一个内部类,用于构建一个审批提交请求。你需要实例化这个对象,并设置关键属性,如要提交的记录 ID、提交人、审批流程名称以及提交备注。
  • Process.ProcessResult: 这是执行审批操作后(如提交、批准、拒绝)的返回对象。通过检查这个对象,我们可以判断操作是否成功,并在失败时获取详细的错误信息。
  • Process.process(request): 这是执行提交动作的静态方法。它接受一个 Process.SubmitRequest 对象或 Process.ProcessWorkitemRequest 对象作为参数,并返回一个 Process.ProcessResult 对象。

当我们想要批准或拒绝一个已经处于待审批状态的记录时,逻辑会稍有不同。我们需要与 ProcessInstanceWorkitem 对象交互:

  • ProcessInstance: 代表一个审批流程的实例,即一条记录进入审批流程后创建的整体流程记录。
  • ProcessInstanceWorkitem: 代表一个待处理的审批工作项,也就是分配给某个用户的“待我审批”项。要批准或拒绝,我们必须先获取这个工作项的 ID。
  • Process.ProcessWorkitemRequest:SubmitRequest 类似,这是用于构建批准或拒绝请求的内部类。你需要设置工作项 ID、要执行的操作(Approve, Reject)以及备注。

总而言之,开发者的工作流程通常是:

  1. (如果需要)通过 SOQL 查询确定记录是否满足复杂的提交条件。
  2. 实例化 Process.SubmitRequest,设置记录 ID 和其他参数。
  3. 调用 Process.process() 方法提交请求。
  4. 检查返回的 Process.ProcessResult 以确认成功或处理错误。
  5. (如果需要批准/拒绝)通过 SOQL 查询 ProcessInstanceWorkitem 获取待处理项的 ID。
  6. 实例化 Process.ProcessWorkitemRequest,设置操作类型。
  7. 再次调用 Process.process() 方法执行批准或拒绝。


示例代码

以下示例代码均严格参考 Salesforce Developer 官方文档,展示了如何通过 Apex 提交记录进行审批,以及如何以编程方式批准一个待处理的请求。

示例 1: 提交记录进行审批

假设我们有一个自定义对象 Expense__c(报销单),并且已经为其创建了一个名为 Expense_Approval_Process 的审批流程。当报销金额超过 1000 时,需要通过一个自定义的 LWC 组件按钮触发 Apex 来提交审批。

// Apex Controller for a custom component
public with sharing class ExpenseApprovalController {

    @AuraEnabled
    public static String submitForApproval(Id recordId) {
        // 首先,检查该记录是否已经被锁定或处于审批流程中
        // 这是一个最佳实践,可以避免重复提交或在不应提交时提交
        List<ProcessInstance> processInstances = [
            SELECT Id
            FROM ProcessInstance
            WHERE TargetObjectId = :recordId AND Status = 'Pending'
            LIMIT 1
        ];

        if (!processInstances.isEmpty()) {
            return 'This record is already in an approval process.';
        }

        // 创建一个审批提交请求
        Process.SubmitRequest approvalRequest = new Process.SubmitRequest();
        
        // 设置要提交审批的记录的 ID
        approvalRequest.setObjectId(recordId);

        // 设置提交备注,这些备注会显示在审批历史中
        approvalRequest.setComments('Submitting expense report for approval via custom UI.');

        // 设置提交人 ID。如果设为 null,则默认为当前上下文用户
        // approvalRequest.setSubmitterId(UserInfo.getUserId());

        // 指定要使用的审批流程的唯一名称 (API Name)。
        // 如果设置为 null,Salesforce 会自动选择此记录符合条件的第一个审批流程。
        // 显式指定是一种好习惯,可以避免歧义。
        approvalRequest.setProcessDefinitionNameOrId('Expense_Approval_Process');

        // 如果希望跳过此审批流程的进入条件,可以设置为 true。
        // 这在需要强制提交记录时非常有用。
        approvalRequest.setSkipEntryCriteria(true);

        try {
            // 提交审批请求
            Process.ProcessResult result = Process.process(approvalRequest);

            // 检查提交是否成功
            if (result.isSuccess()) {
                // 操作成功,返回成功消息
                return 'Record submitted for approval successfully. New process instance ID: ' + result.getNewWorkitemIds().get(0);
            } else {
                // 操作失败,收集并返回错误信息
                String errors = '';
                for (Process.ProcessResult.Error error : result.getErrors()) {
                    errors += error.getStatusCode() + ': ' + error.getMessage() + '\n';
                }
                return 'Failed to submit for approval. Errors: ' + errors;
            }
        } catch (Exception e) {
            // 捕获可能发生的任何意外异常
            return 'An unexpected error occurred: ' + e.getMessage();
        }
    }
}

示例 2: 批准或拒绝一个待审批请求

假设我们需要构建一个自动化的服务,当某个外部条件满足时(例如,从外部财务系统接收到一个 webhook),自动批准一个待处理的报销单。

// A service class to handle automated approvals
public with sharing class AutomatedApprovalService {

    public static void approveOrRejectExpense(Id targetRecordId, String action, String comments) {
        // 通过记录 ID 查询待处理的工作项 (ProcessInstanceWorkitem)
        // 必须找到分配给特定用户或队列的工作项,这里假设我们代表某个集成用户进行操作
        ProcessInstanceWorkitem workitem = [
            SELECT Id 
            FROM ProcessInstanceWorkitem 
            WHERE ProcessInstance.TargetObjectId = :targetRecordId 
            AND ProcessInstance.Status = 'Pending'
            // 在实际场景中,你可能还需要根据 ActorId (审批人) 进行过滤
            // WHERE ActorId = :UserInfo.getUserId()
            LIMIT 1
        ];

        if (workitem == null) {
            // 没有找到待处理的审批项
            System.debug('No pending approval work item found for record: ' + targetRecordId);
            return;
        }

        // 创建一个审批工作项请求
        Process.ProcessWorkitemRequest workitemRequest = new Process.ProcessWorkitemRequest();
        
        // 设置要处理的工作项 ID
        workitemRequest.setWorkitemId(workitem.Id);

        // 设置要执行的操作,可以是 'Approve', 'Reject', 或 'Removed' (撤回)
        // 这是一个关键参数
        if (action.equalsIgnoreCase('Approve')) {
            workitemRequest.setAction('Approve');
        } else if (action.equalsIgnoreCase('Reject')) {
            workitemRequest.setAction('Reject');
        } else {
            System.debug('Invalid action specified: ' + action);
            return;
        }
        
        // 设置审批或拒绝的备注
        workitemRequest.setComments(comments);

        try {
            // 执行审批/拒绝操作
            Process.ProcessResult result = Process.process(workitemRequest);

            // 检查结果
            if (result.isSuccess()) {
                System.debug('Successfully processed work item ' + workitem.Id + ' with action: ' + action);
            } else {
                // 记录错误
                for (Process.ProcessResult.Error error : result.getErrors()) {
                    System.debug('Error processing work item: ' + error.getStatusCode() + ' - ' + error.getMessage());
                }
            }
        } catch (Exception e) {
            System.debug('An exception occurred while processing work item: ' + e.getMessage());
        }
    }
}

注意事项

权限 (Permissions)

  • 提交权限: 运行 Apex 代码的用户(或在 SubmitRequest 中指定的提交人)必须拥有提交该记录进行审批的权限。这通常由用户的简档或权限集中的对象权限和“提交审批”系统权限控制。
  • 审批权限: 当以编程方式批准或拒绝时,Apex 代码的上下文用户必须是该审批步骤中指定的审批人,或者是拥有“修改所有数据”权限的管理员。Apex 虽然在很多情况下以系统模式运行,但在审批操作上,它仍然会校验操作发起者是否为合法的审批人。
  • 记录访问权限: 用户必须对要操作的记录拥有至少“读取”权限。

API 限制 (Governor Limits)

  • DML 限制: Process.process() 方法的每次调用都算作一次 DML 操作。在一个 Apex 事务中,DML 操作的上限是 150 次。因此,绝对不能在循环中直接调用此方法来处理大量记录。
  • 批量处理: 如果需要批量提交数百条记录,必须使用异步 Apex,例如 Batch ApexQueueable Apex。你可以将记录 ID 的列表传递给异步作业,然后在作业中分批次构建请求列表并进行处理。
  • CPU 时间限制: 复杂的审批流程可能会触发一系列的字段更新、邮件发送和出站消息,这些都会消耗 CPU 时间。在设计和测试时需要密切关注性能。

错误处理 (Error Handling)

  • 始终检查 ProcessResult 永远不要想当然地认为 Process.process() 会成功。务必检查 isSuccess() 方法的返回值,并为失败情况准备好日志记录和回滚逻辑。
  • 使用 Try-Catch 块:Process.process() 调用包裹在 try-catch 块中,以捕获由于权限问题、无效 ID 或其他意外配置错误而可能抛出的异常。

记录锁定 (Record Locking)

一旦记录进入审批流程,Salesforce 会自动将其锁定 (Lock),防止除当前审批人和管理员之外的用户进行编辑。这可能会与其他自动化工具(如触发器、流)或用户操作产生冲突。在设计解决方案时,必须考虑记录锁定的影响。如果需要在审批期间更新记录,可以配置审批流程以允许管理员或当前审批人编辑记录。


总结与最佳实践

通过 Apex 与 Salesforce Approval Process 进行交互,为开发者提供了强大的能力来解决复杂业务需求。它将声明式配置的便捷性与编程的灵活性完美结合。以下是总结的最佳实践:

  1. 声明式优先 (Declarative First): 始终优先使用标准的点击式配置来构建审批流程的基础。只有当遇到声明式工具无法满足的需求时,才引入 Apex 进行扩展。
  2. 代码必须是可批处理的 (Bulkify Your Code): 在设计 Apex 解决方案时,要始终假设它需要处理多条记录。避免在循环中执行 SOQL 查询或 DML 操作,特别是 Process.process()
  3. 明确指定审批流程 (Be Specific): 在使用 Process.SubmitRequest 时,尽可能通过 setProcessDefinitionNameOrId() 方法明确指定要使用的审批流程。这可以防止因多条符合条件的流程而导致的不确定性。
  4. 健壮的测试覆盖 (Robust Test Coverage): 编写 Apex 测试类时,需要覆盖成功提交、提交失败、批准和拒绝等所有场景。使用 Test.startTest()Test.stopTest() 来封装你的审批操作,以便在测试后查询 ProcessInstanceProcessInstanceWorkitem 的状态来断言结果。
  5. 用户体验至上 (Consider User Experience): 如果你正在构建一个自定义 UI,请务必向用户提供清晰、即时的反馈。告知他们提交是否成功,如果失败,则显示易于理解的错误信息。

作为一名 Salesforce 开发人员,熟练掌握以编程方式操作审批流程是一项关键技能。它不仅能帮助你交付功能更强大的解决方案,还能让你在面对复杂的业务自动化挑战时更加从容不迫。

评论

此博客中的热门博文

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

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

Salesforce Data Loader 全方位指南:数据迁移与管理的最佳实践