使用该方法发送的单封电子邮件计入发送组织的每日单封电子邮件限制。当此限制为 到达时,对方法 using 的调用将被拒绝,并且用户会收到错误代码。然而 允许通过应用程序发送单封电子邮件。sendEmailsendEmailSingleEmailMessageSINGLE_EMAIL_LIMIT_EXCEEDED
使用该方法发送的大量电子邮件 计入发送组织的每日群发电子邮件限制。当达到此限制时, 对方法 using 的调用被拒绝,并且用户会收到错误代码。sendEmailsendEmailMassEmailMessageMASS_MAIL_LIMIT_EXCEEDED
// First, reserve email capacity for the current Apex transaction to ensure
// that we won't exceed our daily email limits when sending email after
// the current transaction is committed.
Messaging.reserveSingleEmailCapacity(2);
// Processes and actions involved in the Apex transaction occur next,
// which conclude with sending a single email.
// Now create a new single email message object
// that will send out a single email to the addresses in the To, CC & BCC list.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
// Strings to hold the email addresses to which you are sending the email.
String[] toAddresses = new String[] {'user@acme.com'};
String[] ccAddresses = new String[] {'smith@gmail.com'};
// Assign the addresses for the To and CC lists to the mail object.
mail.setToAddresses(toAddresses);
mail.setCcAddresses(ccAddresses);
// Specify the address used when the recipients reply to the email.
mail.setReplyTo('support@acme.com');
// Specify the name used as the display name.
mail.setSenderDisplayName('Salesforce Support');
// Specify the subject line for your email address.
mail.setSubject('New Case Created : ' + case.Id);
// Set to True if you want to BCC yourself on the email.
mail.setBccSender(false);
// Optionally append the Salesforce email signature to the email.
// The email address of the user executing the Apex Code will be used.
mail.setUseSignature(false);
// Specify the text content of the email.
mail.setPlainTextBody('Your Case: ' + case.Id +' has been created.');
mail.setHtmlBody('Your case:<b> ' + case.Id +' </b>has been created.<p>'+
'To view your case <a href=https://MyDomainName.my.salesforce.com/'+case.Id+'>click here.</a>');
// Send the email you have created.
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
trigger ReviewFeedItem on FeedItem (before insert) {
for (Integer i = 0; i<trigger.new.size(); i++) {
// We don't want to leak "test phrase" information.
if (trigger.new[i].body.containsIgnoreCase('test phrase')) {
trigger.new[i].status = 'PendingReview';
System.debug('caught one for pendingReview');
}
}
}
此示例显示了 ChatterMessage 上用于查看的 before insert 触发器 每条新消息。此触发器调用类方法 ,以在插入每条新消息之前对其进行检查。moderator.review()
trigger PrivateMessageModerationTrigger on ChatterMessage (before insert) {
ChatterMessage[] messages = Trigger.new;
// Instantiate the Message Moderator using the factory method
MessageModerator moderator = MessageModerator.getInstance();
for (ChatterMessage currentMessage : messages) {
moderator.review(currentMessage);
}
}
if (proposedMsg.contains(nextBlockListedWord)) {
theMessage.Body.addError(
'This message does not conform to the acceptable use policy');
System.debug('moderation flagged message with word: '
+ nextBlockListedWord);
problemsFound=true;
break;
}
public class MessageModerator {
private Static List<String> blocklistedWords=null;
private Static MessageModerator instance=null;
/**
Overall review includes checking the content of the message,
and validating that the sender is allowed to send messages.
**/
public void review(ChatterMessage theMessage) {
reviewContent(theMessage);
reviewSender(theMessage);
}
/**
This method is used to review the content of the message. If the content
is unacceptable, field level error(s) are added.
**/
public void reviewContent(ChatterMessage theMessage) {
// Forcing to lower case for matching
String proposedMsg=theMessage.Body.toLowerCase();
boolean problemsFound=false; // Assume it's acceptable
// Iterate through the blocklist looking for matches
for (String nextBlockListedWord : blocklistedWords) {
if (proposedMsg.contains(nextBlockListedWord)) {
theMessage.Body.addError(
'This message does not conform to the acceptable use policy');
System.debug('moderation flagged message with word: '
+ nextBlockListedWord);
problemsFound=true;
break;
}
}
// For demo purposes, we're going to add a "seal of approval" to the
// message body which is visible.
if (!problemsFound) {
theMessage.Body = theMessage.Body +
' *** approved, meets conduct guidelines';
}
}
/**
Is the sender allowed to send messages in this context?
-- Moderators -- always allowed to send
-- Internal Members -- always allowed to send
-- Site Members -- in general only allowed to send if they have
a sufficient Reputation
-- Site Members -- with insufficient reputation may message the
moderator(s)
**/
public void reviewSender(ChatterMessage theMessage) {
// Are we in a Site Context?
boolean isSiteContext = (theMessage.SendingNetworkId != null);
// Get the User
User sendingUser = [SELECT Id, Name, UserType, IsPortalEnabled
FROM User where Id = :theMessage.SenderId ];
// ...
}
/**
Enforce a singleton pattern to improve performance
**/
public static MessageModerator getInstance() {
if (instance==null) {
instance = new MessageModerator();
}
return instance;
}
/**
Default contructor is private to prevent others from instantiating this class
without using the factory.
Initializes the static members.
**/
private MessageModerator() {
initializeBlockList();
}
/**
Helper method that does the "heavy lifting" to load up the dictionaries
from the database.
Should only run once to initialize the static member which is used for
subsequent validations.
**/
private void initializeBlockList() {
if (blocklistedWords==null) {
// Fill list of blocklisted words
// ...
}
}
}
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, 'me', ConnectApi.FeedElementType.FeedItem, 'Working from home today.');
新发布的属性 源项包含上下文用户的parentConnectApi.UserSummary发布给其他用户此代码将源项发布到上下文用户以外的用户。指定用户 目标的 ID 用户。subjectId
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, '005D00000016Qxp', ConnectApi.FeedElementType.FeedItem, 'Kevin, do you have information about the new categories?');
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegmentInput.text = 'Does anyone know anyone with contacts here?';
messageBodyInput.messageSegments.add(textSegmentInput);
// Mention a group.
mentionSegmentInput.id = '0F9D00000000oOT';
messageBodyInput.messageSegments.add(mentionSegmentInput);
feedItemInput.body = messageBodyInput;
feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;
// Use a record ID for the subject ID.
feedItemInput.subjectId = '001D000000JVwL9';
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, feedItemInput);
顶层不允许聚合(最小值、最大值、平均值、计数) 选择。---FAIL select max(Individual_dense_viv__dlm.age__c) from Individual_dense_viv__dlm ---FAIL select count(Individual_dense_viv__dlm.individualid__c) from Individual_dense_viv__dlm ---
顶层不允许全选 (*) 表达式 选择。---FAIL select * from Individual_dense_viv__dlm
只有 table 上段的主键(sql 的 from 子句中的第一个表 语句)被允许选择。---PASS select Individual_dense_viv__dlm.individualid__c from Individual_dense_viv__dlm
无法选择多个列,即使其中一个列是 桌子。---FAIL select Individual_dense_viv__dlm.individualid__c, Individual_dense_viv__dlm.age__c from Individual_dense_viv__dlm
主数据中不允许有大小写语句 选择。---FAIL select case when Individual_dense_viv__dlm.individualid__c > 10 then Individual_dense_viv__dlm.individualid__c else null end from Individual_dense_viv__dlm
如果 segmentOn 实体的主键具有键限定符,则可以提供 加入条件中的附加条件。---PASS select Individual__dlm.id__c from Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c and Individual__dlm.kq__id__c is not distinct from Sales__dlm.kq__soldToCustomerId__c
所有列都必须在查询和子选择中由 tablename 完全限定 查询。---FAIL select individualid__c from Individual_dense_viv__dlm
子查询仅在 where 子句中受支持,并且只能发出一个子查询 列。---FAIL select Individual_dense_viv__dlm.individualid__c from (select * from Individual_dense_viv__dlm)
比较相同数据类型的列。若要比较不同数据类型的列,请强制转换一个 或两个操作数,以便它们具有相同的 类型。---PASS select t.id__c from Individual__dlm as t where cast(t.id__c as varchar(100)) = t1.name
支持限制和偏移。---FAIL select Individual_dense_viv__dlm.individualid__c from Individual_dense_viv__dlm limit 10
只有查询的 from 块中的 DMO 才支持别名。列不能是 锯齿。---PASS select t.id__c from Individual__dlm as t
要加入两个 DMO,DMO 之间必须存在关系,并且必须使用 它们在 join on 条件中的相关 join 键。join on 条件只能包含一个 连接键之间的相等性比较和用于比较的可选附加条件 FQK 字段。---PASS select Individual__dlm.id__c from Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c--PASS Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c and Individual__dlm.kq__id__c is not distinct from Sales__dlm.kq__soldToCustomerId__c
使用 ConnectApi 输入和输出类
命名空间中的某些类包含 访问 Connect REST API 数据的静态方法。命名空间还包含要传递的输入类 调用静态方法返回的参数和输出类。
global class NewsFeedClass {
global static Integer getNewsFeedCount() {
ConnectApi.FeedElementPage elements =
ConnectApi.ChatterFeeds.getFeedElementsFromFeed(null,
ConnectApi.FeedType.News, 'me');
return elements.elements.size();
}
}
@isTest
private class NewsFeedClassTest {
@IsTest
static void doTest() {
// Build a simple feed item
ConnectApi.FeedElementPage testPage = new ConnectApi.FeedElementPage();
List<ConnectApi.FeedItem> testItemList = new List<ConnectApi.FeedItem>();
testItemList.add(new ConnectApi.FeedItem());
testItemList.add(new ConnectApi.FeedItem());
testPage.elements = testItemList;
// Set the test data
ConnectApi.ChatterFeeds.setTestGetFeedElementsFromFeed(null,
ConnectApi.FeedType.News, 'me', testPage);
// The method returns the test page, which we know has two items in it.
Test.startTest();
System.assertEquals(2, NewsFeedClass.getNewsFeedCount());
Test.stopTest();
}
}
// Define the FeedItemInput object to pass to postFeedElement
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
feedItemInput.subjectId = 'me';
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
textSegmentInput.text = 'Would you please review these docs?';
// The MessageBodyInput object holds the text in the post
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
messageBodyInput.messageSegments.add(textSegmentInput);
feedItemInput.body = messageBodyInput;
// The FeedElementCapabilitiesInput object holds the capabilities of the feed item.
// For this feed item, we define a files capability to hold the file(s).
List<String> fileIds = new List<String>();
fileIds.add('069xx00000000QO');
fileIds.add('069xx00000000QT');
fileIds.add('069xx00000000Qn');
fileIds.add('069xx00000000Qi');
fileIds.add('069xx00000000Qd');
ConnectApi.FilesCapabilityInput filesInput = new ConnectApi.FilesCapabilityInput();
filesInput.items = new List<ConnectApi.FileIdInput>();
for (String fileId : fileIds) {
ConnectApi.FileIdInput idInput = new ConnectApi.FileIdInput();
idInput.id = fileId;
filesInput.items.add(idInput);
}
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
feedElementCapabilitiesInput.files = filesInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
// Post the feed item.
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
ConnectApi.ActionLinkGroupDefinitionInput actionLinkGroupDefinitionInput = new ConnectApi.ActionLinkGroupDefinitionInput();
ConnectApi.ActionLinkDefinitionInput actionLinkDefinitionInput = new ConnectApi.ActionLinkDefinitionInput();
ConnectApi.RequestHeaderInput requestHeaderInput1 = new ConnectApi.RequestHeaderInput();
ConnectApi.RequestHeaderInput requestHeaderInput2 = new ConnectApi.RequestHeaderInput();
// Create the action link group definition.
actionLinkGroupDefinitionInput.actionLinks = New List<ConnectApi.ActionLinkDefinitionInput>();
actionLinkGroupDefinitionInput.executionsAllowed = ConnectApi.ActionLinkExecutionsAllowed.OncePerUser;
actionLinkGroupDefinitionInput.category = ConnectApi.PlatformActionGroupCategory.Primary;
// To Do: Verify that the date is in the future.
// Action link groups are removed from feed elements on the expiration date.
datetime myDate = datetime.newInstance(2016, 3, 1);
actionLinkGroupDefinitionInput.expirationDate = myDate;
// Create the action link definition.
actionLinkDefinitionInput.actionType = ConnectApi.ActionLinkType.Api;
actionLinkDefinitionInput.actionUrl = '/services/data/v33.0/chatter/feed-elements';
actionLinkDefinitionInput.headers = new List<ConnectApi.RequestHeaderInput>();
actionLinkDefinitionInput.labelKey = 'Post';
actionLinkDefinitionInput.method = ConnectApi.HttpRequestMethod.HttpPost;
actionLinkDefinitionInput.requestBody = '{\"subjectId\": \"me\",\"feedElementType\": \"FeedItem\",\"body\": {\"messageSegments\": [{\"type\": \"Text\",\"text\": \"This is a test post created via an API action link.\"}]}}';
actionLinkDefinitionInput.requiresConfirmation = true;
// To Do: Substitute an OAuth value for your Salesforce org.
requestHeaderInput1.name = 'Authorization';
requestHeaderInput1.value = 'OAuth 00DD00000007WNP!ARsAQCwoeV0zzAV847FTl4zF.85w.EwsPbUgXR4SAjsp';
actionLinkDefinitionInput.headers.add(requestHeaderInput1);
requestHeaderInput2.name = 'Content-Type';
requestHeaderInput2.value = 'application/json';
actionLinkDefinitionInput.headers.add(requestHeaderInput2);
// Add the action link definition to the action link group definition.
actionLinkGroupDefinitionInput.actionLinks.add(actionLinkDefinitionInput);
// Instantiate the action link group definition.
ConnectApi.ActionLinkGroupDefinition actionLinkGroupDefinition = ConnectApi.ActionLinks.createActionLinkGroupDefinition(Network.getNetworkId(), actionLinkGroupDefinitionInput);
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
ConnectApi.AssociatedActionsCapabilityInput associatedActionsCapabilityInput = new ConnectApi.AssociatedActionsCapabilityInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
// Set the properties of the feedItemInput object.
feedItemInput.body = messageBodyInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
feedItemInput.subjectId = 'me';
// Create the text for the post.
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegmentInput.text = 'Click to post a feed item.';
messageBodyInput.messageSegments.add(textSegmentInput);
// The feedElementCapabilitiesInput object holds the capabilities of the feed item.
// Define an associated actions capability to hold the action link group.
// The action link group ID is returned from the call to create the action link group definition.
feedElementCapabilitiesInput.associatedActions = associatedActionsCapabilityInput;
associatedActionsCapabilityInput.actionLinkGroupIds = new List<String>();
associatedActionsCapabilityInput.actionLinkGroupIds.add(actionLinkGroupDefinition.id);
// Post the feed item.
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
注意
如果 开机自检失败,请检查 OAuth ID。
在模板中定义操作链接并使用 Feed 元素发布
创建操作链接和操作链接组,并从 一个模板。
此示例创建与示例 Define an Action Link and Post with a Feed Element 相同的操作链接和操作链接组,但此示例 从模板实例化操作链接组。
步骤 1:创建操作链接模板
在“设置”中,输入“快速” “查找”框,然后选择“操作链接模板”。Action Link Templates
// Get the action link group template Id.
ActionLinkGroupTemplate template = [SELECT Id FROM ActionLinkGroupTemplate WHERE DeveloperName='Doc_Example'];
// Add binding name-value pairs to a map.
// The names are defined in the action link template(s) associated with the action link group template.
// Get them from Setup UI or SOQL.
Map<String, String> bindingMap = new Map<String, String>();
bindingMap.put('ApiVersion', 'v33.0');
bindingMap.put('Text', 'This post was created by an API action link.');
bindingMap.put('SubjectId', 'me');
// Create ActionLinkTemplateBindingInput objects from the map elements.
List<ConnectApi.ActionLinkTemplateBindingInput> bindingInputs = new List<ConnectApi.ActionLinkTemplateBindingInput>();
for (String key : bindingMap.keySet()) {
ConnectApi.ActionLinkTemplateBindingInput bindingInput = new ConnectApi.ActionLinkTemplateBindingInput();
bindingInput.key = key;
bindingInput.value = bindingMap.get(key);
bindingInputs.add(bindingInput);
}
// Set the template Id and template binding values in the action link group definition.
ConnectApi.ActionLinkGroupDefinitionInput actionLinkGroupDefinitionInput = new ConnectApi.ActionLinkGroupDefinitionInput();
actionLinkGroupDefinitionInput.templateId = template.id;
actionLinkGroupDefinitionInput.templateBindings = bindingInputs;
// Instantiate the action link group definition.
ConnectApi.ActionLinkGroupDefinition actionLinkGroupDefinition =
ConnectApi.ActionLinks.createActionLinkGroupDefinition(Network.getNetworkId(), actionLinkGroupDefinitionInput);
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
ConnectApi.AssociatedActionsCapabilityInput associatedActionsCapabilityInput = new ConnectApi.AssociatedActionsCapabilityInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
// Define the FeedItemInput object to pass to postFeedElement
feedItemInput.body = messageBodyInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
feedItemInput.subjectId = 'me';
// The MessageBodyInput object holds the text in the post
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegmentInput.text = 'Click to post a feed item.';
messageBodyInput.messageSegments.add(textSegmentInput);
// The FeedElementCapabilitiesInput object holds the capabilities of the feed item.
// For this feed item, we define an associated actions capability to hold the action link group.
// The action link group ID is returned from the call to create the action link group definition.
feedElementCapabilitiesInput.associatedActions = associatedActionsCapabilityInput;
associatedActionsCapabilityInput.actionLinkGroupIds = new List<String>();
associatedActionsCapabilityInput.actionLinkGroupIds.add(actionLinkGroupDefinition.id);
// Post the feed item.
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
String communityId = Network.getNetworkId();
// Get the last feed item created by the context user.
List<FeedItem> feedItems = [SELECT Id FROM FeedItem WHERE CreatedById = :UserInfo.getUserId() ORDER BY CreatedDate DESC];
if (feedItems.isEmpty()) {
// Return null within anonymous apex.
return null;
}
String feedElementId = feedItems[0].id;
ConnectApi.FeedEntityIsEditable isEditable = ConnectApi.ChatterFeeds.isFeedElementEditableByMe(communityId, feedElementId);
if (isEditable.isEditableByMe == true){
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegmentInput.text = 'This is my edited post.';
messageBodyInput.messageSegments.add(textSegmentInput);
feedItemInput.body = messageBodyInput;
ConnectApi.FeedElement editedFeedElement = ConnectApi.ChatterFeeds.updateFeedElement(communityId, feedElementId, feedItemInput);
}
String communityId = Network.getNetworkId();
// Get the last feed item created by the context user.
List<FeedItem> feedItems = [SELECT Id FROM FeedItem WHERE CreatedById = :UserInfo.getUserId() ORDER BY CreatedDate DESC];
if (feedItems.isEmpty()) {
// Return null within anonymous apex.
return null;
}
String feedElementId = feedItems[0].id;
ConnectApi.FeedEntityIsEditable isEditable = ConnectApi.ChatterFeeds.isFeedElementEditableByMe(communityId, feedElementId);
if (isEditable.isEditableByMe == true){
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
ConnectApi.QuestionAndAnswersCapabilityInput questionAndAnswersCapabilityInput = new ConnectApi.QuestionAndAnswersCapabilityInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegmentInput.text = 'This is my edited question.';
messageBodyInput.messageSegments.add(textSegmentInput);
feedItemInput.body = messageBodyInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
feedElementCapabilitiesInput.questionAndAnswers = questionAndAnswersCapabilityInput;
questionAndAnswersCapabilityInput.questionTitle = 'Where is my edited question?';
ConnectApi.FeedElement editedFeedElement = ConnectApi.ChatterFeeds.updateFeedElement(communityId, feedElementId, feedItemInput);
}
// Define the FeedItemInput object to pass to postFeedElement
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
feedItemInput.subjectId = 'me';
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
textSegmentInput.text = 'Look at this post I'm sharing.';
// The MessageBodyInput object holds the text in the post
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
messageBodyInput.messageSegments.add(textSegmentInput);
feedItemInput.body = messageBodyInput;
ConnectApi.FeedEntityShareCapabilityInput shareInput = new ConnectApi.FeedEntityShareCapabilityInput();
shareInput.feedEntityId = '0D5R0000000SEbc';
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new
ConnectApi.FeedElementCapabilitiesInput();
feedElementCapabilitiesInput.feedEntityShare = shareInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
// Post the feed item.
ConnectApi.FeedElement feedElement =
ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
// Define the FeedItemInput object to pass to postFeedElement
ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
textSegmentInput.text = 'Thanks for attending my presentation test run this morning. Send me any feedback.';
// The MessageBodyInput object holds the text in the post
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
messageBodyInput.messageSegments.add(textSegmentInput);
feedItemInput.body = messageBodyInput;
// The FeedElementCapabilitiesInput object holds the capabilities of the feed item.
// For this feed item, we define a direct message capability to hold the member(s) and the subject.
List<String> memberIds = new List<String>();
memberIds.add('005B00000016OUQ');
memberIds.add('005B0000001rIN6');
ConnectApi.DirectMessageCapabilityInput dmInput = new ConnectApi.DirectMessageCapabilityInput();
dmInput.subject = 'Thank you!';
dmInput.membersToAdd = memberIds;
ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
feedElementCapabilitiesInput.directMessage = dmInput;
feedItemInput.capabilities = feedElementCapabilitiesInput;
// Post the feed item.
ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
final string repositoryId = '0XCxx0000000123GAA';
final ConnectApi.ContentHubRepository repository = ConnectApi.ContentHub.getRepository(repositoryId);
ConnectApi.OAuthCredentialAuthUrlInput input = new ConnectApi.OAuthCredentialAuthUrlInput();
input.externalCredential = 'MyExternalCredentialDeveloperName';
input.principalType = ConnectApi.CredentialPrincipalType.PerUserPrincipal;
input.principalName = 'MyPrincipal'; // Only required when principalType = NamedPrincipal
ConnectApi.OAuthCredentialAuthUrl output = ConnectApi.NamedCredentials.getOAuthCredentialAuthUrl(input);
String authenticationUrl = output.authenticationUrl; // Redirect users to this URL to authenticate in the browser
global with sharing class SampleAdapter implements commercepayments.PaymentGatewayAdapter {
global SampleAdapter() {}
global commercepayments.GatewayResponse processRequest(commercepayments.paymentGatewayContext gatewayContext) {
}
}
处理初始付款请求
当支付平台 接收付款 API 请求,并将请求传递给网关适配器 进一步评估。适配器通过调用 processRequest 方法开始请求评估过程,该方法表示同步的第一步 付款流程。我们可以将 processRequest 实现分为三个部分。
首先,它构建一个网关可以 理解。
commercepayments.RequestType requestType = gatewayContext.getPaymentRequestType();
if (requestType == commercepayments.RequestType.Capture) {
req.setEndpoint('/pal/servlet/Payment/v52/capture');
body = buildCaptureRequest((commercepayments.CaptureRequest)gatewayContext.getPaymentRequest());
} else if (requestType == commercepayments.RequestType.ReferencedRefund) {
req.setEndpoint('/pal/servlet/Payment/v52/refund');
body = buildRefundRequest((commercepayments.ReferencedRefundRequest)gatewayContext.getPaymentRequest());
}
然后 适配器将请求发送到付款 网关。
req.setBody(body);
req.setMethod('POST');
commercepayments.PaymentsHttp http = new commercepayments.PaymentsHttp();
HttpResponse res = null;
try {
res = http.send(req);
} catch(CalloutException ce) {
commercepayments.GatewayErrorResponse error = new commercepayments.GatewayErrorResponse('500', ce.getMessage());
return error;
}
if ( requestType == commercepayments.RequestType.Capture) {
// Refer to the end of this doc for sample createCaptureResponse implementation
response = createCaptureResponse(res);
} else if ( requestType == commercepayments.RequestType.ReferencedRefund) {
response = createRefundResponse(res);
}
return response;
global with sharing class SampleAdapter implements commercepayments.PaymentGatewayAsyncAdapter, commercepayments.PaymentGatewayAdapter {
global SampleAdapter() {}
global commercepayments.GatewayResponse processRequest(commercepayments.paymentGatewayContext gatewayContext) {
}
global commercepayments.GatewayNotificationResponse processNotification(commercepayments.PaymentGatewayNotificationContext gatewayNotificationContext) {
}
}
处理初始付款请求
当支付平台 接收付款 API 请求,并将请求传递给网关适配器 进一步评估。适配器通过调用 processRequest 方法开始请求评估过程,该方法表示异步的第一步 付款流程。我们可以将 processRequest 实现分为三个部分。
首先,它构建一个网关可以 理解。
commercepayments.RequestType requestType = gatewayContext.getPaymentRequestType();
if (requestType == commercepayments.RequestType.Capture) {
req.setEndpoint('/pal/servlet/Payment/v52/capture');
body = buildCaptureRequest((commercepayments.CaptureRequest)gatewayContext.getPaymentRequest());
} else if (requestType == commercepayments.RequestType.ReferencedRefund) {
req.setEndpoint('/pal/servlet/Payment/v52/refund');
body = buildRefundRequest((commercepayments.ReferencedRefundRequest)gatewayContext.getPaymentRequest());
}
然后 适配器将请求发送到付款 网关。
req.setBody(body);
req.setMethod('POST');
commercepayments.PaymentsHttp http = new commercepayments.PaymentsHttp();
HttpResponse res = null;
try {
res = http.send(req);
} catch(CalloutException ce) {
commercepayments.GatewayErrorResponse error = new commercepayments.GatewayErrorResponse('500', ce.getMessage());
return error;
}
if ( requestType == commercepayments.RequestType.Capture) {
// Refer to the end of this doc for sample createCaptureResponse implementation
response = createCaptureResponse(res);
} else if ( requestType == commercepayments.RequestType.ReferencedRefund) {
response = createRefundResponse(res);
}
return response;
commercepayments.GatewayNotificationResponse gnr = new commercepayments.GatewayNotificationResponse();
if (saveResult.isSuccess()) {
system.debug('Notification accepted by platform');
} else {
system.debug('Errors in the result '+ Blob.valueOf(saveResult.getErrorMessage()));
}
gnr.setStatusCode(200);
gnr.setResponseBody(Blob.valueOf('[accepted]'));
return gnr;
/** @description Method to set Gateway token to persist in Encrypted Text */
global void setGatewayTokenEncrypted(String gatewayTokenEncrypted) {
if (gatewayTokenSet) {
throwTokenError();
}
this.delegate.setGatewayTokenEncrypted(gatewayTokenEncrypted);
gatewayTokenEncryptedSet = true;
}
如果实例化的类已具有网关令牌,则会引发错误。setGatewayTokenEncrypted
注意
虽然 PaymentMethodTokenizationResponse 的 setGatewayToken 方法(在 API v48.0 及更高版本中可用)也返回 付款方式令牌,则令牌化值未加密。
public class TestApproval {
void submitAndProcessApprovalRequest() {
// Insert an account
Account a = new Account(Name='Test',annualRevenue=100.0);
insert a;
User user1 = [SELECT Id FROM User WHERE Alias='SomeStandardUser'];
// Create an approval request for the account
Approval.ProcessSubmitRequest req1 =
new Approval.ProcessSubmitRequest();
req1.setComments('Submitting request for approval.');
req1.setObjectId(a.id);
// Submit on behalf of a specific submitter
req1.setSubmitterId(user1.Id);
// Submit the record to specific process and skip the criteria evaluation
req1.setProcessDefinitionNameOrId('PTO_Request_Process');
req1.setSkipEntryCriteria(true);
// Submit the approval request for the account
Approval.ProcessResult result = Approval.process(req1);
// Verify the result
System.assert(result.isSuccess());
System.assertEquals(
'Pending', result.getInstanceStatus(),
'Instance Status'+result.getInstanceStatus());
// Approve the submitted request
// First, get the ID of the newly created item
List<Id> newWorkItemIds = result.getNewWorkitemIds();
// Instantiate the new ProcessWorkitemRequest object and populate it
Approval.ProcessWorkitemRequest req2 =
new Approval.ProcessWorkitemRequest();
req2.setComments('Approving request.');
req2.setAction('Approve');
req2.setNextApproverIds(new Id[] {UserInfo.getUserId()});
// Use the ID from the newly created item to specify the item to be worked
req2.setWorkitemId(newWorkItemIds.get(0));
// Submit the request for approval
Approval.ProcessResult result2 = Approval.process(req2);
// Verify the results
System.assert(result2.isSuccess(), 'Result Status:'+result2.isSuccess());
System.assertEquals(
'Approved', result2.getInstanceStatus(),
'Instance Status'+result2.getInstanceStatus());
}
}
global class Concur extends Auth.AuthProviderPluginClass {
public String redirectUrl; // use this URL for the endpoint that the authentication provider calls back to for configuration
private String key;
private String secret;
private String authUrl; // application redirection to the Concur website for authentication and authorization
private String accessTokenUrl; // uri to get the new access token from concur using the GET verb
private String customMetadataTypeApiName; // api name for the custom metadata type created for this auth provider
private String userAPIUrl; // api url to access the user in concur
private String userAPIVersionUrl; // version of the user api url to access data from concur
global String getCustomMetadataType() {
return customMetadataTypeApiName;
}
global PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
authUrl = authProviderConfiguration.get('Auth_Url__c');
key = authProviderConfiguration.get('Key__c');
//Here the developer can build up a request of some sort
//Ultimately they’ll return a URL where we will redirect the user
String url = authUrl + '?client_id='+ key +'&scope=USER,EXPRPT,LIST&redirect_uri='+ redirectUrl + '&state=' + stateToPropagate;
return new PageReference(url);
}
global Auth.AuthProviderTokenResponse handleCallback(Map<string,string> authProviderConfiguration, Auth.AuthProviderCallbackState state ) {
//Here, the developer will get the callback with actual protocol.
//Their responsibility is to return a new object called AuthProviderToken
//This will contain an optional accessToken and refreshToken
key = authProviderConfiguration.get('Key__c');
secret = authProviderConfiguration.get('Secret__c');
accessTokenUrl = authProviderConfiguration.get('Access_Token_Url__c');
Map<String,String> queryParams = state.queryParameters;
String code = queryParams.get('code');
String sfdcState = queryParams.get('state');
HttpRequest req = new HttpRequest();
String url = accessTokenUrl+'?code=' + code + '&client_id=' + key + '&client_secret=' + secret;
req.setEndpoint(url);
req.setHeader('Content-Type','application/xml');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
String responseBody = res.getBody();
String accessToken = getTokenValueFromResponse(responseBody, 'AccessToken', null);
//Parse access token value
String refreshToken = getTokenValueFromResponse(responseBody, 'RefreshToken', null);
//Parse refresh token value
return new Auth.AuthProviderTokenResponse('Concur', accessToken, 'refreshToken', sfdcState);
//don’t hard-code the refresh token value!
}
global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
//Here the developer is responsible for constructing an Auth.UserData object
String token = response.oauthToken;
HttpRequest req = new HttpRequest();
userAPIUrl = authProviderConfiguration.get('API_User_Url__c');
userAPIVersionUrl = authProviderConfiguration.get('API_User_Version_Url__c');
req.setHeader('Authorization', 'OAuth ' + token);
req.setEndpoint(userAPIUrl);
req.setHeader('Content-Type','application/xml');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
String responseBody = res.getBody();
String id = getTokenValueFromResponse(responseBody, 'LoginId',userAPIVersionUrl);
String fname = getTokenValueFromResponse(responseBody, 'FirstName', userAPIVersionUrl);
String lname = getTokenValueFromResponse(responseBody, 'LastName', userAPIVersionUrl);
String flname = fname + ' ' + lname;
String uname = getTokenValueFromResponse(responseBody, 'EmailAddress', userAPIVersionUrl);
String locale = getTokenValueFromResponse(responseBody, 'LocaleName', userAPIVersionUrl);
Map<String,String> provMap = new Map<String,String>();
provMap.put('what1', 'noidea1');
provMap.put('what2', 'noidea2');
return new Auth.UserData(id, fname, lname, flname, uname,
'what', locale, null, 'Concur', null, provMap);
}
private String getTokenValueFromResponse(String response, String token, String ns) {
Dom.Document docx = new Dom.Document();
docx.load(response);
String ret = null;
dom.XmlNode xroot = docx.getrootelement() ;
if(xroot != null){
ret = xroot.getChildElement(token, ns).getText();
}
return ret;
}
}
示例测试类
下面的示例包含 Concur 类的测试类。
@IsTest
public class ConcurTestClass {
private static final String OAUTH_TOKEN = 'testToken';
private static final String STATE = 'mocktestState';
private static final String REFRESH_TOKEN = 'refreshToken';
private static final String LOGIN_ID = 'testLoginId';
private static final String USERNAME = 'testUsername';
private static final String FIRST_NAME = 'testFirstName';
private static final String LAST_NAME = 'testLastName';
private static final String EMAIL_ADDRESS = 'testEmailAddress';
private static final String LOCALE_NAME = 'testLocalName';
private static final String FULL_NAME = FIRST_NAME + ' ' + LAST_NAME;
private static final String PROVIDER = 'Concur';
private static final String REDIRECT_URL = 'http://localhost/services/authcallback/orgId/Concur';
private static final String KEY = 'testKey';
private static final String SECRET = 'testSecret';
private static final String STATE_TO_PROPOGATE = 'testState';
private static final String ACCESS_TOKEN_URL = 'http://www.dummyhost.com/accessTokenUri';
private static final String API_USER_VERSION_URL = 'http://www.dummyhost.com/user/20/1';
private static final String AUTH_URL = 'http://www.dummy.com/authurl';
private static final String API_USER_URL = 'www.concursolutions.com/user/api';
// in the real world scenario , the key and value would be read from the (custom fields in) custom metadata type record
private static Map<String,String> setupAuthProviderConfig () {
Map<String,String> authProviderConfiguration = new Map<String,String>();
authProviderConfiguration.put('Key__c', KEY);
authProviderConfiguration.put('Auth_Url__c', AUTH_URL);
authProviderConfiguration.put('Secret__c', SECRET);
authProviderConfiguration.put('Access_Token_Url__c', ACCESS_TOKEN_URL);
authProviderConfiguration.put('API_User_Url__c',API_USER_URL);
authProviderConfiguration.put('API_User_Version_Url__c',API_USER_VERSION_URL);
authProviderConfiguration.put('Redirect_Url__c',REDIRECT_URL);
return authProviderConfiguration;
}
static testMethod void testInitiateMethod() {
String stateToPropogate = 'mocktestState';
Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
Concur concurCls = new Concur();
concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url__c');
PageReference expectedUrl = new PageReference(authProviderConfiguration.get('Auth_Url__c') + '?client_id='+
authProviderConfiguration.get('Key__c') +'&scope=USER,EXPRPT,LIST&redirect_uri='+
authProviderConfiguration.get('Redirect_Url__c') + '&state=' +
STATE_TO_PROPOGATE);
PageReference actualUrl = concurCls.initiate(authProviderConfiguration, STATE_TO_PROPOGATE);
System.assertEquals(expectedUrl.getUrl(), actualUrl.getUrl());
}
static testMethod void testHandleCallback() {
Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
Concur concurCls = new Concur();
concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url_c');
Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
Map<String,String> queryParams = new Map<String,String>();
queryParams.put('code','code');
queryParams.put('state',authProviderConfiguration.get('State_c'));
Auth.AuthProviderCallbackState cbState = new Auth.AuthProviderCallbackState(null,null,queryParams);
Auth.AuthProviderTokenResponse actualAuthProvResponse = concurCls.handleCallback(authProviderConfiguration, cbState);
Auth.AuthProviderTokenResponse expectedAuthProvResponse = new Auth.AuthProviderTokenResponse('Concur', OAUTH_TOKEN, REFRESH_TOKEN, null);
System.assertEquals(expectedAuthProvResponse.provider, actualAuthProvResponse.provider);
System.assertEquals(expectedAuthProvResponse.oauthToken, actualAuthProvResponse.oauthToken);
System.assertEquals(expectedAuthProvResponse.oauthSecretOrRefreshToken, actualAuthProvResponse.oauthSecretOrRefreshToken);
System.assertEquals(expectedAuthProvResponse.state, actualAuthProvResponse.state);
}
static testMethod void testGetUserInfo() {
Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
Concur concurCls = new Concur();
Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
Auth.AuthProviderTokenResponse response = new Auth.AuthProviderTokenResponse(PROVIDER, OAUTH_TOKEN ,'sampleOauthSecret', STATE);
Auth.UserData actualUserData = concurCls.getUserInfo(authProviderConfiguration, response) ;
Map<String,String> provMap = new Map<String,String>();
provMap.put('key1', 'value1');
provMap.put('key2', 'value2');
Auth.UserData expectedUserData = new Auth.UserData(LOGIN_ID, FIRST_NAME, LAST_NAME, FULL_NAME, EMAIL_ADDRESS,
null, LOCALE_NAME, null, PROVIDER, null, provMap);
System.assertNotEquals(expectedUserData,null);
System.assertEquals(expectedUserData.firstName, actualUserData.firstName);
System.assertEquals(expectedUserData.lastName, actualUserData.lastName);
System.assertEquals(expectedUserData.fullName, actualUserData.fullName);
System.assertEquals(expectedUserData.email, actualUserData.email);
System.assertEquals(expectedUserData.username, actualUserData.username);
System.assertEquals(expectedUserData.locale, actualUserData.locale);
System.assertEquals(expectedUserData.provider, actualUserData.provider);
System.assertEquals(expectedUserData.siteLoginUrl, actualUserData.siteLoginUrl);
}
// implementing a mock http response generator for concur
public class ConcurMockHttpResponseGenerator implements HttpCalloutMock {
public HTTPResponse respond(HTTPRequest req) {
String namespace = API_USER_VERSION_URL;
String prefix = 'mockPrefix';
Dom.Document doc = new Dom.Document();
Dom.XmlNode xmlNode = doc.createRootElement('mockRootNodeName', namespace, prefix);
xmlNode.addChildElement('LoginId', namespace, prefix).addTextNode(LOGIN_ID);
xmlNode.addChildElement('FirstName', namespace, prefix).addTextNode(FIRST_NAME);
xmlNode.addChildElement('LastName', namespace, prefix).addTextNode(LAST_NAME);
xmlNode.addChildElement('EmailAddress', namespace, prefix).addTextNode(EMAIL_ADDRESS);
xmlNode.addChildElement('LocaleName', namespace, prefix).addTextNode(LOCALE_NAME);
xmlNode.addChildElement('AccessToken', null, null).addTextNode(OAUTH_TOKEN);
xmlNode.addChildElement('RefreshToken', null, null).addTextNode(REFRESH_TOKEN);
System.debug(doc.toXmlString());
// Create a fake response
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/xml');
res.setBody(doc.toXmlString());
res.setStatusCode(200);
return res;
}
}
}
// Only 1,000 pencils are in stock.
// Purchasing 5,000 pencils cause the validation rule to fail,
// which results in an exception in the invoice method.
Id invoice = MerchandiseOperations.invoice('Pencils', 5000, 'test 1');
public class MerchandiseOperations {
public static Id invoice( String pName, Integer pSold, String pDesc) {
// Retrieve the pencils sample merchandise
Merchandise__c m = [SELECT Price__c,Total_Inventory__c
FROM Merchandise__c WHERE Name = :pName LIMIT 1];
// break if no merchandise is found
System.assertNotEquals(null, m);
// Add a new invoice
Invoice_Statement__c i = new Invoice_Statement__c(
Description__c = pDesc);
insert i;
// Add a new line item to the invoice
Line_Item__c li = new Line_Item__c(
Name = '1',
Invoice_Statement__c = i.Id,
Merchandise__c = m.Id,
Unit_Price__c = m.Price__c,
Units_Sold__c = pSold);
insert li;
// Update the inventory of the merchandise item
m.Total_Inventory__c -= pSold;
// This causes an exception due to the validation rule
// if there is not enough inventory.
update m;
return i.Id;
}
}
for 循环遍历 List 变量中包含的行项。对于每个行项,它会为 Description__c字段,然后更新订单项。如果列表包含超过 150 个项目,第 151 个更新调用返回超出 DML 的运行时异常 语句限制为 150。我们如何解决这个问题?查看第二个示例的简单 溶液。liList
for(Line_Item__c li : liList) {
if (li.Units_Sold__c > 10) {
li.Description__c = 'New description';
}
// Not a good practice since governor limits might be hit.
update li;
}
List<Line_Item__c> updatedList = new List<Line_Item__c>();
for(Line_Item__c li : liList) {
if (li.Units_Sold__c > 10) {
li.Description__c = 'New description';
updatedList.add(li);
}
}
// Once DML call for the entire list of line items
update updatedList;
trigger LimitExample on Invoice_Statement__c (before insert, before update) {
for(Invoice_Statement__c inv : Trigger.new) {
// This SOQL query executes once for each item in Trigger.new.
// It gets the line items for each invoice statement.
List<Line_Item__c> liList = [SELECT Id,Units_Sold__c,Merchandise__c
FROM Line_Item__c
WHERE Invoice_Statement__c = :inv.Id];
for(Line_Item__c li : liList) {
// Do something
}
}
}
trigger EnhancedLimitExample on Invoice_Statement__c (before insert, before update) {
// Perform SOQL query outside of the for loop.
// This SOQL query runs once for all items in Trigger.new.
List<Invoice_Statement__c> invoicesWithLineItems =
[SELECT Id,Description__c,(SELECT Id,Units_Sold__c,Merchandise__c from Line_Items__r)
FROM Invoice_Statement__c WHERE Id IN :Trigger.newMap.KeySet()];
for(Invoice_Statement__c inv : invoicesWithLineItems) {
for(Line_Item__c li : inv.Line_Items__r) {
// Do something
}
}
}
SOQL For 循环
使用 SOQL for 循环以 200 条为一组对记录进行操作。这有助于避免堆 大小限制为 6 MB。请注意,此限制适用于同步运行的代码,它是 对于异步代码执行,该值更高。
例:不带 for 循环的查询
以下是检索所有商品和 将它们存储在 List 变量中。如果退货商品尺寸较大 并且返回了大量它们,可能会达到堆大小限制。
List<Merchandise__c> ml = [SELECT Id,Name FROM Merchandise__c];
推荐的替代方案:for 循环中的查询
为了防止这种情况发生,第二个版本使用了 SOQL for 循环,该循环 以 200 条记录为一组循环访问返回的结果。这样可以减小尺寸 现在包含 200 的 list 变量 项,而不是查询结果中的所有项,并为每个项重新创建 批。ml
for (List<Merchandise__c> ml : [SELECT Id,Name FROM Merchandise__c]){
// Do something.
}
public with sharing class CreateTaskEmailExample implements Messaging.InboundEmailHandler {
public Messaging.InboundEmailResult handleInboundEmail(Messaging.inboundEmail email,
Messaging.InboundEnvelope env){
// Create an InboundEmailResult object for returning the result of the
// Apex Email Service
Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();
String myPlainText= '';
// Add the email plain text into the local variable
myPlainText = email.plainTextBody;
// New Task object to be created
Task[] newTask = new Task[0];
// Try to look up any contacts based on the email from address
// If there is more than one contact with the same email address,
// an exception will be thrown and the catch statement will be called.
try {
Contact vCon = [SELECT Id, Name, Email
FROM Contact
WHERE Email = :email.fromAddress
WITH USER_MODE
LIMIT 1];
// Add a new Task to the contact record we just found above.
newTask.add(new Task(Description = myPlainText,
Priority = 'Normal',
Status = 'Inbound Email',
Subject = email.subject,
IsReminderSet = true,
ReminderDateTime = System.now()+1,
WhoId = vCon.Id));
// Insert the new Task
insert as user newTask;
System.debug('New Task Object: ' + newTask );
}
// If an exception occurs when the query accesses
// the contact record, a QueryException is called.
// The exception is written to the Apex debug log.
catch (QueryException e) {
System.debug('Query Issue: ' + e);
}
// Set the result to true. No need to send an email back to the user
// with an error message
result.success = true;
// Return the result for the Apex Email Service
return result;
}
}
public with sharing class unsubscribe implements Messaging.inboundEmailHandler{
public Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email,
Messaging.InboundEnvelope env ) {
// Create an inboundEmailResult object for returning
// the result of the email service.
Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();
// Create contact and lead lists to hold all the updated records.
List<Contact> lc = new List <contact>();
List<Lead> ll = new List <lead>();
// Convert the subject line to lower case so the program can match on lower case.
String mySubject = email.subject.toLowerCase();
// The search string used in the subject line.
String s = 'unsubscribe';
// Check the variable to see if the word "unsubscribe" was found in the subject line.
Boolean unsubMe;
// Look for the word "unsubcribe" in the subject line.
// If it is found, return true; otherwise, return false.
unsubMe = mySubject.contains(s);
// If unsubscribe is found in the subject line, enter the IF statement.
if (unsubMe == true) {
try {
// Look up all contacts with a matching email address.
for (Contact c : [SELECT Id, Name, Email, HasOptedOutOfEmail
FROM Contact
WHERE Email = :env.fromAddress
AND hasOptedOutOfEmail = false
WITH USER_MODE
LIMIT 100]) {
// Add all the matching contacts into the list.
c.hasOptedOutOfEmail = true;
lc.add(c);
}
// Update all of the contact records.
update as user lc;
}
catch (System.QueryException e) {
System.debug('Contact Query Issue: ' + e);
}
try {
// Look up all leads matching the email address.
for (Lead l : [SELECT Id, Name, Email, HasOptedOutOfEmail
FROM Lead
WHERE Email = :env.fromAddress
AND isConverted = false
AND hasOptedOutOfEmail = false
WITH USER_MODE
LIMIT 100]) {
// Add all the leads to the list.
l.hasOptedOutOfEmail = true;
ll.add(l);
System.debug('Lead Object: ' + l);
}
// Update all lead records in the query.
update as user ll;
}
catch (System.QueryException e) {
System.debug('Lead Query Issue: ' + e);
}
System.debug('Found the unsubscribe word in the subject line.');
}
else {
System.debug('No Unsuscribe word found in the subject line.' );
}
// Return True and exit.
// True confirms program is complete and no emails
// should be sent to the sender of the unsubscribe request.
result.success = true;
return result;
}
}
@isTest
private class unsubscribeTest {
// The following test methods provide adequate code coverage
// for the unsubscribe email class.
// There are two methods, one that does the testing
// with a valid "unsubcribe" in the subject line
// and one the does not contain "unsubscribe" in the
// subject line.
static testMethod void testUnsubscribe() {
// Create a new email and envelope object.
Messaging.InboundEmail email = new Messaging.InboundEmail() ;
Messaging.InboundEnvelope env = new Messaging.InboundEnvelope();
// Create a new test lead and insert it in the test method.
Lead l = new lead(firstName='John',
lastName='Smith',
Company='Salesforce',
Email='user@acme.com',
HasOptedOutOfEmail=false);
insert l;
// Create a new test contact and insert it in the test method.
Contact c = new Contact(firstName='john',
lastName='smith',
Email='user@acme.com',
HasOptedOutOfEmail=false);
insert c;
// Test with the subject that matches the unsubscribe statement.
email.subject = 'test unsubscribe test';
env.fromAddress = 'user@acme.com';
// Call the class and test it with the data in the testMethod.
unsubscribe unsubscribeObj = new unsubscribe();
unsubscribeObj.handleInboundEmail(email, env );
}
static testMethod void testUnsubscribe2() {
// Create a new email and envelope object.
Messaging.InboundEmail email = new Messaging.InboundEmail();
Messaging.InboundEnvelope env = new Messaging.InboundEnvelope();
// Create a new test lead and insert it in the test method.
Lead l = new lead(firstName='john',
lastName='smith',
Company='Salesforce',
Email='user@acme.com',
HasOptedOutOfEmail=false);
insert l;
// Create a new test contact and insert it in the test method.
Contact c = new Contact(firstName='john',
lastName='smith',
Email='user@acme.com',
HasOptedOutOfEmail=false);
insert c;
// Test with a subject that does not contain "unsubscribe."
email.subject = 'test';
env.fromAddress = 'user@acme.com';
// Call the class and test it with the data in the test method.
unsubscribe unsubscribeObj = new unsubscribe();
unsubscribeObj.handleInboundEmail(email, env );
// Assert that the Lead and Contact have been unsubscribed
Lead updatedLead = [Select Id, HasOptedOutOfEmail from Lead where Id = :l.Id];
Contact updatedContact = [Select Id, HasOptedOutOfEmail from Contact where Id = :c.Id];
Assert.isTrue(l.HasOptedOutOfEmail);
Assert.isTrue(c.HasOptedOutOfEmail);
}
}
您可以将 Apex 方法公开为 SOAP Web 服务,以便外部应用程序 可以访问您的代码和应用程序。
若要公开 Apex 方法,请使用 Web 服务方法。
提示
Apex SOAP Web 服务允许外部应用程序调用 Apex 方法 通过 SOAP Web 服务。Apex 标注使 Apex 能够调用外部 Web 或 HTTP 服务业。
Apex REST API 将您的 Apex 类和方法公开为 REST Web 服务。请参阅将 Apex 类公开为 REST Web 服务。
Webservice 方法
使用 Web 服务方法公开数据
使用 webservice 关键字的注意事项
重载 Web 服务方法
Webservice 方法
Apex 类方法可以公开为自定义 SOAP Web 服务调用。 这允许外部应用程序调用 Apex Web 服务来执行 Salesforce 中的操作。使用关键字来定义这些方法。webservice为 例:
global class MyWebService {
webservice static Id makeContact(String contactLastName, Account a) {
Contact c = new Contact(lastName = contactLastName, AccountId = a.Id);
insert c;
return c.id;
}
}
调用自定义方法始终使用 system 上下文。因此,不会使用当前用户的凭据,并且任何具有访问权限的用户 这些方法可以使用其全部功能,而不管权限、字段级安全性或 共享规则。因此,使用关键字公开方法的开发人员应注意不要无意中暴露 任何敏感数据。webservicewebservice
警告
通过带有关键字的 API 公开的 Apex 类方法不强制执行对象权限,并且 默认情况下为字段级安全性。我们建议您使用适当的对象或 字段描述结果方法,用于检查当前用户对对象和字段的访问级别 Web Service 方法正在访问。请参阅 DescribeSObjectResult 类和 DescribeFieldResult 类。webservice
将关键字与 要作为 Web 服务的一部分公开的任何成员变量。不要标记这些 成员变量作为 。webservicestatic
调用 Apex SOAP Web 服务方法的注意事项:
Salesforce 拒绝对 Web 服务的访问,并拒绝来自具有受限访问权限的 AppExchange 包的请求。executeanonymous
使用 API 版本 15.0 及更高版本保存(编译)的 Apex 类和触发器会生成 如果为字段分配的 String 值太长,则运行时错误。
如果从API对密码过期或临时密码的用户进行登录调用, 不支持对自定义 Apex SOAP Web 服务方法的后续 API 调用,并导致 在INVALID_OPERATION_WITH_EXPIRED_PASSWORD错误中。重置用户的密码并制作 使用未过期的密码进行调用,以便能够调用 Apex Web 服务方法。
下面的示例演示一个包含 Web 服务成员变量和 Web 服务的类 方法:
global class SpecialAccounts {
global class AccountInfo {
webservice String AcctName;
webservice Integer AcctNumber;
}
webservice static Account createAccount(AccountInfo info) {
Account acct = new Account();
acct.Name = info.AcctName;
acct.AccountNumber = String.valueOf(info.AcctNumber);
insert acct;
return acct;
}
webservice static Id [] createAccounts(Account parent,
Account child, Account grandChild) {
insert parent;
child.parentId = parent.Id;
insert child;
grandChild.parentId = child.Id;
insert grandChild;
Id [] results = new Id[3];
results[0] = parent.Id;
results[1] = child.Id;
results[2] = grandChild.Id;
return results;
}
}
// Test class for the previous class.
@isTest
private class SpecialAccountsTest {
testMethod static void testAccountCreate() {
SpecialAccounts.AccountInfo info = new SpecialAccounts.AccountInfo();
info.AcctName = 'Manoj Cheenath';
info.AcctNumber = 12345;
Account acct = SpecialAccounts.createAccount(info);
System.assert(acct != null);
}
}
可以使用 AJAX 调用此 Web 服务。有关详细信息,请参阅 AJAX 中的 Apex。
重载 Web 服务方法
SOAP 和 WSDL 不能为重载方法提供良好的支持。因此,Apex 不会 允许两个标有关键字的方法 具有相同的名称。在同一类中具有相同名称的 Web 服务方法会生成一个 编译时错误。webservice
将 Apex 类公开为 REST Web 服务
您可以公开 Apex 类和方法,以便外部应用程序可以访问 通过REST架构的代码和应用程序。
本文概述了如何将 Apex 类公开为 REST Web 服务。您将了解到 关于类和方法批注,并参阅演示如何 实现此功能。
提示
Apex SOAP Web 服务允许外部应用程序调用 Apex 方法 通过 SOAP Web 服务。请参阅将 Apex 方法公开为 SOAP Web 服务。
如果从 API 为具有过期或临时用户的用户进行登录调用 password,则对自定义 Apex REST Web 服务方法的后续 API 调用不会 支持并导致MUTUAL_AUTHENTICATION_FAILED错误。重置用户的 密码并使用未过期的密码拨打电话,以便能够呼叫 Apex Web 服务方法。
@RestResource(urlMapping='/user_defined_type_example/*')
global with sharing class MyOwnTypeRestResource {
@HttpPost
global static MyUserDefinedClass echoMyType(MyUserDefinedClass ic) {
return ic;
}
global class MyUserDefinedClass {
global String string1;
global String string2 { get; set; }
private String privateString;
global transient String transientString;
global static String staticString;
}
}
此方法的有效 JSON 和 XML 请求数据如下所示:
{
"ic" : {
"string1" : "value for string1",
"string2" : "value for string2",
"privateString" : "value for privateString"
}
}
<request>
<ic>
<string1>value for string1</string1>
<string2>value for string2</string2>
<privateString>value for privateString</privateString>
</ic>
</request>
@RestResource(urlMapping='/CycleExample/*')
global with sharing class ApexRESTCycleExample {
@HttpGet
global static MyUserDef1 doCycleTest() {
MyUserDef1 def1 = new MyUserDef1();
MyUserDef2 def2 = new MyUserDef2();
def1.userDef2 = def2;
def2.userDef1 = def1;
return def1;
}
global class MyUserDef1 {
MyUserDef2 userDef2;
}
global class MyUserDef2 {
MyUserDef1 userDef1;
}
}