Outlook&钉钉日程双向同步方案

1.背景   

    笔者目前所在的公司为合资公司,去年初,开始大面积推广使用钉钉,尤其是其在线会议功能,但是由于很多年的使用习惯,大家仍旧基于Outlook进行日程的预定及会议室的预占,但是Outlook日程和钉钉的日程打通一直是一个业界的大难题,很多头部企业都为此头痛,但是随着钉钉功能及其开放平台能力的逐步完善,及Microsoft Graph API的开放,日程同步不再变得那般不可实现。(当然很多人都设置了手机日历与钉钉以及Outlook日程的同步功能,但是这个功能好不好用,相信很多人都体会过,如果好用,也不会有下面的方案的出世)

      但是,由于钉钉和Outlook毕竟是两家头部互联网公司的产品,设计理念必然有着差异,要实现完全的同步目前来看是不可能的。前面说到的Microsoft Graph API前期在国内使用的是V21版本,功能的使用和国际班是有所区别的,目前随着微软北京数据中心的落地,此差异应该在逐步缩小;但是,笔者所在公司使用的是私有化部署的Exchange服务,所以并未使用Graph API的方案来实现同步,如果想寻求Graph API的解决方案,后面的内容请略过。

    正因为是私有化部署的Exchange服务,笔者所使用语言为java,所以ews-java-api几乎成为了必选,而EWS于2015年9月之后再无更新,笔者已验证,要达到完全的同步基本是不可能的,所以,如果采用此方案进行同步,对最后的同步结果要有心理预期,当然,如果是作为锦上添花之选的话,足够使用。通过实践,同步中所遇到的坑非常多,因此有了这篇博客,将一些难避的坑与大家共享。因当前程序还在完善阶段,此篇博文不会涉及到太多的代码细节。

2.解决方案

 2.1 同步方案

同步方案有三种,

 ①单向同步:从Outlook单向到钉钉

 ②单项同步:从钉钉同步到Outlook

 ③双向互写:最难实现的方案,判断失误很容易造成死循环。

此处笔者选择是的方案③,双向互写。

2.2 逻辑设计

① 从Outlook到钉钉

②从钉钉到Outlook

2.3 方案简述

2.3.1从Outlook到钉钉

        从Outlook到钉钉,如何获取到用户的日程面板的数据或者其变动呢?很多公司的解决方案采取的是通过设置一个能够读取邮箱系统的所有人的日程Outlook账户,然后通过定时轮询的方式,将差异批量同步至Outlook,这种方案短时间内会对邮箱服务器造成过大的压力,风险较大,而且延迟取决于同步的时间频率设置,体验较差,而且还因为此用户的权限,一旦该用户被盗取,很容易造成权限误用或数据泄露等风险。因此,此方案否决。

      笔者所采用的是通过ews的订阅功能,对每个用户的日程面板进行订阅,一旦发现变更,就将变更内容同步通过钉钉的API至钉钉。这部分逻辑相对较为容易实现,读者可以百度自行查阅。

2.3.2 从钉钉到Outlook

    钉钉日程面板的变动可以通过钉钉应用去订阅对应类型的消息即可实现,2023年4月,阿里增加Stream模式消息的订阅(之前是http的方式,更繁琐,可靠性不高),更大程度上简化了消息监听的逻辑,感谢阿里钉钉团队的辛勤付出,大家可以查阅官方文档进行配置。笔者写文时,地址如下:日程变更 - 钉钉开放平台icon-default.png?t=N7T8https://open.dingtalk.com/document/orgapp/event-calendar-event-change

既然变更数据已经获取到了,那么处理变更数据就想对较为简单了。通过ews可以实现日程的增删改查等操作。可以查阅百度自行实现。

3.难点(坑点)记录

3.1 Outlook日程代理用户的选项

  如果我们要对Outlook用户的日程面板进行同步,那么必然需要有一个Outlook账户能对用户的日程进行读取和编辑,那么可以采取以下三种模式:

  ①模拟模式(ApplicationImpersonation):超级用户,可以操作用户的所有权限

  ②代理模式(Delegate access):可以用户通过Outlook客户端授权,也可以通过Exchange管理员授权,权限相对较小。可以单独设置任务、邮箱、日程等方面的权限。

  ③日历权限设置:可以单独为用户设置日历权限,设置日历编辑者。风险控制较小。

无论是①还是②,均可能造成权限蔓延的情况,有一定的安全风险,因此均否决,因此,技术方案选择③,用户可以在Outlook的日历面板进行日历权限的设置,让某个用户拥有用户自身日历编辑的权限即可。

3.2 Outlook日程ID重复

      Outlook日程设计理念是以用户为中心,一个日程会在每个用户都建立一个日程的副本,每个副本对应的日程ID均不一样,但是也存在unionId的概念,也就是说一个日程的最底层的ID是一样的,可以通过ews中Appointment对象的getICalUid()方法来获取,后续都称之为UID,但是笔者在实践过程中居然发现UID有重复的现象,这是一个令人费解的问题,按道理,微软这种级别的公司应该不会出现这种错误。同时,Appointment对象也可以获取副本的ID,Appointment.getId()即可获取,同样,这个ID也存在着非常之高的重合率,这种重合率发生在同一个用户日程下,很多情况是刚取消了一个日程,然后重新在这个日程的时间点上新建日程,就很容易重复(不一定可以复现),我咨询过微软中国的资深专家,他对此百思不得其解,但因公司并未购买官方的服务,无法开case进行解决,最后不了了之。所以请慎用Appointment.getId()方法获取的ID来进行日程对象的获取,而应该使用UID结合会议组织者的方法进行日程的获取。同样,监听到日程的变动,其实获取到的日程对象一定是准确的,此时可以选择即时处理变更或者尽量准确的将所需要的信息拿出来存储到数据库中,而不是事后再通过ID去查询日程的信息。

3.3 历史数据

 针对历史数据,个人建议是不要做处理,而不是实现完全的同步,历史日程可以让用户手动修改触发通知后,就会实现同步。

3.4 Outlook订阅刷新

    Exchange服务器及EWS接口设置订阅的最长时间为30分钟,30分钟内必须重新开启订阅,否则订阅过期将无法监听日程的变动。同时,建立新的订阅的同时,需要取消老的订阅,释放资源的同时防止老的订阅持续通知,对新建订阅造成影响。此订阅不会自动延长,笔者经过多轮验证,并不会如同钉钉的Access Token一样自动延迟订阅时间;笔者目前采取的是20分钟批量性的新建新的订阅和取消旧的订阅。

3.5 设计理念差异

Outlook日程在会议参与者侧采取的是事件驱动机制,而且有强烈的隐私保护机制,用户的应答(接受、暂定、谢绝)及其通知模式(发送前编辑答复、立即发送答复、不发送答复)同步与否取决于参与者本身,会直接影响到日程的同步结果,用户存储的其实是日程的副本,而且可以这个副本可以不同步更改至主日程。而钉钉的设计理念是事实驱动,用户存取的是同一份数据,从而达到实时通信的效果

3.6 参与者组织差异

Outlook有通讯组的概念,可以直接添加通讯组如人事部作为参会人,通讯组对应的是单独的e-mail地址,而且存在层级关系,父子group会以父子对应的email地址进行存储;而钉钉虽然支持通过选择群聊或者组织进行日程的创建,但是实际上选择的仍是人员,底层数据存储的也是人员。因此,需要对Outlook通讯组进行查询分解,分解成单独的人员。

3.7 会议室预定差异

3.7.1 处理逻辑差异

Outlook会议室预定失败后,会议室对应的e-mail会发送预定失败的提醒,同时底层数据存储有会议室拒绝参会的记录。钉钉通过API预定会议室成功必须满足两个条件:①对会议室有操作预定的权限,②会议室空闲,二者缺一不可,否则将直接预定失败,在日程数据中不会存储预定失败的记录。而且钉钉的会议室预定是单独的接口,而ews操作Outlook是一个接口搞定,因此,此处注意使用分布式锁对日程操作进行锁定,否则容易引起多次的Outlook侧的更改。

3.7.2 跨天会议

 Outlook会议室支持24小事内的会议室预约,而钉钉不行,钉钉默认是开启早上7点到晚上23点的会议室预约权限(此权限可以在钉钉工作台的智能会议室中可以改为0点到24点),不支持跨天会议的预约,这个难点我和钉钉团队沟通过,也许是出于设计理念的差异,他们并无改动的意愿。但是,从使用中来讲,也几乎没有人会在Outlook中定个只有两天的会议,而且跨度只能是24小时之内。同时Outlook也好,钉钉也好,会议室只支持一个自然年内的循环预定,这倒是很一致。

3.7.3 全天会议

钉钉会议室不支持全天会议,全天会议还是很常见的,但是,可以将Outlook同步到钉钉的全天会议,做一下更改,将开始时间改为0点,技术时间改为24点,已验证通过,同时将钉钉全天会议的标志去除。当然,前提是已设置会议室可用是0点到24点。

3.7.4 会议闲忙查询

 钉钉会议室支持闲忙查询,但是要注意的是钉钉判断会议室闲忙处理的逻辑是开始时间取的是闭区间,什么意思呢?就是钉钉判断这段时间内有无会议的逻辑是 ≥开始时间,≤结束时间,如果你要约9点到10点的会,刚好这个会议室有个8点到9点的会议,你如果直接传2024-01-19 9:00:00作为开始时间去查询,那么不好意思,会议室是忙的,哈哈。我沟通过钉钉团队,但是钉钉团队并不愿意改,毕竟会涉及到开发工作量呀,那就我们自己改吧,把时间查询外后挪一秒,变成2024-01-19 9:00:01,搞定。

3.8 循环日程

3.8.1 年循环日程

 ews当前并不支持间隔n年的循环日程设定,当然现实生活中,也几乎没有人会定个例如两年一循环的日程,如果有,那这人挺神。

3.8.2 子日程同步 - Outlook到钉钉

 子日程同步是个老大难问题,Outlook的子日程的获取要通过下标来获取,其子日程变动时候是以主日程的方式来通知,然后在对象的修改对象和删除对象中来彰显谁改了,所以这里的处理挺恶心的,设想哈,一个日程半年前建的,中间有很多的子日程都发生了变化,这个时候你的程序上线了,来个变动通知,那前面的日程是同步呢还是不同步呢?个人建议就同步近期的吧,Appointment.getLastModifiedTime()可以获取到最后的修改时间,以此来定位即可。从Outlook的子日程同步到钉钉的子日程难点在于如何获取钉钉的子日程的下标,其实细心的开发者就可以发现,钉钉的子日程不就是子日程开始时间的下标么?那问题就解决了,当然如果哪天钉钉改规则了,那就没辙了。这里取的是Appointment.getOriginalStart(),不是getStart哈,这是因为钉钉的日程子日程其实只新建到了当前日期,后面的子日程在数据库中并没有实际新建,而且,Outlook的子日程是可以改开始时间的,只有原始开始时间才能对应上。

3.8.3 子日程同步 - 钉钉到Outlook

  前面说了,Outlook子日程要通过下标获取,下标从0快开始,既然我们都得知了钉钉子日程的id的规律,那就简单了,直接拿着钉钉的子日程的尾缀反推Outlook子日程的下标即可。

3.8.4 循环次数限定

大家看钉钉开放平台的官方文档,日程次数其实最开始是99次,但是如果是每日循环,一年的日程呢,那是不是就不可以同步了?

答案是否定的,这里其实是365次,笔者通过和钉钉团队沟通,此数据已经改为365次,但是不知道为什么官方并没有及时更新文档。

3.9 富文本传输

钉钉不支持富文本的传输,但是Outlook支持呀,这点得感谢阿里钉钉团队,这也是他们在宁王实践的结果,公开出来了,这格局,杠杠的。但是,一旦使用此字段,原来的描述字段就作废了。而且在钉钉中展示的是一张图片,无法更改,大家可以试试。

3.10 时区设置

从钉钉创建循环日程同步至outlook中时,如果不设置时区参数将导致时区显示UTC 西零区(蒙罗维亚),而ews-java-api2.0中提供的appointment.setStartTimeZone方法其参数TimeZoneDefinition对象无法通过TimeZoneDefinition.setId(“China Standard Time”)来获取,需要通过service.getServerTimeZones()得到所有的时区列表再对比其ID来获取,而此方法因加载时长较长,因此提前进行单例初始化处理。

3.11 邮件通知

  通过ews接口操作Outlook日程会触发两封邮件的发送,一封来自日历权限汇总设定的有编辑权限的代理用户(XXXXX代表小明,会议转发通知……),一封来自Exchange服务器(Microsoft Outlook代表XXXX,会议转发通知……),是真烦呀,我试过各种方案来不发送邮件,包括禁用XXXX的发送权限,无效,辛苦Exchange管理员配合了半天,如果大家有好的办法,可以私信我,不胜感激。当然,这个在有的企业还是很有用的,毕竟用于合规嘛。

3.12 日程更新模式

不论是主日程还是更改子日程,需要是用的模式是SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy,否则将导致会议室收不到变更,从而导致会议室的不同步,那么问题来了,如果有个上百人参与的会议,然后钉钉里面不断有人拉其他人进来,或者拒绝会议,然后呢,如果都使用SendToAllAndSaveCopy,Outlook会干嘛呢,他会认为日程变动了,然后把变动发送给每参与者,哈哈,那就有意思了,会产生巨量的垃圾邮件,肯定有人找你麻烦(踩过坑呀,都是泪)。所以呢,如何处理呢?我建议的逻辑是,判断如果是只有参与者变动的话,通知模式使用SendInvitationsOrCancellationsMode.SendToChangedAndSaveCopy,这样就只有新增或者删除的参会者会收到邮件了。

3.13 日程会议更新或者修改

       基于ews对日程的更新或者修改很容易发生The operation can't be performed because the item is out of date. Reload the item and try again,这是什么问题呢?其实这与钉钉的处理逻辑有关系,在钉钉取消或者修改日程是通常会发送多条updated或者canceled的消息,而你获取到的Appointment对象不一定是最新的,所以,在处理逻辑时候一定要加锁呀。尤其是子日程的修改,钉钉偶尔会抽风,同时发送n条子日程的变更,但是Outlook子日程的变更需要通过主日程来进行修改,如果不加锁,也会报上述异常。

3.14 钉钉的偶尔抽风

   钉钉偶尔会大批量的发送变更消息,结果一看,某些人离职,他所属的日程全部在短时间内发送到订阅方,n年前的也会,我的处理方式比较暴力,直接拦截了。

4.总结

笔者只点出了其中出现的较为频繁的问题,中间还有n多坑需要解决,例如500人以上的会议如何处理等等。但是篇幅有限,暂且陈述到此,希望后来者可以少走一些坑。因版权问题,代码无法开源。总之,日程同步就是个悖论,终极解决之道那就是干掉其中一方,但是目前来看,在合资公司是比较难办的一件事情。以上见解并不一定最优或者正确,不到之处还请多多指正!

5.群聊沟通

应童鞋们要求,建群供交流,QQ群号:341386140

  • 32
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vol火山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值