适用版本:EspoCRM 9.2.2+
外部集成的难点不是”调通 API”,而是”长期稳定运行”。
TL;DR
OAuth 2.0 实现 EspoCRM 与外部系统的安全连接
增量同步用 deltaLink/skipToken 机制,避免全量拉取
Token 失效有恢复策略,同步失败不中断服务
关键判断打英文日志,保证线上可定位
1. 集成场景概述 1.1 常见集成类型
集成类型
典型场景
同步方向
日历同步
Outlook/Google Calendar
双向
联系人同步
Outlook/Google Contacts
双向
邮件集成
SMTP/IMAP
单向(到 EspoCRM)
数据同步
ERP/财务系统
双向
Webhook
第三方通知
单向(到 EspoCRM)
1.2 日历同步的核心挑战
增量同步 :只拉取变更数据,避免全量拉取的性能开销
Token 管理 :访问令牌会过期,需要自动刷新
错误恢复 :网络中断、Token 失效后的恢复机制
数据一致性 :双向同步时的冲突处理
2. OAuth 认证流程 2.1 OAuth 2.0 基本流程 flowchart LR
A[用户点击连接] --> B[跳转到 Microsoft 授权页]
B --> C[用户同意授权]
C --> D[回调到 EspoCRM 带 code]
D --> E[用 code 换 access_token]
E --> F[保存 token 到 external_account]
F --> G[开始同步数据]
2.2 EspoCRM 中的 External Account 机制 EspoCRM 提供了 external_account 实体来存储第三方系统的认证信息:
1 2 3 4 5 6 7 8 { "id" : "xxx" , "userId" : "user_id" , "integration" : "Outlook" , "accessToken" : "<ACCESS_TOKEN>" , "refreshToken" : "<REFRESH_TOKEN>" , "expiresAt" : "2025-01-27 10:00:00" }
2.3 Token 刷新机制 1 2 3 4 5 6 7 8 if ($token ->isExpired ()) { $newToken = $this ->oauthHelper->refresh ($token ->getRefreshToken ()); $externalAccount ->set ('accessToken' , $newToken ->access_token); $externalAccount ->set ('refreshToken' , $newToken ->refresh_token); $externalAccount ->set ('expiresAt' , $newToken ->expires_at); $this ->entityManager->saveEntity ($externalAccount ); }
3. 增量同步实现 3.1 为什么需要增量同步 全量拉取的问题:
每次拉取所有事件,数据量大时性能差
浪费 API 配额
同步时间长,用户体验差
增量同步 :只拉取有变化的记录。
3.2 Microsoft Graph API 的 Delta 机制 stateDiagram-v2
[*] --> 初始同步
初始同步 --> 分页中: 收到@odata.nextLink
分页中 --> 分页中: 继续下一页
分页中 --> 增量同步: 收到@odata.deltaLink
增量同步 --> 增量同步: 使用deltaToken
增量同步 --> 错误恢复: Token过期
错误恢复 --> 初始同步: 重新开始
3.3 Delta Token 管理 响应格式 :
1 2 3 4 5 6 7 8 9 10 { "@odata.context" : "..." , "value" : [ { "id" : "AAMkAG..." , "subject" : "会议1" , "@removed" : { "reason" : "deleted" } } , { "id" : "AAMkAG..." , "subject" : "会议2" } ] , "@odata.nextLink" : "https://graph.microsoft.com/...?skipToken=xxx" , "@odata.deltaLink" : "https://graph.microsoft.com/...?deltaToken=xxx" }
存储策略 :
Token
存储位置
用途
deltaToken
outlook_calendar_user.delta_token
下次增量同步的起点
skipToken
outlook_calendar_user.skip_token
分页继续的标记
3.4 同步逻辑骨架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public function syncCalendar (OutlookCalendarUser $calendarUser ): void { $deltaToken = $calendarUser ->get ('deltaToken' ); $skipToken = $calendarUser ->get ('skipToken' ); $params = [ 'start' => new \DateTime ('-1 month' ), 'end' => new \DateTime ('+3 months' ), 'deltaToken' => $deltaToken , 'skipToken' => $skipToken , ]; do { $response = $this ->graphClient->getCalendarView ($params ); foreach ($response ->value as $item ) { if (isset ($item ['@removed' ])) { $this ->handleDeletedEvent ($item ); } else { $this ->handleEvent ($item ); } } if (isset ($response ['@odata.nextLink' ])) { $calendarUser ->set ('skipToken' , $this ->extractSkipToken ($response )); $params ['skipToken' ] = $calendarUser ->get ('skipToken' ); } elseif (isset ($response ['@odata.deltaLink' ])) { $calendarUser ->set ('deltaToken' , $this ->extractDeltaToken ($response )); $calendarUser ->set ('skipToken' , null ); break ; } } while (isset ($response ['@odata.nextLink' ])); $this ->entityManager->saveEntity ($calendarUser ); }
4. 错误处理与恢复 4.1 常见错误类型
错误
原因
恢复策略
401 Unauthorized
Token 过期
用 refreshToken 刷新
410 Gone
deltaToken 过期
重新开始全量同步
429 TooManyRequests
API 限流
延迟重试
500+ 服务器错误
临时故障
延迟重试
4.2 错误处理流程 flowchart TD
A[API 请求] --> B{响应状态}
B -->|成功| C[处理事件数据]
B -->|401| D[刷新 Token]
B -->|410| E[清除 Token]
D --> F[重试请求]
E --> G[重新开始全量同步]
B -->|429/500| H[延迟后重试]
4.3 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $response = $this ->graphClient->request ($url );if ($response ->getStatusCode () === 401 ) { $newToken = $this ->refreshAccessToken ($calendarUser ); if ($newToken ) { continue ; } else { $this ->log->error ('Token refresh failed' ); break ; } } if ($response ->getStatusCode () === 410 ) { $this ->log->warning ('Delta token expired, restarting sync' ); $calendarUser ->set ('deltaToken' , null ); $calendarUser ->set ('skipToken' , null ); $this ->syncCalendar ($calendarUser ); return ; }
5. 监控与排障 5.1 关键日志 1 2 3 4 5 [INFO] Outlook sync: Processing calendar {calendarName}. [INFO] Outlook sync: Received deltaToken. Sync completed. [INFO] Outlook sync: 3 events created, 1 updated, 2 deleted. [WARNING] Outlook sync: Sync state generation expired, restarting. [ERROR] Outlook sync: Failed to refresh token for user {userId}.
5.2 数据库验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SELECT u.name, cu.last_synced_at, cu.delta_token IS NOT NULL as has_delta FROM outlook_calendar_user cuJOIN user u ON cu.user_id = u.id;SELECT user_id, COUNT (* ) as total_events, MAX (synced_at) as last_sync FROM outlook_calendar_eventGROUP BY user_id;
5.3 同步配置参数 1 2 3 4 5 { "outlookCalendarSyncPeriod" : "1 year" , "outlookCalendarSyncEndPeriod" : "3 months" , "outlookCalendarSyncMaxPortionSize" : 100 }
参数
说明
syncPeriod
向前拉取多长时间的数据
syncEndPeriod
向后拉取多长时间的数据
maxPortionSize
每页返回多少条记录
本篇总结
OAuth 2.0 实现 EspoCRM 与外部系统的安全连接
增量同步用 deltaLink/skipToken 机制,避免全量拉取
Token 失效有恢复策略,同步失败不中断服务
关键节点打英文日志,保证线上可定位