Salesforce批量录入捐赠时Trigger出站API调用报错失效问题排查
问题原因
- 取消
@future(callout=true)注释后API无法调用:Apex触发器属于同步数据库事务上下文,平台禁止在该上下文内直接发起同步HTTP出站调用,因此请求被拦截不会执行。 - 批量录入时的嵌套future报错:批量录入捐赠的操作本身是通过Batch Apex异步执行的,平台规则不允许在异步(@future/Batch/Queueable)上下文内调用另一个@future方法,因此触发报错。
- 隐藏的性能风险:现有触发器在循环内单条调用业务方法,批量场景下会产生大量重复查询和异步任务,极易超出 governor 限制。
修复方案
采用Queueable Apex替代@future实现异步调用,Queueable支持从Batch上下文调用,同时支持批量传参优化性能,具体修改如下:
1. 修改业务类ProcessDonor
将原有的单参数SerializeJson方法替换为支持批量处理的Queueable实现:
public class ProcessDonor implements Queueable, Database.AllowsCallouts { // 批量接收捐赠参数 private List<DonationParam> donationParams; // 参数结构体 public class DonationParam { public String accountId; public String amount; public String donationId; public DonationParam(String accId, String amt, String donId) { accountId = accId; amount = amt; donationId = donId; } } // 构造方法传入批量参数 public ProcessDonor(List<DonationParam> params) { donationParams = params; } // Queueable执行入口 public void execute(QueueableContext context) { // 先批量查询所有关联Account避免循环查询 Set<String> accountIds = new Set<String>(); for(DonationParam param : donationParams) { accountIds.add(param.accountId); } Map<Id, Account> accountMap = new Map<Id, Account>([ SELECT Id, Name, FirstGiftDate__c, LatestGiftDate__c, LifetimeDonationCount__c, Billing_Street, Billing_City, Billing_State, Billing_Postal_Code, EnvelopeSalutation__c FROM Account WHERE Id IN :accountIds ]); // 批量查询关联Contact Map<Id, List<Contact>> accountContactMap = new Map<Id, List<Contact>>(); for(Contact con : [SELECT AccountId, Addressee__c, salutation FROM Contact WHERE AccountId IN :accountIds]) { if(!accountContactMap.containsKey(con.AccountId)) { accountContactMap.put(con.AccountId, new List<Contact>()); } accountContactMap.get(con.AccountId).add(con); } // 遍历处理每笔捐赠 for(DonationParam param : donationParams) { Account acc = accountMap.get(param.accountId); if(acc == null || acc.LifetimeDonationCount__c != 1.00) { continue; } List<Contact> cons = accountContactMap.get(acc.Id); if(cons == null || cons.isEmpty()) { continue; } // 执行API调用 jsonHandler( JSON.serialize(new List<Account>{acc}), JSON.serialize(new List<Contact>{cons[0]}), param.accountId, param.amount, param.donationId ); } } // 原有HTTP请求方法保持不变 public static String jsonHandler(String json1, String json2, String account, String donoAmount, String donoId){ Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('https://urlToTerver'); request.setMethod('POST'); request.setHeader('Content-Type', 'application/json;charset=UTF-8'); request.setHeader('accountDataFields', json1); request.setHeader('contactDataFields', json2); request.setHeader('accountId', account); request.setHeader('donationAmount', donoAmount); request.setHeader('donationId', donoId); HttpResponse response = http.send(request); if (response.getStatusCode() != 200) { System.debug('The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getBody()); return 'The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus(); } else { System.debug(response.getBody()); return response.getBody(); } } }
2. 修改触发器,批量收集参数后调用Queueable
trigger NewDonor on Donation (after insert) { List<ProcessDonor.DonationParam> params = new List<ProcessDonor.DonationParam>(); for(Donation d : Trigger.new) { if(d.Stage == 'Closed Won' || d.Stage == 'Received'){ params.add(new ProcessDonor.DonationParam( d.AccountId, d.Amount.toPlainString(), d.Id )); } } // 仅当有符合条件的捐赠时发起异步调用 if(!params.isEmpty()) { System.enqueueJob(new ProcessDonor(params)); } }
额外注意事项
- 该方案避免了循环查询和单条异步调用的问题,符合Salesforce bulkified代码规范,适配批量录入场景
- 如果需要对失败的API调用做重试,可以在jsonHandler方法中捕获异常,结合Platform Event或自定义重试队列实现
- 如果批量场景单次调用量过大,可以拆分参数队列,多次调用System.enqueueJob拆分负载
内容的提问来源于stack exchange,提问作者Trout
相关产品推荐
相关产品推荐

