您可以构建连接器来集成 Google Cloud 与客户关系管理 (CRM) 系统(例如 Jira Service Desk、Zendesk 和 ServiceNow)之间的支持请求。
此连接器使用 Customer Care 的 Cloud Support API (CSAPI)。本文档举例说明了如何构建和使用连接器。您可以根据应用场景调整设计。
假设条件
我们会对您的 CRM 系统的工作原理以及编写连接器所用的语言做出一些重要假设。如果您的 CRM 具有不同的功能,您仍然可以构建完全正常运行的连接器,但您可能需要采用与本指南不同的方式实现连接器。
本指南基于以下假设:
- 您正在使用 Python 和 Flask 微框架构建连接器。
- 我们假定您使用 Flask,是因为它是一个可用于构建小型应用的简单框架。您也可以使用其他语言或框架,例如 Java。
- 您想要同步附件、评论、优先级、支持请求元数据和支持请求状态。除非您愿意,否则无需同步所有数据。例如,如果您不想同步附件,就不要同步附件。
- 您的 CRM 会提供端点,这些端点能够读取和写入您要同步的字段。如果您想按照本指南中的说明同步每个字段,请确保您的 CRM 端点支持以下操作:
操作 CSAPI 等效值 使用一些不变的静态 ID 获取支持请求。 cases.get
创建支持请求。 cases.create
了结支持请求。 cases.close
更新支持请求的优先级。 cases.patch
列出支持请求的附件。 cases.attachments.list
下载支持请求的附件。 media.download
向支持请求上传附件。 media.upload
列出支持请求的评论。 cases.comments.list
为支持请求添加新评论。 cases.comments.create
搜索支持请求*。 cases.search
*您必须能按上次更新时间过滤。还必须通过某种方法确定哪些支持请求要同步到客户服务。例如,如果 CRM 中的支持请求可以有自定义字段,您可以填充名为 synchronizeWithGoogleCloudSupport
的自定义布尔值字段,并根据该字段进行过滤。
概要设计
此连接器完全使用 Google Cloud 产品和您的 CRM 系统构建。这是一个在 Flask 微框架中运行 Python 的 App Engine 应用。它使用 Cloud Tasks 定期轮询 CSAPI 和 CRM,以查看是否有新案例和现有案例的更新,并在它们之间同步更改。有关支持请求的一些元数据存储在 Firestore 中,但在不再需要时,系统会将其删除。
下图简要展示了设计:
连接器目标
连接器的主要目标是,在您的 CRM 系统中创建您要同步的支持请求时,会在客户服务中创建相应的支持请求,并且支持请求的所有后续更新会在这些支持请求之间同步。同样,在客户服务中创建支持请求后,该支持请求必须同步到您的 CRM 系统中。
具体来说,必须在以下方面同步支持请求:
- 支持请求创建:
- 如果在某个系统中创建支持请求,连接器必须在另一个系统中创建对应的支持请求。
- 如果某个系统不可用,则一旦该系统可用,就必须在其中创建支持请求。
- 备注:
- 在某个系统中的案例中添加评论时,必须将其添加到另一个系统中的相应案例。
- 附件:
- 将某个附件添加到一个系统中的案例时,必须将其添加到另一个系统中的相应案例。
- 优先级:
- 当一个系统中案例的优先级更新时,必须更新另一个系统中相应案例的优先级。
- 支持请求状态:
- 如果某个支持请求在一个系统中关闭,则必须在另一个系统中也关闭。
基础架构
Google Cloud 产品
连接器是一个 App Engine 应用,它使用以 Datastore 模式配置的 Cloud Firestore,用于存储已同步案例的数据。它使用 Cloud Tasks 通过自动重试逻辑安排作业。
要访问 Customer Care 中的支持请求,连接器会使用服务帐号来调用 V2 Cloud Support API。您需要向服务帐号授予适当的权限,以进行身份验证。
您的 CRM
连接器使用您提供的机制访问 CRM 中的案例。可能会通过调用 CRM 提供的 API 来实现。
贵组织的安全注意事项
连接器会同步您构建连接器的组织和组织的所有子项目中的案例。这样一来,该组织内的用户可能就可以访问您可能不希望他们访问的客户服务数据。请仔细考虑如何设计 IAM 角色的结构,以维护组织的安全性。
详细设计
设置 CSAPI
如需设置 CSAPI,请按以下步骤操作:
- 为贵组织购买 Cloud Customer Care 支持服务。
- 对于要在其中运行连接器的项目,请启用 Cloud Support API。
- 获取要由连接器使用的默认 Apps 框架服务帐号的凭据。
- 在组织级层向服务帐号授予以下角色:
Tech Support Editor
Organization Viewer
如需详细了解如何设置 CSAPI,请参阅 Cloud Support API V2 用户指南。
调用 CSAPI
我们使用 Python 来调用 CSAPI。我们建议使用以下两种方法使用 Python 调用 CSAPI:
- 通过其 proto 生成的客户端库。这些 API 更加现代和惯用,但不支持调用 CSAPI 附件端点。如需了解详情,请参阅 GAPIC 生成器。
- 根据其发现文档生成的客户端库。这类附件较旧,但支持附件。如需了解详情,请参阅 Google API 客户端。
以下示例展示了如何使用通过发现文档创建的客户端库来调用 CSAPI:
"""
Gets a support case using the Cloud Support API.
Before running, do the following:
- Set the GOOGLE_APPLICATION_CREDENTIALS environment variable to
your service account credentials.
- Install the Google API Python Client: https://github.com/googleapis/google-api-python-client
- Change NAME to point to a case that your service account has permission to get.
"""
import os
import googleapiclient.discovery
NAME = "projects/some-project/cases/43595344"
def main():
api_version = "v2"
supportApiService = googleapiclient.discovery.build(
serviceName="cloudsupport",
version=api_version,
discoveryServiceUrl=f"https://cloudsupport.googleapis.com/$discovery/rest?version={api_version}",
)
request = supportApiService.cases().get(
name=NAME,
)
print(request.execute())
if __name__ == "__main__":
main()
如需查看调用 CSAPI 的更多示例,请参阅此代码库。
Google 资源名称、ID 和编号
让 organization_id
成为贵组织的 ID。您可以在组织的客户服务中创建支持请求,也可以在组织内的项目下创建支持请求。将 project_id
作为您可能在其中创建案例的项目的名称。
支持请求名称
支持请求的名称如下所示:
organizations/{organization_id}/cases/{case_number}
projects/{project_id}/cases/{case_number}
其中 case_number
是分配给案例的编号。例如 51234456
。
评论名称
评论的名称如下所示:
organizations/{organization_id}/cases/{case_number}/comments/{comment_id}
其中 comment_id
是分配给评论的数字。例如 3
。此外,除了组织之外,项目还可以是父级。
附件名称
附件的名称如下所示:
organizations/{organization_id}/cases/{case_number}/attachments/{attachment_id}
其中,attachment_id
是案例中附件的 ID(如果有)。例如 0684M00000JvBpnQAF
。此外,除了组织之外,项目还可以是父级。
Firestore 实体
CaseMapping
CaseMapping
是定义为存储案例元数据的对象。
系统会为每个已同步的支持请求创建,并在不再需要该支持请求时将其删除。如需详细了解 Firebase 支持的数据类型,请参阅支持的数据类型。
CaseMapping
具有以下属性:
属性 | 说明 | 类型 | 示例 |
---|---|---|---|
ID |
主键。创建 CaseMapping 时由 Firestore 自动分配。 |
整数 | 123456789 |
googleCaseName |
支持请求的全名,其中包含组织或项目 ID 及支持请求编号。 | 文本字符串 | organizations/123/cases/456 |
companyCaseID |
您的 CRM 系统中的支持请求 ID。 | 整数 | 789 |
newContentAt |
上次在 Google 支持请求或您的 CRM 支持请求中检测到新内容的时间。 | 日期和时间 | 0001-01-01T00:00:00Z |
resolvedAt |
表示 Google 支持请求解决时间的时间戳。用于在不再需要 CaseMappings 时将其删除。 |
日期和时间 | 0001-01-01T00:00:00Z |
companyUpdatesSyncedAt |
连接器上次成功轮询您的 CRM 以获取更新并将所有更新同步到 Google 支持请求的时间戳。用于检测服务中断。 | 日期和时间 | 0001-01-01T00:00:00Z |
googleUpdatesSyncedAt |
连接器上次成功向 Google 轮询更新并将任何更新同步到您的 CRM 案例的时间戳。用于检测服务中断。 | 日期和时间 | 0001-01-01T00:00:00Z |
outageCommentSentToGoogle |
在检测到服务中断时,是否向 Google 支持请求添加了评论。用于防止添加多个服务中断备注。 | 布尔值 | False |
outageCommentSentToCompany |
如果检测到服务中断,请说明您的 CRM 案例中是否添加了评论。用于防止添加多个服务中断备注。 | 布尔值 | False |
priority |
案例的优先级。 | 整数 | 2 |
Global
Global
是用于存储连接器全局变量的对象。
系统只会创建一个 Global
对象,并且该对象永远不会被删除。该架构如下所示:
属性 | 说明 | 类型 | 示例 |
---|---|---|---|
ID | 主键。创建此对象时由 Firestore 自动分配。 | 整数 | 123456789 |
google_last_polled_at |
上次对客户服务进行轮询以更新信息的时间。 | 日期和时间 | 0001-01-01T00:00:00Z |
company_last_polled_at |
最近一次调查贵公司以了解最新动态的时间。 | 日期和时间 | 0001-01-01T00:00:00Z |
Tasks
PollGoogleForUpdates
This task is scheduled to run every 60 seconds. It does the following:
- Search for recently updated cases:
- Call
CSAPI.SearchCases(organization_id, page_size=100, filter="update_time>{Global.google_last_polled_at - GOOGLE_POLLING_WINDOW}")
- Continue fetching pages as long as a
nextPageToken
is returned.
GOOGLE_POLLING_WINDOW
represents the period during which a case is continually checked for updates, even after it has been synced. The larger its value, the more tolerant the connector is to changes that are added while a case is syncing. We recommend that you set GOOGLE_POLLING_WINDOW
to 30 minutes to avoid any problems with comments being added out of order.
- Make a
CaseMapping
for any new cases:
- If
CaseMapping
does not exist for case.name
and case.create_time
is less than 30 days ago, then create a CaseMapping
with the following values:
Property
Value
caseMappingID
N/A
googleCaseName
case.name
companyCaseID
null
newContentAt
current_time
resolvedAt
null
companyUpdatesSyncedAt
current_time
googleUpdatesSyncedAt
null
outageCommentSentToGoogle
False
outageCommentSentToCompany
False
priority
case.priority
(converted to an integer)
- Queue tasks to sync all recently updated cases:
- Specifically,
SyncGoogleCaseToCompany(case.name)
.
- Update
CaseMappings
:
- For each open
CaseMapping
not recently updated, update CaseMapping.googleUpdatesSyncedAt
to the current time.
- Update last polled time:
- Update
Global.google_last_polled_at
in Firestore to the current time.
Retry logic
Configure this task to retry a few times within the first minute and then expire.
PollCompanyForUpdates
This task is scheduled to run every 60 seconds. It does the following:
- Search for recently updated cases:
- Call
YOUR_CRM.SearchCases(page_size=100, filter=”update_time>{Global.company_last_polled_at - COMPANY_POLLING_WINDOW} AND synchronizeWithGoogleCloudSupport=true”)
.
COMPANY_POLLING_WINDOW
can be set to whatever time duration works for you. For example, 5 minutes.
- Make a
CaseMapping
for any new cases:
- For each case, if
CaseMapping
does not exist for case.id
and case.create_time
is less than 30 days ago, create a CaseMapping
that looks like this:
Property
Value
caseMappingID
N/A
googleCaseName
null
companyCaseID
case.id
newContentAt
current_time
resolvedAt
null
companyUpdatesSyncedAt
null
googleUpdatesSyncedAt
current_time
outageCommentSentToGoogle
False
outageCommentSentToCompany
False
priority
case.priority
(converted to an integer)
- Queue tasks to sync all recently updated cases:
- Specifically, queue
SyncCompanyCaseToGoogle(case.name)
.
- Update
CaseMappings
:
- For each open
CaseMapping
not recently updated, update CaseMapping.companyUpdatesSyncedAt
to the current time.
- Update last polled time:
- Update
Global.company_last_polled_at
in Firestore to the current time.
Retry logic
Configure this task to retry a few times within the first minute and then expire.
SyncGoogleUpdatesToCompany(case_name)
Implementation
- Get the case and case mapping:
- Get
CaseMapping
for case_name
.
- Call
CSAPI.GetCase(case_name)
.
- If necessary, update resolved time and case status:
- If
CaseMapping.resolvedAt == null
and case.status == CLOSED
:
- Set
CaseMapping.resolvedAt
to case.update_time
- Close the case in the CRM as well
- Try to connect to an existing case in the CRM. If unable, then make a new one:
- If
CaseMapping.companyCaseID == null
:
- Try to get your CRM case with
custom_field_google_name == case_name
custom_field_google_name
is a custom field you create on the case object in your CRM.
- If the CRM case can't be found, call
YOUR_CRM.CreateCase(case)
with the following case:
Case field name in your CRM
Value
Summary
Case.diplay_name
Priority
Case.priority
Description
"CONTENT MIRRORED FROM GOOGLE SUPPORT:\n" + Case.description
Components
"Google Cloud
"
Customer Ticket (custom_field_google_name
)
case_name
Attachments
N/A
- Update the
CaseMapping
with a CaseMapping
that looks like this:
Property
Value
companyCaseID
new_case.id
googleUpdatesSyncedAt
current_time
- Add comment to Google case: "This case is now syncing with Company Case:
{case_id}
".
- Synchronize the comments:
- Get all comments:
- Call
CSAPI.ListComments(case_name, page_size=100)
. The maximum page size is 100. Continue retrieving successive pages until the oldest comment retrieved is older than googleUpdatesSyncedAt - GOOGLE_POLLING_WINDOW
.
- Call
YOUR_CRM.GetComments(case_id, page_size=50)
. Continue retrieving successive pages until the oldest comment retrieved is older than companyUpdatesSyncedAt - COMPANY_POLLING_WINDOW
.
- Optional: If you'd like, consider caching comments in some way so you can avoid making extra calls here. We leave the implementation of that up to you.
- Compare both lists of comments to determine if there are new comments on the Google Case.
- For each new Google comment:
- Call
YOUR_CRM.AddComment(comment.body)
, starting with "[Google Comment {comment_id}
by {comment.actor.display_name}
]".
- Repeat for attachments.
- Update
CaseMapping.googleUpdatesSyncedAt
to the current time.
Retry logic
Configure this task to retry indefinitely with exponential backoff.
SyncCompanyUpdatesToGoogle(case_id)
Implementation:
- Get the case and case mapping.
- Get
CaseMapping
for case.id
.
- Call
YOUR_CRM.GetCase(case.id)
.
- If necessary, update resolved time and case status:
- If
CaseMapping.resolvedAt == null
and case.status == CLOSED
:
- Set
CaseMapping.resolvedAt
to case.update_time
- Close the case in CSAPI as well
- Try to connect to an existing case in CSAPI. If unable, then make a new one:
- If
CaseMapping.googleCaseName == null
:
- Search through cases in CSAPI. Try to find a case that has a comment containing “This case is now syncing with Company Case:
{case_id}
”. If you're able to find one, then set googleCaseName
equal to its name.
- Otherwise, call
CSAPI.CreateCase(case)
:
- Synchronize the comments.
- Get all comments for the case from CSAPI and the CRM:
- Call
CSAPI.ListComments(case_name, page_size=100)
. Continue retrieving successive pages until the oldest comment retrieved is older than googleUpdatesSyncedAt - GOOGLE_POLLING_WINDOW
.
- Call
YOUR_CRM.GetComments(case_id, page_size=50)
. Continue retrieving successive pages until the oldest comment retrieved is older than companyUpdatesSyncedAt - COMPANY_POLLING_WINDOW
.
- NOTE: If you'd like, consider caching comments in some way so you can avoid making extra calls here. We leave the implementation of that up to you.
- Compare both lists of comments to determine if there are new comments on the CRM case.
- For each new Company comment:
- Call
CSAPI.AddComment
, starting with "[Company Comment {comment.id}
by {comment.author.displayName}
]".
- Repeat for attachments.
- Update
CaseMapping.companyUpdatesSyncedAt
to the current time.
Retry logic
Configure this task to retry indefinitely with exponential backoff.
CleanUpCaseMappings
This task is scheduled to run daily. It deletes any CaseMapping
for a case that has been closed for 30 days according to resolvedAt
.
Retry logic
Configure this task to retry with exponential backoff for up to 24 hours.
DetectOutages
This task is scheduled to run once every 5 minutes. It detects outages and alerts your Google and CRM cases (when possible) if a case is not syncing within the expected latency_tolerance
.
latency_tolerance
is defined as follows, where Time Since New Content = currentTime - newContentAt
:
Priority
Fresh (<1 hour)
Default (1 hour-1day)
Stale (>1 day)
P0
10 min
10 min
15 min
P1
10 min
15 min
60 min
P2
10 min
20 min
120 min
P3
10 min
20 min
240 min
P4
10 min
30 min
240 min
The latency that is relevant for the connector is not request latency, but rather the latency between when a change is made in one system and when it is propagated to the other. We make latency_tolerance
dependent on priority and freshness to avoid spamming cases unnecessarily. If there is a short outage, such as scheduled maintenance on either system, we don't need to alert P4
cases that haven't been updated recently.
When DetectOutages
runs, it does the following:
- Determine if a
CaseMapping
needs an outage comment, whereupon it adds one:
- For each
CaseMapping
in Firestore:
- If recently added (
companyCaseId
or googleUpdatesSyncedAt
is not defined), then ignore.
- If
current_time > googleUpdatesSyncedAt + latency_tolerance OR current_time > companyUpdatesSyncedAt + latency_tolerance
:
- If
!outageCommentSentToGoogle
:
- Try:
- Add comment to Google that "This case has not synced properly in
{duration since sync}
."
- Set
outageCommentSentToGoogle = True
.
- If
!outageCommentSentToCompany
:
- Try:
- Add comment to your CRM that "This case has not synced properly in
{duration since sync}
."
- Set
outageCommentSentToCompany = True
.
- Else:
- If
outageCommentSentToGoogle
:
- Try:
- Add comment to Google that "Syncing has resumed."
- Set
outageCommentSentToGoogle = False
.
- If
outageCommentSentToCompany
:
- Try:
- Add comment to your CRM that "Syncing has resumed."
- Set
outageCommentSentToCompany = False
.
- Return a failing status code (4xx or 5xx) if an outage is detected. This causes any monitoring you've set up to notice that there is a problem with the task.
Retry logic
Configure this task to retry a few times within the first 5 minutes and then expire.
What's next
Your connector is now ready to use.
If you'd like, you can also implement unit tests and integration tests. Also, you can add monitoring to check that the connector is working correctly on an ongoing basis.