注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

创业关闭大潮,它潜伏修炼

创业关闭大潮,它潜伏修炼    在去年通过朋友圈的推荐,下载了一款产品摘客,一开始被他们的独特的分类阅读所吸引,虽然产品当时还有很多的问题,但是对于用户来说,其独特的细分个性化阅读有别于其他APP,当时的体验还是不错的。还有其搜索功能,当时发现搜索时,很多...
继续阅读 »
创业关闭大潮,它潜伏修炼 
 
在去年通过朋友圈的推荐,下载了一款产品摘客,一开始被他们的独特的分类阅读所吸引,虽然产品当时还有很多的问题,但是对于用户来说,其独特的细分个性化阅读有别于其他APP,当时的体验还是不错的。还有其搜索功能,当时发现搜索时,很多时候都能意外的发现很多好文章,因此建立了该产品的第一映像。 
 
后来机缘巧合,认识到这款产品的团队负责人,因此也特地对摘客与其进行了相关的交流和探讨。 
 
惊讶的是整个团队不过10+人,就包括了后台、前端、测试、运营等人员,原本以为主打个性化推荐算法的产品,相对而言对产品的复杂度和门槛会高一点点,应该有几十人的团队在维护,如今日头条,虽然人家已经上千人的团队,但起步的时候也远远不止10人的团队。不过所有创业团队在起步的时候一定是精简的,一人能敌好几个人用。光这一点,对摘客又多了几分好感。 
 
当然,我也从我的角度指出了很多产品存在的问题,比如界面相较于其他和产品定位而言,UI风格的混乱和不统一;用户评论机制的鸡肋和打造UGC社区氛围问题;产品人群定位等问题都进行了探讨;
 
就个性化推荐而言,摘客切入的是互联网资讯垂直行业,相较于目前已经做得较好的科技媒体,像36氪、虎嗅、极客公园,以及同样主打个性推荐的今日头像,摘客还是有明显的差异性,找到了细分领域。而就阅读而言,用户是否真的需要这么细分的需求呢?这是产品值得思考的问题。 
 
摘客负责人告诉我们,已将上线的2.0.0版本会对目前的产品有一个较大的改进和提高,尤其是目前被人诟病的界面,因此当下就去下载更新了,大改版的白蓝为主色的界面,真实小清晰一脸啊,侧拉导航改为底部导航,也大大比旧版提高了用户的交互体验; 
 


图片1.png




图片2.png



通过深入的了解后,整个团队给人的感觉相较一般的创业团队,更耐得住性子,沉下心来打磨一件他们认为有价值的产品。相比那些为了拿钱融资,冲业务指标而放弃初心的很多创业团队,更欣赏现在还可以静心做产品的团队。 
 
值得恭喜的是,他们告诉我摘客已经在融资阶段,并且有了明确性的资金意向,具体的消息相信在不久的将来可以听到。 收起阅读 »

【猿团专访】|刘俊彦:PaaS、SaaS齐头并进 环信领军企业服务市场

刘俊彦,毕业于伦敦大学国王学院,计算机硕士,是一个有17年研发经验的老程序员。在创办环信之前,先后任职IONA,RedHat等跨国公司的研发中心,主要专注在高并发消息中间件、实时消息系统、异构分布式企业系统集成和应用服务器。同时,刘俊彦本人还是开源软件的重度倡...
继续阅读 »
刘俊彦,毕业于伦敦大学国王学院,计算机硕士,是一个有17年研发经验的老程序员。在创办环信之前,先后任职IONA,RedHat等跨国公司的研发中心,主要专注在高并发消息中间件、实时消息系统、异构分布式企业系统集成和应用服务器。同时,刘俊彦本人还是开源软件的重度倡导者,是Apache CXF、JBOSS Drools、JBPM、JBOSS ESB、SOA-P等多个世界知名开源软件项目的核心代码贡献者。


4ea62a46ff9f4512fcbdc14bfdf7fcc3.jpg


作为环信CEO,刘俊彦认为,要保持行业竞争力,最重要的是远见、视野以及执行力。这个目光如炬的男人,用他独有的市场洞察力,带领环信成为国内最大的即时通讯云平台及国内最大的SaaS客服平台。本期猿团专访,笔者将带着大家一起来了解环信背后的故事。

以下为猿团记者专访内容,原创作品,如需转载请注明出处。
 
回望:创业伊始 环信面临招聘难

和所有创业公司一样,环信最初的创立也是艰难重重,最大的挑战还是人才的招聘。刘俊彦告诉笔者,环信是从中关村创业大街上的车库咖啡起家的。当时,几位创始人带着几位初出茅庐的年轻程序员,在一个咖啡桌边一坐就是一年。由于办公环境简陋,因此招聘特别困难。每次打电话过去,候选人一听说办公地点是在一个咖啡厅,就狐疑丛生,不是当场拒绝,就是约好的面试频频被放鸽子。幸运的是,襁褓中的环信,凝聚了一批坚定的创业者,怀揣着用技术改变世界的理想,团队在一点点的壮大成熟。更为幸运的是,当时那条咖啡桌边的初出茅庐的年轻程序员们,没有一位离开环信,而且全部都成长成熟起来,在各自的岗位上发挥着中坚作用。

如今的环信,团队规模已有数百人,早已搬进高大上的办公楼,但环信“团结最优秀的人,一起用技术改变世界”的初心一直不曾改变。谈到这里,刘俊彦表示:”最近经常有人问,’现在加入环信会不会晚了点,还有没有足够好的位置留给我?’。我想说的是,不管是创业还是加入一家公司,选择永远大于努力。如果有一张登上火箭飞船的票,你不需要问这是几等舱。”

两大产品线齐头并进 环信领军通讯市场

虽然前期遭遇了种种困难,但在团队的努力下,目前环信即时通讯云目前已经是国内最大的即时通讯云平台,共服务了50833家 App 客户,SDK覆盖手机终端3.19亿台,平台日均发送消息2.1亿条。而环信移动客服也已经成为国内最大的SaaS客服平台,覆盖了电商、O2O、金融保险、教育等多个行业的众多标杆客户,典型用户包括国美在线,58到家,神州专车,金融界,学而思等。环信初步完成了对连接人与人和连接人与商业的两个核心场景的全覆盖。

91331a0a0ef3ef37cac2f69b29921efc.jpg


笔者了解到,目前环信有两条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。

为什么会有两条产品线呢?刘俊彦表示,在环信看来,IM天然就有2种场景。第一种是使用IM让人和人之间沟通。这个是做社交,也就是连接人与人。不管是微信、陌陌、还是环信即时通讯云平台上的6万多家APP,他们做的都是在连接人与人。在这方面,环信即时通讯云希望帮助开发者和企业更好的连接人与人,做即时通讯云领导者。

IM的第二种场景就是使用IM让人和商家之间沟通。这个就是连接人与商业。不管是淘宝旺旺,还是微信公共账号,其本质都是用IM的方式让人(消费者)和商家更好的沟通和服务。环信移动客服的愿景就是更好的连接人与商业,领军移动客服。这个产品的主要服务对象是各行各业的企业和商家。只要商家有和消费者沟通的需求,有服务消费者的需求,就可以使用环信移动客服。

1bc87932d2c90428fe3da6507ccfa32b.jpg


好PaaS产生好SaaS 环信优势明显

环信是国内最早提供即时通讯云服务的企业,技术先发优势和获客先发优势非常明显,现在主流的大型APP市场基本都被环信垄断。在很多用户使用完环信的PaaS服务后,越来越多的涌现出客服使用的需求。而互联网的发展,也的确将客服服务从PC端转移到移动端,移动端客服成为整个客服软件市场的核心战场,谁打赢了移动端客服的战斗,谁就打赢了整个客服软件的战斗。这一点的发现,让刘俊彦坚持要把移动客服做下去。

说到环信移动客服这条产品线的开发,刘俊彦回想起当时的情景:“在最开始,我们完全没有想过要做环信移动客服这么一个SaaS形态的客服产品。甚至当我决定要做这个产品的时候,连股东内部都有不同的声音。但我顶住压力,不但做了这个产品,而且是‘all in’地去做。后来,环信移动客服一经推出,就获得了巨大成功,这也证明当初的决定是多么正确。“

根据易观国际的研究报告,环信移动客服在移动端SaaS客服市场的占有率高达77.4%。对于移动客服取得如此的成功,刘俊彦并不惊讶,因为在他看来,移动端客服软件最根本的核心技术就是IM技术。没有IM技术,就没法做移动端客服。环信作为中国最大的即时通讯云厂商,在IM技术的领先优势可想而知。这一优势直接造就了环信移动客服目前的市场地位。

目前环信是众多SaaS客服厂商中唯一一个拥有自主成熟IM技术的厂商,拥有专为移动互联网和手机终端深度优化的的私有通讯协议,同时,电信级高可靠、高并发的即时通讯公有云平台支撑移动客服的稳定性,经过实际验证,可支持亿级用户同时在线。“好PaaS必定产生好SaaS ”刘俊彦如是说。

加大智能投入 环信引领客户服务行业进入人工智能时代

和其他友商不同,环信极其重视在人工智能和大数据领域的投入。环信坚信,人工智能技术将在2年内彻底改变客户服务软件行业。环信很早就建立了人工智能和大数据研究院,数十名业界顶尖的科学家和工程师在人工智能与客户服务结合的这一新兴领域做了大量世界前沿的研究和产品化工作,尽管成本巨大,但刘俊彦认为非常值得。目前,环信已经推出了环信机器人,环信客户声音,环信智能质检,环信社交大数据等多个产品。使得环信在产品布局和产品能力上一直保持行业领先地位。

当被问及环信未来还有何发展时,刘俊彦淡淡一笑说:“当每天有几亿条消息在你的系统里流过,当每天有几百个客户,同时也是这个时代的一批最优秀的创业者向你提出他们最期待的新功能需求时,你已经‘被’站在了时代的风口浪尖,占据了制高点。只要你善于倾听,不畏惧改变,你一定不会错过时代的脉搏。所以我并不是特别确定今后我们环信公司还能为我们整个移动互联的大时代带来什么新的突破和惊喜,但我相信一点,当未来到来时,我们一定已经在那里。”
(文章来源:猿团 作者:瘦司)
  收起阅读 »

iOS项目更新之升级Xcode7 & iOS9

前言 Apple 的WWDC所发布内容在给大家带来惊喜之际,给各位iOS开发的同仁却也带来了不同程度的麻烦。首先不讲新功能,就单指原来老版本的项目升级、代码升级,就是一堆问题,而且是不得不面临的问题。下面就跟着笔者一起来回顾下,此次在项目升级过程中,所遇到的...
继续阅读 »
前言

Apple 的WWDC所发布内容在给大家带来惊喜之际,给各位iOS开发的同仁却也带来了不同程度的麻烦。首先不讲新功能,就单指原来老版本的项目升级、代码升级,就是一堆问题,而且是不得不面临的问题。下面就跟着笔者一起来回顾下,此次在项目升级过程中,所遇到的各个问题点,以及解决方案,与各位已经做过和正在做iOS代码升级的同仁共勉,也给各位将要做Xcode 7和iOS9兼容的同仁以参考。

开发环境安装
 
原本运行得好好的项目,要升级Xcode7,首先就得安装Xcode7,具体的可以从开发者官网下载(目前最新版本是Xcode_7_GM_seed).下载好后,就双击下载好的dmg包,当然,前提还是需要我们的Mac环境升级到Mac OS 10.10.4+(图1.1),就可以打开Xcode安装镜像,如图1.2:



vYjia2.png


图1.1 Mac OS 更新示意图


 



BRviYf7.png


图1.2 Xcode 7 GM安装


 接下来,我们只要将图1.1所示的Xcode拖动到指定文件夹,即可完成安装,接下来,我们只要双击运行即可。

开发环境运行

各位可能会觉得,笔者在此还要讲开发环境的运行,是不是多此一举。其实并非如此,综合笔者这几年iOS开发经验的总结,运行新版本,特别是测试版本的Xcode是一个需要格外小心的事情,讲起来都是血泪史。

在运行Beta 版本Xcode时,我们需要特别注意以下几个方面:

- 在运行Beta版本Xcode前,务必要退出原来正式版本Xcode(如Xcode 6.4)

- 在运行Beta版本Xcode时,务必要避免双击打开工程文件(也是为了避免新旧版本同时运行)。

- 如果要切换回原来版本时,一定要先退出Beta版本,而且尽可能将Xcode的缓存数据清除。

当然,可能在实际的过程中,还是会有不少朋友就这么干了,当然,如果我们App后续只需要使用新版本Xcode,自然是没有太大关系,只是对于还需要用旧版本来开发或者发布App的朋友,可能就会有点麻烦,可能在用旧版本编译App在运行的时候,就会出现各种诡异的现象(如打印信息明明是正常,App运行逻辑却不正常等)。这时,可能大家要考虑的就是把Xcode删除掉,重新来过,甚至是重装操作系统。当然,不知道是否有朋友有更好的方案。不过笔者是不再想经历这种事情了。

App 项目运行

待项目运行,首先会碰到的问题就是配置兼容,会出现如下错误



bUZNJz.png


图2.1 BitCode 错误


 当我们看到App编译报错的时候,首先想项目不兼容Xcode7,再仔细一看
ld: ‘/Volumes/MacintoshHD/…/AnimationDesk Universal/Sources/AnimaitonDesk Universal/Classes/Supporting Files/GoogleLibrary/libGoogleAnalyticsServices.a(TAGDataProvider.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

 其中 ENABLE_BITCODE 吸引了我们的注意,看结合其它的描述信息,基本可以确定是我们使用的第三方静态库(.a)不支持BitCode,当然,我们对应就有如下两种方案来解决:

方法一:更新对应的第三方静态库(现在更新的静态库,基本都能支持BitCode)

方法二:可以将Xcode7默认开启的BitCode功能关闭,如图2.2所示



nUJFJr.png


图2.2 关闭BitCode 操作示意图


 当然,除了上面的问题外,当我们在添加Framework的时候,会发现此前导入的动态链接库(dylib)他部变成了红色,如图2.3所示,所幸的是,就算不替换成Xcode 7新的动态库文件(.tbd),仍然可以正常运行.



7RrAbaF.png


图2.3 动态链接库丢失示意图


 最后,部分App在编译的时候,可能还会收到如下报错,小编也遇到过一次
All interface orientations must be supported unless the app requires full screen.
 看到这句提示,就是说App默认是有开启了多任务功能,而多任务功能是需要App支持所有方向,如果我们App是有需要支持多任务,则需要开启App对各个方向(上、下、左、右)的支持;如果App不需要开启多任务,则只需要将如下示意图的 requires full screen 勾选上就ok(如图2.4)。



vu2MB3.png


图2.4 勾选 Requires full screen示意图


 不出意外,接下来,App应该是能正常编译运行(小编的AnimationDesk Cloud接下来是可以正常运行),但紧接着,发生了更诡异的事情,以前的的网络访问,现在完全访问不通;大家也许会觉得这可能是服务器挂了,或是外网被墙了,小编最初也是这么想的,但事实上,服务器(从Safari)还是能照常被访问,只是App访问不了,于是后来联想到iOS9 WWDC讲到的网络数据传输安全部分,经过一翻折腾,最终,网络访问的部分也恢复了正常。

其实只要在App的Info.plist里面加入如下信息就可以
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
 添加成功后的示意图如下图(图2.4)



QfyIjy.png


图2.4 添加Transport Security 示意图


 其它事项

可能还有部分朋友跟小编一样,有碰到另外一个现象,就是UITextView,无论怎么设置它的textColor显示的总会是黑色,小编已找到具体的原理,准确地讲,应该是Xcode的一个Bug。

当小编在App开发时,在Xib上面设置过UITextView的背景色(BackgroundColor)为非默认颜色(WhiteColor)时,UITextView的文字颜色(textColor)无论怎么设置,都将会是黑色,如果想要颜色值正常,可以在设置好文本后,再重设一次颜色即可正常。
 
本篇笔记由CocoonJin发表在个人博客,原文地址:http://www.cnblogs.com/CocoonJin/p/4798081.html 收起阅读 »

环信CEO刘俊彦:永远领先一步 用人工智能颠覆客服行业

环信这个成立仅三年的全通讯能力云服务提供商,去年已占据国内SaaS移动端客服市场77.4%的份额。环信CEO刘俊彦在谈及过往企业级服务市场差异的时候说道:“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服...
继续阅读 »
环信这个成立仅三年的全通讯能力云服务提供商,去年已占据国内SaaS移动端客服市场77.4%的份额。环信CEO刘俊彦在谈及过往企业级服务市场差异的时候说道:“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服务市场打开了,这是最大的区别之一。”

环信现在有2条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。



1462783486852n2i39.jpg


环信CEO刘俊彦



通过IM连接人与人,连接人与商业

当艾媒网记者问到刘俊彦环信为什么要做环信移动客服这个产品时,刘俊彦提到,在移动互联网时代,消费者会通过很多渠道来找商家解决问题,比如电话,网页,微博,微信,手机APP,而环信提供的是一种不管通过什么渠道都能够得到服务的这样一款产品,这就是环信的全媒体客服。

刘俊彦说道:“我们做IM起家,我们发现IM天然有两种场景,一种是连接人与人,另一种是连接人与商业。”用IM做社交,像微信那样发送文字语音图片等功能,促进人和人之间沟通,这个是连接人和人;第二种是用IM连接人和商业,最典型的是淘宝旺旺,或者微信公众账号,消费者可以向商家提出问题。环信刚开始做即时通讯云来连接人与人,后来发现连接“人与商业”这个市场和连接“人与人”的即时通讯云市场一样大,于是又做了环信移动客服这个产品。现在环信已经初步完成了对连接人与人和连接人与商业的2个核心场景的全覆盖。

刘俊彦说:“我们的规模和服务用户数量是最高的。我们有六万个APP用户,覆盖了3.1亿部活跃手机。每天发送信息3亿条,高并发能力是被验证过的。”

从移动IM技术入手,垄断行业

客服软件行业很早就存在,随着技术的进步,发展也极其迅速,大致可以分为三个阶段。前互联网时代,互联网还没有得到发展和普及,主要是电话客服,消费者给商家打电话;第二个阶段是进入互联网时代,即在2000——2011年,这时客服行业多了一个渠道,消费者可以通过网站找到商家,线上服务开始了;第三个是在移动互联网时代,据双11天猫统计的结果,用户80%的订单是在手机上完成的,说明如今消费者不再是在PC端或者是在电话上解决问题,遇到问题的第一反应是怎么通过手机上的原生应用得到商家支持。

环信的定位选择在移动端客服入口。刘俊彦说:“客服软件厂家的主战场一定是移动端客服,在这个战场上打赢了就相当于打赢整个客服软件的战斗。所以环信是从我们最擅长的移动IM技术入手,然后进入到移动端的客服软件。在移动端客服软件这个市场,可以说我们环信是一个绝对垄断的地位。”刘俊彦表示环信具有非常深的技术壁垒,从移动端客服这个点做深做透,然后再向全媒体客服市场拓展,做电话的客服,做微信的客服,做网页的客服,以点带面,逐渐垄断整个客服软件市场。刘俊彦认为这也是环信相比于其他客服企业的第一个优势。

加大人工智能投入巅峰行业

环信的第二个优势是在人工智能技术上,刘俊彦在采访时说道:“我们认为人工智能在实体行业第一个落地领域一定是客户服务领域。”客服领域当前是一个非常劳动密集型领域,这个行业面临着中国人口红利消失,90后不愿从事这样的工作,招聘非常难等很多问题。而环信认为这些问题一定要通过技术手段来解决,一些简单的重复的问题由机器人解答,一些复杂的问题也可以改为在人机混合模式下,由机器人提供备选答案,由真人来选择答案。




28期_本周头条.jpg


 


人工智能能会改变这个行业,阿尔法Go和人类的围棋大战将人工智能市场的关注度推上了高潮,在云服务市场,人工智能发展尚未普及和成熟。值得注意的是App市场已经趋于饱和,人工智能聊天正成为新的入口和蓝海。“人工智能会改变这个行业,跟其他的公司比较起来,在人工智能,我们技术的投入在行业是比较靠前的。”刘俊彦坚定地答道。大数据和人工智能的发展对这个行业会有一个颠覆性的改变。我们在大数据方面的投入也是靠前的。环信已推出全媒体智能客服,通过完全自主或人机混合模式的智能机器人技术极大降低人工客服工作量。在这个市场若想实现可持续发展,一定不是陷入同质化产品的无谓竞争,而是基础IT能力的实力较量。一针见血。这个技术出身、目前仍自诩程序员的创业者,洞察市场和未来的时候却不失犀利。

剖析用户痛点 解决用户痛处,永远领先对手一步

刘俊彦在接受艾媒网记者采访时,深入剖析了用户的痛点。首先,他认为客户需要全媒体客服解决方案,去年之前,很多企业客服是以电话为主。去年之后,消费者很快速地向移动端转移。消费者只要不是紧急和复杂的情况,都不想打电话,因为电话是一个很重的沟通方式。很多企业需要同时服务四个渠道,包括电话、网页、微信、APP,以前没有厂家提供。

其次,中国的人口红利正在消失,现在的年轻人越来越不愿意做客服工作。很多公司的客服部门经历了从北京市区迁到郊区,再迁到合肥贵州,下一步可能就要迁到老挝越南了,这是一种不可持续的局面,必须要通过技术手段来解决人力的问题。

刘俊彦针对用户的这些痛点,介绍环信的解决方案。

第一,目前环信已经把全媒体客服产品打磨的很好了。“我们是第一家真正提供全媒体客服的厂家。用户需要的是电话加网页加微信加APP的四合一的整体客服解决方案,数据完全打通。环信这种全媒体客服就解决这种痛点。”刘俊彦说道。

第二,环信已经有一套比较好用的智能机器人系统。人工智能和大数据技术将改变客服行业,将客服行业从一个劳动密集型的行业变成一个高科技驱动的行业,用机器代替一部分的人工。

刘俊彦最后总结说:“我们希望可以永远领先对手一步。当对手气喘吁吁爬上一座山岗以为追上环信时,发现环信已经不在这里了,环信已经在另外一座更高的山岗了。所以我们做了很多有别竞争对手的差异化产品。比如环信反垃圾产品。”。很多社交产品用户被垃圾消息骚扰特别严重从而导致用户流失,我们帮助企业做反垃圾服务从而帮助提高用户留存。环信也是行业首个推出反垃圾服务的厂商。

另一个和对手差异化的产品是环信社交大数据。这个产品帮助企业分析IM用户的关系链和社交行为,从而提高APP的日活和粘性。还有环信业界首推的环信红包功能,这种产品能够帮助客户提高变现能力环信永远在功能上领先对手一个层面。 收起阅读 »

环信即时通讯单聊集成,添加好友,实现单聊

前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。    先上一下效果图吧 首先,我们要去环信官网注册账号,这个...
继续阅读 »
前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。 
 
先上一下效果图吧



20160510104925517.png




20160510105128098.png




20160510105400382.png




20160510105511478.png




20160510105614263.png




首先,我们要去环信官网注册账号,这个我就不多说了,注册完登录,创建应用,新建两个测试IM用户,

20160510100622399.png


这里主要用到的是应用标示(Appkey) 

20160510100635834.png




D870.tmp_.jpg


好了,在环信官网下载对应的sdk,这个不多说了,最好下载一个文档,里面讲的很详细的。 
好了,一下是源码 

20160510101347448.png


AppManager.Java
public class AppManager {
private static Stack<Activity> mActivityStack;
private static AppManager mAppManager;

private AppManager() {
}

/**
* 单一实例
*/
public static AppManager getInstance() {
if (mAppManager == null) {
mAppManager = new AppManager();
}
return mAppManager;
}

/**
* 添加Activity
*/
public void addActivity(Activity activity) {
if (mActivityStack == null) {
mActivityStack = new Stack<Activity>();
}
mActivityStack.add(activity);
}

/**
* 获取栈顶Activity
*/
public Activity getTopActivity() {
Activity activity = mActivityStack.lastElement();
return activity;
}

/**
* 结束栈顶Activity
*/
public void killTopActivity() {
Activity activity = mActivityStack.lastElement();
killActivity(activity);
}

/**
* 结束指定的Activity
*/
public void killActivity(Activity activity) {
if (activity != null) {
mActivityStack.remove(activity);
activity.finish();
activity = null;
}
}

/**
* 结束指定类名的Activity
*/
public void killActivity(Class<?> cls) {
for (Activity activity : mActivityStack) {
if (activity.getClass().equals(cls)) {
killActivity(activity);
}
}
}

/**
* 结束所有Activity
*/
public void killAllActivity() {
for (int i = 0, size = mActivityStack.size(); i < size; i++) {
if (null != mActivityStack.get(i)) {
mActivityStack.get(i).finish();
}
}
mActivityStack.clear();
}

/**
* 退出应用程序
*/
public void AppExit(Context context) {
try {
killAllActivity();
ActivityManager activityMgr = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityMgr.restartPackage(context.getPackageName());
System.exit(0);
} catch (Exception e) {}
}
}
BaseActivity.java​
public abstract class BaseActivity extends Activity {

protected Context context = null;
protected BaseApplication mApplication;
protected Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApplication = (BaseApplication) getApplication();
AppManager.getInstance().addActivity(this);
// check netwotk
context = this;
}

@Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}

@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
}

}
BaseApplication.java​
public class BaseApplication extends Application {

private static final String TAG = BaseApplication.class.getSimpleName();

private static BaseApplication mInstance = null;

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/*
* (non-Javadoc)
*
* @see android.app.Application#onCreate()
*/
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果app启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process
// name就立即返回

if (processAppName == null
|| !processAppName.equalsIgnoreCase("com.xmliu.imsample")) {
Log.e(TAG, "enter the service process!");
// "com.easemob.chatuidemo"为demo的包名,换到自己项目中要改成自己包名

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

// EMChat.getInstance().setAutoLogin(false);
EMChat.getInstance().init(getApplicationContext());
// 在做代码混淆的时候需要设置成false
EMChat.getInstance().setDebugMode(true);
initHXOptions();
mInstance = this;

}

protected void initHXOptions() {
Log.d(TAG, "init HuanXin Options");

// 获取到EMChatOptions对象
EMChatOptions options = EMChatManager.getInstance().getChatOptions();
// 默认添加好友时,是不需要验证的true,改成需要验证false
options.setAcceptInvitationAlways(false);
// 默认环信是不维护好友关系列表的,如果app依赖环信的好友关系,把这个属性设置为true
options.setUseRoster(true);
options.setNumberOfMessagesLoaded(1);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this
.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i
.next());
try {
if (info.pid == pID) {
CharSequence c = pm.getApplicationLabel(pm
.getApplicationInfo(info.processName,
PackageManager.GET_META_DATA));
// Log.d("Process", "Id: "+ info.pid +" ProcessName: "+
// info.processName +" Label: "+c.toString());
// processName = c.toString();
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}

@Override
public void onLowMemory() {
// TODO Auto-generated method stub
super.onLowMemory();
Log.i(TAG, "onLowMemory");
}

@Override
public void onTerminate() {
// TODO Auto-generated method stub
Log.i(TAG, "onTerminate");
super.onTerminate();
}

public static BaseApplication getInstance() {
return mInstance;
}

}
ChatListAdapter.java​
public class ChatListAdapter extends BaseAdapter {

Context mContext;
List<ChatListData> mListData;

public ChatListAdapter(Context mContext, List<ChatListData> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub

Holder holder;
if (cView == null) {
holder = new Holder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.chat_listview_item, null);
holder.rAvatar = (Button) cView
.findViewById(R.id.listview_item_receive_avatar);
holder.rContent = (TextView) cView
.findViewById(R.id.listview_item_receive_content);
holder.chatTime = (TextView) cView
.findViewById(R.id.listview_item_time);
holder.sContent = (TextView) cView
.findViewById(R.id.listview_item_send_content);
holder.sAvatar = (Button) cView
.findViewById(R.id.listview_item_send_avatar);
holder.sName = (TextView) cView.findViewById(R.id.name1);
holder.sName1 = (TextView) cView.findViewById(R.id.name2);
cView.setTag(holder);

} else {
holder = (Holder) cView.getTag();
}

holder.chatTime.setVisibility(View.GONE);

if (mListData.get(index).getType() == 2) {
holder.rAvatar.setVisibility(View.VISIBLE);
holder.rContent.setVisibility(View.VISIBLE);
holder.sName.setVisibility(View.VISIBLE);
holder.sName.setText("您的朋友说:");
holder.sContent.setVisibility(View.GONE);
holder.sAvatar.setVisibility(View.GONE);
holder.sName1.setVisibility(View.GONE);

} else if (mListData.get(index).getType() == 1) {
holder.rAvatar.setVisibility(View.GONE);
holder.sName.setVisibility(View.GONE);
holder.rContent.setVisibility(View.GONE);
holder.sContent.setVisibility(View.VISIBLE);
holder.sAvatar.setVisibility(View.VISIBLE);
holder.sName1.setVisibility(View.VISIBLE);
holder.sName1.setText("我");
}
holder.chatTime.setText(mListData.get(index).getChatTime());
holder.rContent.setText(mListData.get(index).getReceiveContent());
holder.sContent.setText(mListData.get(index).getSendContent());

return cView;
}

class Holder {
Button rAvatar;
TextView rContent;
TextView chatTime;
TextView sContent;
TextView sName;
TextView sName1;
Button sAvatar;
}
}
FriendListAdapter.java​
public class FriendListAdapter extends BaseAdapter {

Context mContext;
List<String> mListData;

public FriendListAdapter(Context mContext, List<String> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
FHolder holder;
if (cView == null) {
holder = new FHolder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.friend_listview_item, null);
holder.name = (TextView) cView
.findViewById(R.id.friend_listview_name);
cView.setTag(holder);

} else {
holder = (FHolder) cView.getTag();
}

holder.name.setText(mListData.get(index));

return cView;
}

class FHolder {
TextView name;
}
}
ChatListData.java​
public class ChatListData {

String receiveAvatar;
String receiveContent;
String chatTime;
String sendAvatar;
String sendContent;
/**
* 1 发送; 2接收
*/
int type;
/**
* 1 发送; 2接收
*/
public int getType() {
return type;
}
/**
* 1 发送; 2接收
*/
public void setType(int type) {
this.type = type;
}

public String getReceiveAvatar() {
return receiveAvatar;
}

public void setReceiveAvatar(String receiveAvatar) {
this.receiveAvatar = receiveAvatar;
}

public String getReceiveContent() {
return receiveContent;
}

public void setReceiveContent(String receiveContent) {
this.receiveContent = receiveContent;
}

public String getChatTime() {
return chatTime;
}

public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}

public String getSendAvatar() {
return sendAvatar;
}

public void setSendAvatar(String sendAvatar) {
this.sendAvatar = sendAvatar;
}

public String getSendContent() {
return sendContent;
}

public void setSendContent(String sendContent) {
this.sendContent = sendContent;
}

}
ChatListActivity.java​
public class ChatListActivity extends BaseActivity {

private EditText contentET;
private TextView topNameTV;
private Button sendBtn;
private NewMessageBroadcastReceiver msgReceiver;

private ListView mListView;
private List<ChatListData> mListData = new ArrayList<ChatListData>();
private ChatListAdapter mAdapter;
private InputMethodManager imm;

private String receiveName = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_main);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0x00001:
imm.hideSoftInputFromWindow(
contentET.getApplicationWindowToken(), 0); // 隐藏键盘
mAdapter.notifyDataSetChanged(); // 刷新聊天列表
mListView.setSelection(mListData.size()); // 跳转到listview最底部
contentET.setText(""); // 清空发送内容
break;
default:
break;
}
}

};

receiveName = this.getIntent().getStringExtra("userid");

initView();

topNameTV.setText(receiveName);
// 只有注册了广播才能接收到新消息,目前离线消息,在线消息都是走接收消息的广播(离线消息目前无法监听,在登录以后,接收消息广播会执行一次拿到所有的离线消息)
msgReceiver = new NewMessageBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(EMChatManager
.getInstance().getNewMessageBroadcastAction());
intentFilter.setPriority(3);
registerReceiver(msgReceiver, intentFilter);

imm = (InputMethodManager) contentET.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);

mAdapter = new ChatListAdapter(ChatListActivity.this, mListData);
mListView.setAdapter(mAdapter);

initEvent();
}

private void initView() {
contentET = (EditText) findViewById(R.id.chat_content);
topNameTV = (TextView) findViewById(R.id.chat_list_name);
sendBtn = (Button) findViewById(R.id.chat_send_btn);
mListView = (ListView) findViewById(R.id.chat_listview);
}

private void initEvent() {
// TODO Auto-generated method stub
sendBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
sendMsg();
}
});

contentET.setOnKeyListener(new OnKeyListener() {

@Override
public boolean onKey(View arg0, int keycode, KeyEvent arg2) {
// TODO Auto-generated method stub
if (keycode == KeyEvent.KEYCODE_ENTER
&& arg2.getAction() == KeyEvent.ACTION_DOWN) {
sendMsg();
return true;
}
return false;
}
});
}

void sendMessageHX(String username, final String content) {
// 获取到与聊天人的会话对象。参数username为聊天人的userid或者groupid,后文中的username皆是如此
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
// 创建一条文本消息
EMMessage message = EMMessage.createSendMessage(EMMessage.Type.TXT);
// // 如果是群聊,设置chattype,默认是单聊
// message.setChatType(ChatType.GroupChat);
// 设置消息body
TextMessageBody txtBody = new TextMessageBody(content);
message.addBody(txtBody);
// 设置接收人
message.setReceipt(username);
// 把消息加入到此会话对象中
conversation.addMessage(message);
// 发送消息
EMChatManager.getInstance().sendMessage(message, new EMCallBack() {

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送失败");
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "正在发送消息");
}

@Override
public void onSuccess() {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送成功");
ChatListData data = new ChatListData();
data.setSendContent(content);
data.setType(1);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
}
});
}

private class NewMessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 注销广播
abortBroadcast();

// 消息id(每条消息都会生成唯一的一个id,目前是SDK生成)
String msgId = intent.getStringExtra("msgid");
// 发送方
String username = intent.getStringExtra("from");
// 收到这个广播的时候,message已经在db和内存里了,可以通过id获取mesage对象
EMMessage message = EMChatManager.getInstance().getMessage(msgId);
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);

MessageBody tmBody = message.getBody();

ChatListData data = new ChatListData();
data.setReceiveContent(((TextMessageBody) tmBody).getMessage());
data.setType(2);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);

Log.i("TAG", "收到消息:" + ((TextMessageBody) tmBody).getMessage());
// 如果是群聊消息,获取到group id
if (message.getChatType() == ChatType.GroupChat) {
username = message.getTo();
}

if (!username.equals(username)) {
// 消息不是发给当前会话,return
return;
}
}
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(msgReceiver);

}

private void sendMsg() {
String content = contentET.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(getApplicationContext(), "请输入发送的内容",
Toast.LENGTH_SHORT).show();
} else {
sendMessageHX(receiveName, content);
}
}

}
ChatLoginActivity.java​
public class ChatLoginActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private TextView mPasswordForgetTV;
private Button mSigninBtn;
private TextView mSignupTV;
private CheckBox mPasswordCB;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_login);

mUsernameET = (EditText) findViewById(R.id.chat_login_username);
mPasswordET = (EditText) findViewById(R.id.chat_login_password);
mPasswordForgetTV = (TextView) findViewById(R.id.chat_login_forget_password);
mSigninBtn = (Button) findViewById(R.id.chat_login_signin_btn);
mSignupTV = (TextView) findViewById(R.id.chat_login_signup);
mPasswordCB = (CheckBox) findViewById(R.id.chat_login_password_checkbox);

if (EMChat.getInstance().isLoggedIn()) {
Log.d("TAG", "已经登陆过");
EMGroupManager.getInstance().loadAllGroups();
EMChatManager.getInstance().loadAllConversations();
startActivity(new Intent(ChatLoginActivity.this,
MainActivity.class));
}

mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
//动态设置密码是否可见
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});

mSigninBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else {
EMChatManager.getInstance().login(userName, password,
new EMCallBack() {// 回调
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
EMGroupManager.getInstance()
.loadAllGroups();
EMChatManager.getInstance()
.loadAllConversations();
Log.d("main", "登陆聊天服务器成功!");
Toast.makeText(
getApplicationContext(),
"登陆成功", Toast.LENGTH_SHORT)
.show();
startActivity(new Intent(
ChatLoginActivity.this,
MainActivity.class));
// mApplication.mSharedPreferences
// .edit()
// .putString("loginName",
// userName).commit();
}
});
}

@Override
public void onProgress(int progress,
String status) {

}

@Override
public void onError(int code, String message) {
if (code == -1005) {
message = "用户名或密码错误";
}
final String msg = message;
runOnUiThread(new Runnable() {
public void run() {
Log.d("main", "登陆聊天服务器失败!");
Toast.makeText(
getApplicationContext(),
msg, Toast.LENGTH_SHORT)
.show();
}
});
}
});
}
}
});

mSignupTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startActivity(new Intent(ChatLoginActivity.this,
ChatRegisterActivity.class));
}
});
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

new AlertDialog.Builder(ChatLoginActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出"
+ getResources().getString(
R.string.app_name) + "客户端吗?")
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
AppManager.getInstance().AppExit(
ChatLoginActivity.this);
ChatLoginActivity.this.finish();
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
}
}).show();
}

return super.onKeyDown(keyCode, event);
}
}
ChatRegisterActivity.java​
public class ChatRegisterActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private EditText mCodeET;
private Button mSignupBtn;
private Handler mHandler;
private CheckBox mPasswordCB;
private TextView mBackTV;
private ImageView mCodeIV;
private String currCode;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_register);

mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1000:
Toast.makeText(getApplicationContext(), "注册成功",
Toast.LENGTH_SHORT).show();
break;
case 1001:
Toast.makeText(getApplicationContext(), "网络异常,请检查网络!",
Toast.LENGTH_SHORT).show();
break;
case 1002:
Toast.makeText(getApplicationContext(), "用户已存在!",
Toast.LENGTH_SHORT).show();
break;
case 1003:
Toast.makeText(getApplicationContext(), "注册失败,无权限",
Toast.LENGTH_SHORT).show();
break;
case 1004:
Toast.makeText(getApplicationContext(),
"注册失败: " + (String) msg.obj, Toast.LENGTH_SHORT)
.show();
break;

default:
break;
}
};
};

mUsernameET = (EditText) findViewById(R.id.chat_register_username);
mPasswordET = (EditText) findViewById(R.id.chat_register_password);
mCodeET = (EditText) findViewById(R.id.chat_register_code);
mSignupBtn = (Button) findViewById(R.id.chat_register_signup_btn);
mPasswordCB = (CheckBox) findViewById(R.id.chat_register_password_checkbox);
mBackTV = (TextView) findViewById(R.id.chat_register_back);
mCodeIV = (ImageView) findViewById(R.id.chat_register_password_code);

mCodeIV.setImageBitmap(IdentifyCode.getInstance().createBitmap());
currCode = IdentifyCode.getInstance().getCode();

mCodeIV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
mCodeIV.setImageBitmap(IdentifyCode.getInstance()
.createBitmap());
currCode = IdentifyCode.getInstance().getCode();
Log.i("TAG", "currentCode==>" + currCode);
}
});

mBackTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
finish();
}
});
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSignupBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
final String code = mCodeET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(code)) {
Toast.makeText(getApplicationContext(), "请输入验证码",
Toast.LENGTH_SHORT).show();
} else if (!code.equals(currCode.toLowerCase())) {
Toast.makeText(getApplicationContext(), "验证码输入不正确",
Toast.LENGTH_SHORT).show();
} else {
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
// 调用sdk注册方法
EMChatManager.getInstance()
.createAccountOnServer(userName,
password);
mHandler.sendEmptyMessage(1000);
} catch (final EaseMobException e) {
// 注册失败
Log.i("TAG", "getErrorCode:" + e.getErrorCode());
int errorCode = e.getErrorCode();
if (errorCode == EMError.NONETWORK_ERROR) {
mHandler.sendEmptyMessage(1001);
} else if (errorCode == EMError.USER_ALREADY_EXISTS) {
mHandler.sendEmptyMessage(1002);
} else if (errorCode == EMError.UNAUTHORIZED) {
mHandler.sendEmptyMessage(1003);
} else {
Message msg = Message.obtain();
msg.what = 1004;
msg.obj = e.getMessage();
mHandler.sendMessage(msg);
}
}
}
}).start();
}
}
});
}
}
MainActivity.java​
public class MainActivity extends BaseActivity {

private ListView mListView;
private Button mAddBtn;
private Button logoutBtn;
private View addView;
private EditText mIdET;
private EditText mReasonET;
private TextView mUserTV;
private TextView mGoTV;
private FriendListAdapter mAdapter;
private List<String> userList = new ArrayList<String>();

/* 常量 */
private final int CODE_ADD_FRIEND = 0x00001;

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_friends);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case CODE_ADD_FRIEND:
Toast.makeText(getApplicationContext(), "请求发送成功,等待对方验证",
Toast.LENGTH_SHORT).show();
break;

default:
break;
}
}

};

EMContactManager.getInstance().setContactListener(
new MyContactListener());
EMChat.getInstance().setAppInited();

mListView = (ListView) findViewById(R.id.chat_listview);
mAddBtn = (Button) findViewById(R.id.chat_add_btn);
mUserTV = (TextView) findViewById(R.id.current_user);
mGoTV = (TextView) findViewById(R.id.friend_list_go);
logoutBtn = (Button) findViewById(R.id.chat_logout_btn);

mUserTV.setText(EMChatManager.getInstance().getCurrentUser());

initList();

mAddBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
addView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.chat_add_friends, null);
mIdET = (EditText) addView
.findViewById(R.id.chat_add_friend_id);
mReasonET = (EditText) addView
.findViewById(R.id.chat_add_friend_reason);
new AlertDialog.Builder(MainActivity.this)
.setTitle("添加好友")
.setView(addView)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String idStr = mIdET.getText()
.toString().trim();
String reasonStr = mReasonET.getText()
.toString().trim();
try {
EMContactManager.getInstance()
.addContact(idStr,
reasonStr);
mHandler.sendEmptyMessage(CODE_ADD_FRIEND);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG", "addContacterrcode==>"
+ e.getErrorCode());
}// 需异步处理
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
dialog.dismiss();
}
}).create().show();

}
});
logoutBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub

showLogoutDialog();

}
});

mListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatListActivity.class).putExtra("userid",
userList.get(arg2)));
}
});

mListView.setOnItemLongClickListener(new OnItemLongClickListener() {

@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
// TODO Auto-generated method stub
showDeleteDialog(userList.get(arg2));
return true;
}
});
}

private void initList() {
try {
userList.clear();
userList = EMContactManager.getInstance().getContactUserNames();
mAdapter = new FriendListAdapter(MainActivity.this, userList);
mListView.setAdapter(mAdapter);
} catch (EaseMobException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
Log.i("TAG", "usernames errcode==>" + e1.getErrorCode());
Log.i("TAG", "usernames errcode==>" + e1.getMessage());
}// 需异步执行
}

private class MyContactListener implements EMContactListener {

@Override
public void onContactAgreed(String username) {
// 好友请求被同意
Log.i("TAG", "onContactAgreed==>" + username);
// 提示有新消息
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
Toast.makeText(getApplicationContext(), username + "同意了你的好友请求",
Toast.LENGTH_SHORT).show();
}

@Override
public void onContactRefused(String username) {
// 好友请求被拒绝
Log.i("TAG", "onContactRefused==>" + username);
}

@Override
public void onContactInvited(String username, String reason) {
// 收到好友添加请求
Log.i("TAG", username + "onContactInvited==>" + reason);
showAgreedDialog(username, reason);
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
}

@Override
public void onContactDeleted(List<String> usernameList) {
// 好友被删除时回调此方法
Log.i("TAG", "usernameListDeleted==>" + usernameList.size());
}

@Override
public void onContactAdded(List<String> usernameList) {
// 添加了新的好友时回调此方法
for (String str : usernameList) {
Log.i("TAG", "usernameListAdded==>" + str);
}
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

showExitDialog();
}

return super.onKeyDown(keyCode, event);
}

private void showLogoutDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要注销" + EMChatManager.getInstance().getCurrentUser()
+ "用户吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// EMChatManager.getInstance().logout();
logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatLoginActivity.class));
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub

}

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub

}
});

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

public void logout(final EMCallBack callback) {
// setPassword(null);
EMChatManager.getInstance().logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
if (callback != null) {
callback.onSuccess();
}
}

@Override
public void onError(int code, String message) {
// TODO Auto-generated method stub

}

@Override
public void onProgress(int progress, String status) {
// TODO Auto-generated method stub
if (callback != null) {
callback.onProgress(progress, status);
}
}

});
}

private void showAgreedDialog(final String user, String reason) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"用户 " + user + " 想要添加您为好友,是否同意?\n" + "验证信息:" + reason)
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMChatManager.getInstance().acceptInvitation(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
try {
EMChatManager.getInstance().refuseInvitation(user);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog2==>" + e.getErrorCode());
}
}
})
.setNeutralButton("忽略", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showDeleteDialog(final String user) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage("确定删除好友 " + user + " 吗?\n")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMContactManager.getInstance().deleteContact(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showExitDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出" + getResources().getString(R.string.app_name)
+ "客户端吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AppManager.getInstance().AppExit(MainActivity.this);
MainActivity.this.finish();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

}
IdentifyCode.java​
public class IdentifyCode {

private static final char CHARS = { '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

private static IdentifyCode bmpCode;

public static IdentifyCode getInstance() {
if (bmpCode == null)
bmpCode = new IdentifyCode();
return bmpCode;
}

// default settings
private static final int DEFAULT_CODE_LENGTH = 3;
private static final int DEFAULT_FONT_SIZE = 25;
private static final int DEFAULT_LINE_NUMBER = 2;
private static final int BASE_PADDING_LEFT = 5, RANGE_PADDING_LEFT = 15,
BASE_PADDING_TOP = 15, RANGE_PADDING_TOP = 20;
private static final int DEFAULT_WIDTH = 60, DEFAULT_HEIGHT = 40;

// settings decided by the layout xml
// canvas width and height
private int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;

// random word space and pading_top
private int base_padding_left = BASE_PADDING_LEFT,
range_padding_left = RANGE_PADDING_LEFT,
base_padding_top = BASE_PADDING_TOP,
range_padding_top = RANGE_PADDING_TOP;

// number of chars, lines; font size
private int codeLength = DEFAULT_CODE_LENGTH,
line_number = DEFAULT_LINE_NUMBER, font_size = DEFAULT_FONT_SIZE;

// variables
private String code;
private int padding_left, padding_top;
private Random random = new Random();

// 验证码图�?
public Bitmap createBitmap() {
padding_left = 0;

Bitmap bp = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas c = new Canvas(bp);

code = createCode();

c.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setTextSize(font_size);

for (int i = 0; i < code.length(); i++) {
randomTextStyle(paint);
randomPadding();
c.drawText(code.charAt(i) + "", padding_left, padding_top, paint);
}

for (int i = 0; i < line_number; i++) {
drawLine(c, paint);
}

c.save(Canvas.ALL_SAVE_FLAG);// 保存
c.restore();//
return bp;
}

public String getCode() {
return code;
}

// 验证�?
private String createCode() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
buffer.append(CHARS[random.nextInt(CHARS.length)]);
}
return buffer.toString();
}

private void drawLine(Canvas canvas, Paint paint) {
int color = randomColor();
int startX = random.nextInt(width);
int startY = random.nextInt(height);
int stopX = random.nextInt(width);
int stopY = random.nextInt(height);
paint.setStrokeWidth(1);
paint.setColor(color);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}

private int randomColor() {
return randomColor(1);
}

private int randomColor(int rate) {
int red = random.nextInt(256) / rate;
int green = random.nextInt(256) / rate;
int blue = random.nextInt(256) / rate;
return Color.rgb(red, green, blue);
}

private void randomTextStyle(Paint paint) {
int color = randomColor();
paint.setColor(color);
paint.setFakeBoldText(random.nextBoolean()); // true为粗体,false为非粗体
float skewX = random.nextInt(11) / 10;
skewX = random.nextBoolean() ? skewX : -skewX;
paint.setTextSkewX(skewX); // float类型参数,负数表示右斜,整数左斜
// paint.setUnderlineText(true); //true为下划线,false为非下划线?
// paint.setStrikeThruText(true); //true为删除线,false为非删除线?
}

private void randomPadding() {
padding_left += base_padding_left + random.nextInt(range_padding_left);
padding_top = base_padding_top + random.nextInt(range_padding_top);
}
}
布局文件就相对简单很多了,登录页面很简单,还是贴出来吧。 
activity_chat_login.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/chat_login_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#CAFFFF"
android:orientation="vertical"
android:paddingBottom="30dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="60dp" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:orientation="vertical" >

<EditText
android:id="@+id/chat_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#00000000"
android:drawableLeft="@drawable/login_user"
android:drawablePadding="5dp"
android:ems="10"
android:hint="用户名"
android:inputType="textPersonName"
android:textColor="#fff"
android:textSize="12sp" />

<View
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >

<EditText
android:id="@+id/chat_login_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="#00000000"
android:drawableLeft="@drawable/login_password"
android:drawablePadding="5dp"
android:ems="10"
android:hint="密码"
android:inputType="textPassword"
android:textColor="#fff"
android:textSize="12sp" />

<CheckBox
android:id="@+id/chat_login_password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:button="@drawable/password_checkbox" />
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/chat_login_signin_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="#359D90"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal" >

<TextView
android:id="@+id/chat_login_signup0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#5D5D5D"
android:textSize="12sp" />

<TextView
android:id="@+id/chat_login_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/chat_login_signup0"
android:text="注册用户"
android:textColor="#6F6F6F"
android:textSize="12sp"
android:textStyle="bold" />

<TextView
android:id="@+id/chat_login_forget_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="忘记密码"
android:textColor="#5D5D5D"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

</LinearLayout>
好友列表页 
activity_chat_friends.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#359D90"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingTop="5dp" >

<TextView
android:id="@+id/current_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我的好友"
android:textColor="#fff" />

<Button
android:id="@+id/chat_logout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:background="@drawable/chat_logout_icon" />
</RelativeLayout>

<TextView
android:id="@+id/friend_list_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textStyle="bold|italic"
android:textColor="#000fff"
android:text="好友列表" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="5dp"
android:background="#DDDDDD" />

<ListView
android:id="@+id/chat_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="none" />

<Button
android:id="@+id/chat_add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/send_btn_bg"
android:paddingBottom="12dp"
android:paddingLeft="10
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七 收起阅读 »

【演讲视频】环信:全媒体智能客服时代的最佳实践

  由CTI论坛主办的2016中国呼叫中心及企业通信大会于4月14至15日在北京辽宁大厦隆重举行,本次会议的主题是:联络中心的数字化DNA。环信创始人刘俊彦出席此次会议并发表主题演讲《全媒体智能客服时代的最佳实践》,以下为演讲视频。 视频地址http://ww...
继续阅读 »
  由CTI论坛主办的2016中国呼叫中心及企业通信大会于4月14至15日在北京辽宁大厦隆重举行,本次会议的主题是:联络中心的数字化DNA。环信创始人刘俊彦出席此次会议并发表主题演讲《全媒体智能客服时代的最佳实践》,以下为演讲视频。
视频地址http://www.ctiforum.com/news/shiping/482405.html 收起阅读 »

Parse 移植:如何将 Parse 服务器迁移部署到 Heroku 或 AWS 上

原文链接:http://www.appcoda.com/parse-server-installation/ 作者:Gregg Mojica at AppCoda 原文日期:2016-04-16 译者:Crystal Sun   继续我之前的这篇文章...
继续阅读 »
原文链接:http://www.appcoda.com/parse-server-installation/
作者:Gregg Mojica at AppCoda
原文日期:2016-04-16
译者:Crystal Sun
 
继续我之前的这篇文章 migrating a parse database to a self-host MongoDB instance,在这次的春季辅导教程中,我们看一下如何将 parse 服务器迁移到 Heroku 和 Amazon Web Service。

对于还不了解 Parse 之死的人来说,这意味着服务器(处理数据,与数据库互动,发送接收请求等待)需要迁移到其他地方了。Parse,后端即服务(BaaS),为开发者提供服务器和数据库的服务。然而,随着 Parse 即将在一月份关闭,官方建议,在2017年1月28日彻底停止服务之前,请迁移 Parse 应用。Parse 官方建议你先迁移数据库,然后在迁移服务器。本节教程会假定你一看完成了数据库的迁移,正如我们在上篇教程第一部分中所做的。

幸运的是,parse-server(GitHub 项目,由 Facebook 开源,伟大的 Parse 统治者)可以部署在大部分的云服务上。在本节教程里,我们会讲述如何将 parse-serve 部署到 Heroku,Salesforce 旗下知名的云服务供应商。在本篇文章的最后部分,我们会演示如何部署到 Amazon Web Services(AWS)上,世界上很多知名的 App 都在使用 AWS 的服务。

准备开始

首先到 Heroku.com 网站注册一个帐号。为了演示 demo,我选择了免费方案。你根据自己的需要,选择合适的方案,比如付费方案。你可以在这里看到所有的付费方案。

部署到 heroku 有两种方法可供选择。第一种是点击 Deploy to Heroku 按钮,然后出现一步接一步的提示流程,因为 Parse 已经在 Heroku 的服务器上设置过 parse-server 了,对非 Javascript 程序员来说,这可能是最简单的方法了。如果你熟悉 git 和命令行,请随意使用克隆应用然后用命令行完成。话虽如此,但是你不能一辈子都避免使用命令行。不管你选择那种方式,都会涉及到命令行的。

Option 1: 使用 Heroku 按钮


68747470733a2f2f7777772e6865726f6b7563646e2e636f6d2f6465706c6f792f627574746f6e2e706e67.png


点击上面的按钮,创建一个新的 heroku 应用,你会看到类似下方图片的界面:


178769-23db0d3bc3bb96b9.png



设置向导出现,让你输入应用名称(全部小写不允许有空格)。

接下来,选择 runtime 选项。如果你住在美国,选择 United States(美国),其他地方,选择 Europe(欧洲)。runtime 选项,就是你希望你的应用部署在哪个地方。考虑到性能和速度,最好将应用服务器部署在离你较近的地方。



178769-926ab59bbb354d32.png


接下来更新配置,填写 Parse 账户里对应的密钥(或者生成新的密钥,如果你不是迁移现存应用的话,这点以后再说)。安装路径为 /parse。

当你填完所有的字段后,点击 deploy 按钮,暂时先空着 MongoLab(也就做 mLab)开发。


178769-68ef235cdeb48627.png


可能需要你输入你的信用卡。

Option 2: 克隆 Heroku 应用

parse-server 是开源项目,目前可以在 GitHub 上下载。如果你选择的是命令行,而不是点击 heroku 按钮,那么继续下方的操作。开始前,先打开终端(Terminal),使用下方的命令来克隆应用:
 
cd ~
cd Desktop

git clone https://github.com/ParsePlatform/parse-server-example.git
git add .
git init
git commit -m "Initial Commit"


178769-5121361f7445b4cc.png


现在,你已经成功地将 parse-server 克隆到桌面上了。

修改数据库的 URI

不管你在上面选择了哪个方式,现在你的应用在一定程度上已经设置过了。如果你使用是 Option 1,你需要在你电脑里复制一份本地代码副本,首先用下列命令行(也会将 App 克隆到电脑桌面)。
注意:下方的选项适用于选择了 Option 1 的人

 
$ heroku login 

$ cd ~/Desktop
$ heroku git:clone -a your-app-name
$ cd your-app-name

$ git add .
$ git commit -am "make it better"
$ git push heroku master

登录后,需要输入认证(之后会详细说明,不过现在只需要输入 Heroku 帐号的邮箱和密码,密码不会出现在屏幕上)。

现在,打开你最喜欢的文本编辑器(我比较喜欢 Sublime Text),打开新克隆的库(repository)(对于新手来说,你可以直接将整个文件夹拖到 sublime text 图标上,然后 sulime text 会自动文件,或者使用顶部菜单的 File -> Open)。



178769-4209280fb02995e1.png


现在,我们需要打开 index.js 文件,修改 API 变量。注意第 14-23 行。

178769-64760bbb74648fba.png


从第 14 行开始,我们需要修改 databaseURL 参数。替换路径,使用在本教程第一部分生成的路径,例如,我会使用下面的 url,不过你必须用你自己的 url 来替换。
mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test

接下来,我们需要填写 appId 和 masterKey 参数。如果你是在迁移一个已经存在的应用,到 parse.com 上找到对应的数据。如果这是你第一次使用 parse-server 创建一个新工程,你可以生成随机的字幕数字组成的密钥。

在 parse.com 网站上登录你的 Parse 帐号,找到 Settings(设置),在这里,选择 Security & Keys。复制粘贴你的 Application ID(这个应用的,不要复制成其他应用的)和 Master Key。下面的图片可供你参考(我的密钥出于安全考虑遮挡住了)。



178769-f2f761775be12eb5.png

注意:如果你选的是 Option 1,你已经设置了你的密钥,你可以直接跳过这一步。即使如此,我还是建议你看一下,这样你能对 parse-server 的工作机制有更深入的理解。
在 index.js 文件里替换上你刚刚复制来的新密钥,你也可以添加 clientKey 作为一个参数,从 Parse 中获取。

最后,记住保存你的操作,快捷键 Command+S(Mac电脑上)。

如果你不是迁移应用,那么使用随机生成器(例如 random.org 或其他类似的东西)来生成字母数字密钥。

接下来,部署 Heroku。

将 Parse 服务器部署到 Heroku

首先在电脑上安装 Heroku 工具条,从链接中可以找到官方安装指南。安装完成后,在终端(Terminal)中输入下列命令行:
heroku login
接下来输入登录 Heroku 信息,注意当你输入密码的时候,密码不会出现在屏幕上。

如果你选择的是 Option 1,就没有必要用下面的命令行创建一个 Heroku 应用了。如果你选择的是 Option 2,确保输入下列命令行来创建一个 Heroku 应用。
heroku create
Heroku 会给你创建一个应用,现在提交修改内容,代码如下:
git add .
git init
git commit -m "Updated api config"
git push heroku master
现在,你已经成功部署了 Heroku!如果你遇到任何错误,请在下方的评论栏中留意,我将尽力帮助你。
 
设置 Heroku 的环境变量

接下来,我们需要设置 Heroku 的环境变量,回到终端(Terminal),输入下列命令行(使用你的 MongoDB 实例中的URI,我们之前谈论过)。
heroku config:set DATABASE_URI=mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test
回到 Heroku 网页上,点击你的应用,在 Settings tab 页下,点击 reveal config variables。
 


178769-00bc8f6ebe084ef5.png


现在你应该可以看到 Heroku 的 config Variables 里有了 database URI。

恭喜你!你的 parse-server 已经成功地部署到了 Heroku。唯一的问题是:还没有连接到你的 iOS 应用上

定位 Parse 服务器的 URL

为了能够将你的应用连接到新的 parse-server,首先要从 Heroku 应用设置里定位托管地址(hosting url)。



178769-975a0fa5659dca4e.png


回到 index.js ,找到第 27 行,注意找 moutPath 变量是 /parse。

178769-1601201470ce9628.png


这个变量表示 parse 在 Heroku 服务器上的地址。目前来说,地址是 /parse。所以,可以在 yourapp.herokuapp.com/parse(改成你自己的域名) 中访问 parse-server。

设置 iOS 应用

现在,我们已经正确地配置和部署了服务器,是时候来设置 iOS 应用设置选项了,让 iOS 应用连接到新的 parse 服务器上。

在 Xcode 里,打开应用,选择 appdelegate.swift 文件,删除你以前的 app key 和 client key(然后写上你自己的密钥和服务器的 url)。

把下面这段代码删掉:
Parse.setApplicationId(“xxxxxxxxxxxxxxxxxxxxxxxx”, clientKey: “xxxxxxxxxxxxxxxxxxxxxxxx”)
替换成:
let config = ParseClientConfiguration(block: {
(ParseMutableClientConfiguration) -> Void in
ParseMutableClientConfiguration.applicationId = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.clientKey = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.server = "xxxxxxxxxxxxxxxxxxxxxxxx.com/parse";
});

Parse.initializeWithConfiguration(config);
完成操作后,点击 Run 按钮,测试一下应用。正常情况下应用会和迁移以前一样运行。如果你使用的YY待命,你可能需要修改一下代码,来适应新的 parse 服务器环境。我们会在下一个教程中涉及这个话题。另外,在下一个教程里,我们还会介绍在服务器里托管 Parse 的 dashboard。不过现在而言,你可以继续使用 parse.com 的 dashboard,知道官方彻底关闭服务,也就是在 2017 年的一月。

恭喜你!你已经成功地在 Heroku 上部署了 parse-server。

将 Parse 服务器部署到 AWS
注意:如果你已经将 parse-server 部署到了 Heroku 上,那么就不需要再部署到 AWS 上了,毕竟你的服务器只能使用一个云服务。这部分主要是用来参考的。如果你不想使用 Heroku,想使用 AWS,你可以继续阅读下面的章节。然而,我会假设你已经阅读过上面 Heroku 部分的教程内容,如果出现同样的设置内容,我不会再次赘述了。
Amazon Web Services(AWS)是全球知名的云服务提供商,为科技界许多知名的大型公司提供云服务。实际上,很大大型科技公司都在使用 AWS 的服务,例如苹果公司的 iCloud,Hulu,AirBnb,Lyft,Adobe,Slack (这些都是国外知名的科技公司)等等,这些只是使用 AWS 云存储服务的众多公司中一小部分。

那么,为什么我先介绍 Heroku 呢?不同于 AWS 的是,Heroku 更容易设置。对于大部分的设置,你可以直接进行无需输入账单信息。AWS 则不一样,设置方法比较复杂。为了演示如何部署到 AWS 上,我们将使用另外一个部署按钮和设置向导,来让所有的工作简单流畅。

再次强调一下,如果你已经将应用部署到了 Heroku,而且对 Heroku 的服务比较满意,你可以直接跳过这部分了。然而,如果你对如何部署到 AWS 上感兴趣,那么让我们开始吧!

第一件事,到 AWS 上注册一个 AWS 帐号,需要提供你的付款信息,这样才能使用免费方案。

完成后,点击下方的按钮,创建一个新的 AWS 应用,AWS 提供一组云服务工具,每个工具都有自己的独特的功能,在本节教程中,我们使用 Elastic Beanstalk(和 Elastic Cloud Compute Engine 或简称 EC2 紧密相关)。



687474703a2f2f64302e6177737374617469632e636f6d2f70726f647563742d6d61726b6574696e672f456c61737469632532304265616e7374616c6b2f6465706c6f792d746f2d6177732e706e67.png

什么是 Elastic Beanstalk ?
根据 Amazon 上的简介,Elastic Beanstalk 是一个易于使用的,用于部署和扩展网页应用和服务,适用的语言有 Java、.NET、PHP、Node.js、Python、Ruby、Go、Docker,例如 Apache, Nginx, Passenger,和 IIS。

好炫的语言是吧?或许吧,总而言之,我们将使用这个服务来设置和运行我们的 parse 服务器。如果你想了解更多有关 Elastic Beanstalk 的信息,请参考官方网页。
点击按钮后,会出现一个增加应用名称的界面,如下图。

178769-8682e3249bc4e858.png


下一步,确保你的设置如下图,然后继续。

178769-d9e2f39ce95712d1.png


在接下来的界面里使用正确的密钥上传 parse 设置,parse 装在 /parse 下。

178769-b450f4cfe8a4a2e5.png


恭喜你!你成功将 parse 服务器部署到了 AWS 上!剩下需要做的事情就是用适当的密钥和新的服务器 url 来设置 iOS 应用(后缀 /parse)。
结束

在本节教程中,我们深入了解了部署 parse 服务器的过程,估计现在你对部署过程已经掌握的比较牢固了。

然而,我们还留下了一些小细节没有处理(感觉这句话翻译的不对)。如果你使用的是云代码,你不得不修改代码,来保证运行正常。另外,你可能还想要一个 Parse dashboard 的替代品。幸运的是,Parse 团队已经将 dashboard 开源了,并提供了分步指南,供你更新云代码。在之后即将到来的教程中,我们会详细讨论这些内容。不过现在,你还是应用集中将应用部署到 AWS 或 Heroku 上!

你觉得本教程怎么样?请尽情地留下评论,分享你的想法。
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

  收起阅读 »

Android V2.2.8 ,ios V2.2.4release,视频通话时支持根据当前网速自动调整码率

  更新简介: 1、视频通话时支持根据当前网速自动调整码率。(android&ios) 之前的版本中视频的码率是固定的,该版本增加了自动调整的API接口,可以设置成根据当时的网络状况自动调整码率,保证视频的流畅性。 2、修复消息扩展字...
继续阅读 »
 


24958PICwjQ_1024.jpg


更新简介:


1、视频通话时支持根据当前网速自动调整码率。(android&ios)

之前的版本中视频的码率是固定的,该版本增加了自动调整的API接口,可以设置成根据当时的网络状况自动调整码率,保证视频的流畅性。

2、修复消息扩展字段里包含特殊字符,接收方kill程序再进的时候消息不显示的bug(android)

3、修复一个登录相关的问题(android&ios)

4、修复一些ui相关的问题,如发送视频消息时,视频时长显示不对(android)

5、修复收到异常消息crash问题。(ios)

rest发的消息中value中的value为null的话之前会crash,现在是忽略掉这这种消息。


 
版本历史:ios更新日志 Android更新日志
下载地址:SDK下载
 
SDK接入过程中有任何问题建议请直接回复跟帖。 收起阅读 »

4.5亿用户,环信助力快牙打造跨平台传输神器

快牙全球用户已超过4.5亿,遍及全球200多个国家和地区,被用户称为“怪兽级传输神器”,快牙的创建连接群和好友即时聊天功能都是集成环信的服务。快牙CEO王晓东表示:“在互联网的发展下,还需要把快牙的社交性发挥出来,我们不是做一个社交产品,而是用社交互动来助推快...
继续阅读 »
快牙全球用户已超过4.5亿,遍及全球200多个国家和地区,被用户称为“怪兽级传输神器”,快牙的创建连接群和好友即时聊天功能都是集成环信的服务。快牙CEO王晓东表示:“在互联网的发展下,还需要把快牙的社交性发挥出来,我们不是做一个社交产品,而是用社交互动来助推快牙的传播,使得我们的核心数据大涨。”截止2015年,快牙月活跃用户8000万,日传输超1.2亿次,以绝对优势位居内容传输APP之首,这其中通过集成环信即时通讯云服务提高APP社交活跃度功不可没。



28期_行业新闻.jpg




快牙简介
 
快牙是众多快速发展、跨平台的无线文件传输工具之一。它传输文件的速度远超蓝牙、NFC和Airdrop,让用户无需使用移动网络流量时即可分享apps、照片、音乐、视频或者其他任意一种文件。除了作为WI-FI文件传输工具外,快牙还提供可以本地安装的网络游戏供用户娱乐。
 
快牙的工作原理 
 
快牙可以让任何内容(包括apps)从一个手机直接复制到另一个手机。但是,如果你朋友还未安装快牙,快牙也提供了简便的方式让你朋友直接从你的手机上把其复制过去,所以你能在任何情况下与你的朋友们分享任何文件。用一句话来说,如果你的朋友向你推荐了一款app而你并不想搜索整个app商店去下载,你只要直接使用快牙就能马上安装这个app。 
 
快牙背后的故事
 
快牙起家于2011年,刚开始是为各个vendors在安卓API中提供无线热点功能的定制化开发。由于它不需要占用移动流量也不需要WI-FI网络连接就能进行大文件(包括图片和电影)分享,快牙迅速在学生及其他负担不起4G网络和大流量套餐的人群中流行开来。同时,其也在运营商信号不稳定的地方极其流行,比如说:中国、东南亚、中东和印度。它在发展中国家增长的速度最快,在想要花尽量少的钱又可以传输文件的劳动人群和学生人群里面最为流行。
 
快牙的进化和挑战:
 
使用环信之前 在刚开始,快牙就在寻找可以增长用户和让他们的用户更有参与感的方式。但他们发现一但学生或者工人身份的用户离开了他们群组的物理地点,对快牙的使用量就马上降低了。此外,文件分享也不是一个每日都会被使用到的工具,这也是快牙用户增长面临的难题之一。但是,快牙通过观察开始注意到这个app其实有着社交的因子,并且发现如果给他们的app加一层社交网络的元素会让app的使用体验更丰富。这也是他们首次开始思考给app增加即时通讯功能来服务他们的用户。同时,快牙还想在未来打造以兴趣为主题的用户群组。
 
快牙还发现在2014年底,他们现有的2亿用户已经给他们的后端系统带来了压力。例如在周末时间段,大量用户分享图片时,数据有达到峰值的趋势。所以,他们开始寻找更好的即时通讯解决办法来助力于他们的高速增长和足以应付突发的流量峰值。很多游戏公司告诉他们,应该要自主开发一套即时通讯功能供app使用,但他们不希望自主开发即时通讯系统使开发团队的重心从他们的核心功能上转移。在他们开始探索可能的解决方案时,最终发现如果执意要自主开发一个能够支持大流量、可扩展、高性价比的即时通讯系统将会是一个十分艰难的挑战。 
 
在估算成本投入时,快牙发现如果他们的用户增长与流量继续按现在这种爆发式增长时,成本的投入将不可负担。在测试使用自主开发的即时通讯系统时发现,如果想要在每个服务器上满足接近一百万用户的连接量时,需要花费几乎不可承担的极高成本。
 
 如果开发一个能满足其流量需求的系统,除了非常昂贵外,还将面临极大的系统风险。因为如果其中一个服务器瘫痪了,将由于资源不足无法导流而使服务中断。如果要消除这种隐患,必须使用更多的服务器,以降低每台服务器的工作负载。然而,这将使系统成本和复杂性急剧增加。 
 
因此,快牙需要一个能处理峰值流量,具备扩展性能,同时拥有低成本及容错性的第三方即时通讯解决方案。
 
快牙的选择和收获:使用环信后 
在对比自主研发和其他厂商的解决方案后,快牙选择了环信。环信在技术和成本上提供了最佳解决方案,同时还拥有一支行动快速、反馈及时的技术团队,能让快牙在最短时间内运行在高容错性、可扩展、低成本的企业级系统上。 
 
如果当时没有环信,快牙将浪费很多时间、金钱,并且将使产品研发重心转移。此外,环信的IM长连接技术使快牙能够通过发送一些专业市场营销内容,大幅提升日活用户数。
 
把复杂的大规模即时通讯移动端优化问题交由环信专家解决后,快牙就能专注于产品和app的快速迭代。快牙现在每日能处理1.2亿并发的消息并且还在持续增长。与优秀的即时通讯厂商合作,使他们对于快速扩张没有任何顾虑。


快牙的挑战 
减轻后端的数据存储负担 
创建客户通讯能力
快速提升并发连接能力 
控制成本的增长 
系统具备容错能力 
产品研发仍聚焦快牙核心竞争力
快速部署 
解决方案: 
环信提供的即时通讯云服务 
具备1000台服务器规模的IM 
系统部署能力 
IM长连接让快牙能与用户主动沟通 
扩展性—按需提供,弹性扩容 
数据在服务器之间平均分配,更具容错性 
即时通讯和后端扩展交由环信,快牙专注于自身产品 
三个月即可就绪的企业级解决方案 


 结果: 
 
快牙成为中国排名第一的工具类App--提供本地文件分享与游戏功能(BT和WIFI)。截止2015年,快牙月活跃用户8000万,日传输超1.2亿次。期间共计54个迭代版本,新增改进功能累计270余个。近期随着快牙4.0的发布,将开启一场自我颠覆的革命。新快牙,是分享,是朋友们发现和获取私密内容及猛料的服务,相信环信即时通讯云服务也将发挥更大的价值!
  收起阅读 »

内容为王 一个好的摘客

“这是一篇不完全的摘客产品报告。” 如何发现互联网的内容时代来了? “我是papi酱,一个集美貌与才华于一身的女子。” 没有看过papi酱的视频,那你也一定听过papi酱的段子,这就是新一代的网红papi酱...
继续阅读 »
“这是一篇不完全的摘客产品报告。”




如何发现互联网的内容时代来了?

“我是papi酱,一个集美貌与才华于一身的女子。”



图片1.png




没有看过papi酱的视频,那你也一定听过papi酱的段子,这就是新一代的网红papi酱,从秒拍开始,逐渐将短视频带领进新时代的网红。


360的总裁齐向东主导的新闻团队即将从360公司脱离出来,成立一个新的公司,专注打造新媒体产品“北京时间”;新一代网红Papi酱拿到了1200万人民币投资,罗辑思维公布了其与Papi酱的具体合作,即拍卖Papi酱视频贴片广告一次,并由罗辑思维全程策划监制服务,掀起内容运营的蓝海;爱奇艺《奇葩说2》首播破4000万,更是引领纯网内容进入黄金时代。

互联网正在越来越成为一个内容创业的平台。做好内容,产品才能有市场。




产品定位

正如应用商店所写的介绍那样,摘客是“个性化资讯推荐”软件。摘客的主要功能是阅读,却又不只是一个简单的阅读平台,应该说摘客是一个包含了推荐、分享、交流等功能的更加懂你的推荐阅读平台。



需求分析

摘客作为一款深度个性化定制的资讯阅读应用,整合了各大门户网站、博客、论坛的资讯,以杂志式的排版方式进行内容再现。做好内容,满足用户阅读的需求,这是摘客产品的初衷。

用户群体是广大对IT资讯感兴趣的读者。他们需要能够找到更多自己感兴趣的文章,获得有趣的阅读体验或者是学习专业知识。



用户真的需要个性化推荐?

网易云音乐的个性化推荐在推荐算法领域风生水起。推荐算法并不能保证推出的东西一定得到该用户的喜爱,但至少能保证推荐出的一些东西一定有用户喜爱的。这其实是一个快速筛选的过程,在互联网阅读碎片化且快节奏的环境下是非常凑效的。

大多数的资讯类产品都有“订阅”的功能,而个性化推荐其实正是订阅的一个隐形形式。所以摘客最主打的部分就是个性化推荐,同样作为摘客产品一个亮点的是热点推荐。




图片2.png



摘客APP首页


与竞争产品的优势?

摘客与网易新闻、新浪新闻的关系应该就像店铺与淘宝的关系。摘客将新闻像商品一样上架对其进行统一格式的整理,分类,最后尽量精确的分发给用户。各类知名网站就是我们商品的货源。

与摘客最相似的两个竞争产品应该是今日头条和推酷。大家都是做内容聚合的平台,且都“不生产内容,只做内容的搬运工”。这个竞争的问题留给摘客的挑战是什么样的呢?

答案是:再做一次有选择的聚合。摘客的重点还是归于“摘”,直接从新闻来源控制质量,再经过每一轮的算法筛选,以留下最精准和优质的文章为目标。相对于今日头条,摘客做更细分的领域;相对于推酷,摘客做更广的领域。




图片3.png



今日头条app首页



图片4.png



推酷市场介绍


摘客的价值在于? 
 
既然作为一个产品就要有价值的体现,而资讯的价值就是摘客的价值。资讯的价值在于传播和学习,摘客作为一个聚合阅读的平台希望给用户正真“干”的东西。未来的摘客有很多可能,可能会侧重发掘展示UGC(用户原创)的内容,或者会借势网红经济的风口再成长?都是摘客作为内容型产品可能在思考的。 
 
更长远来说,资讯本身就是一种商品,所以内容收费未来可能成为趋势,借鉴知乎最新的产品“值乎”。摘客的未来可能走内容收费模式,做高端定制内容需求,其商业模式的想象空间仍然很大。 最后说说这个团队 最后来说说这个产品的制作团队,前端后台一共10个人不到,都是研究型的人才。最开始做的是大数据分析系统,之后也做一些舆情分析,最后发现已经储藏了很多,于是摘客也就诞生了。 
 
因为摘客中的好多技术其实在做旧的项目时都已经有雏形,有些甚至直接是研发人员学位研究课题,正好借摘客的平台付诸实践和尝试,所以摘客也是新思维碰撞的产物。 需要说明的是,产品的更新迭代一直没有停止,摘客也是有很大改进空间的,作为一个用户,最希望的就是web2.0,也就是用户原创内容,在摘客有展示平台就更好了。 
 
最后再说一下,摘客的制作团队是网新恒天的HTA项目组,有兴趣可以来参观交流,可借鉴内容很多~

  收起阅读 »

【公告】环信直播课堂内容、界面全面升级--本期暂停直播

环信直播课堂创建于2016-02-19,迄今为止,已经陪伴了我们走过九期,历史收看人数6191。 非常感谢为我们直播讲解的每一位老师,@beyond @zhuhy @一鸣 @FUccc @shenc @东海 同事也感谢小伙伴们一直以来的陪伴!   ...
继续阅读 »


47z58PICmkp_1024.jpg


环信直播课堂创建于2016-02-19,迄今为止,已经陪伴了我们走过九期,历史收看人数6191。

非常感谢为我们直播讲解的每一位老师,@beyond @zhuhy @一鸣 @FUccc @shenc @东海
同事也感谢小伙伴们一直以来的陪伴!
 
在历经9期的直播里,很高兴服务了那么多小伙伴,同时,也看到了很多我们的不足。

从4月份起,就一直在筹备环信直播课堂的升级,我们新版本将集投票、点播、订阅、聊天于一体,打造最人性化的环信直播课堂(环信小伙伴也可以申请当主播哦!)
 
暂停一期环信直播课堂,下次上线时间待通知。
 
您可以把对环信直播课堂的建议和想听的内容直接跟帖回复。
 
THX!
  收起阅读 »

13个开源项目集体登场,这是一场开源的技术盛宴!

环信编程大赛颁奖典礼将于5月14日在中关村鼎好大厦二楼义创空间召开。届时,基于环信即时通讯云开发的13个优秀开源项目将集体亮相颁奖典礼。各种亮瞎眼的创意,总有一款能够打动你。感受环信开发者的力量,欢迎广大移动互联网开源开发者,投资人参与!   本次活动,我...
继续阅读 »
环信编程大赛颁奖典礼将于5月14日在中关村鼎好大厦二楼义创空间召开。届时,基于环信即时通讯云开发的13个优秀开源项目将集体亮相颁奖典礼。各种亮瞎眼的创意,总有一款能够打动你。感受环信开发者的力量,欢迎广大移动互联网开源开发者,投资人参与!
 
本次活动,我们邀请到了来自业内的开源大牛,分享他们对开源的见解,一同探讨开源项目的未来。

这里没有广告和套路只有满满的干货!有的是开源碰撞出来的火花,有的是对于开源美好未来的无限畅想,有的是环信对于开源理念的践行,还有一群志同道合的小伙伴!




27期_本周头条.jpg



开源项目介绍:




27期_本周头条2.jpg



宅不住:发现城市精彩运动,认识周边潮人

宅男福利:一款美女直播应用
 
图忆:基于位置信息的分享与社交应用

咚咚:一款高效团队沟通的移动客户端

Cloud Developer:程序员之间互相交流学习的平台

美肤GO:专注于个人海外代购及护肤咨询分享的APP

文播:一款文章直播平台性的APP

方圆十里:关注方圆十里内的人和事

薅羊毛:针对技术人员的社交软件

高仿微信:基于环信SDK高仿微信

他乡:发现老乡,联络老乡,老乡互助的平台

公众号助手:未认证的公众号与用户之间的便捷联系

致敬传奇:怀恋偶像kobe



报名来到现场,共聚一堂,与开源大牛们面对面交流,

 
报名来到现场:http://www.easemob.com/event/hackathon_party/ 收起阅读 »

互联网周刊:环信——以朴素的技术坚守改变世界的价值

企业级云服务市场的巨大潜力,不仅吸引BAT级别的互联网企业深入布局,还驱动诞生一大批充满活力的创业公司,例如成立仅两年的全通讯能力云服务提供商环信,去年已占据国内SaaS移动端客服市场77.4%的份额(易观智库《2015中国SaaS客服市场专题研究报告》)。这...
继续阅读 »
企业级云服务市场的巨大潜力,不仅吸引BAT级别的互联网企业深入布局,还驱动诞生一大批充满活力的创业公司,例如成立仅两年的全通讯能力云服务提供商环信,去年已占据国内SaaS移动端客服市场77.4%的份额(易观智库《2015中国SaaS客服市场专题研究报告》)。这一成绩背后凝聚着环信对云服务市场怎样的理解,环信究竟有哪些核心竞争力?带着这些疑问,我们与环信CEO刘俊彦进行了交流,希望能找到他或环信独有的价值,以飨读者。



9885f03a-f5a4-4020-9372-ae1a45fd6087.jpg


市场规模将实现从60亿美金到3000亿美金的跳跃




b50ecd73-ac65-4e9b-abe7-3f855aefc797.jpg


《互联网周刊》专访环信CEO刘俊彦


企业级信息消费是新的系统性机会。

“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服务市场打开了,这是最大的区别之一。”谈及中美企业级服务市场差异的时候,刘俊彦说道。在消费级市场上,中美前三名企业的市值总量均在千亿美金规模,处于同一数量级,而在企业级服务市场,中美之间则存在巨大的鸿沟。美国Oracle、微软、SAP等三家公司加起来市值大约3000亿美金,中国用友、金蝶加起来大约60亿美金。与此同时,中美市场的企业数量均在2000多万家,因此可见中国企业级服务市场的潜力巨大。

“目前在IT领域,中国存在重要的系统性机会,当一个国家两千万中小企业开始进行IT能力消费的时候,市场规模将实现从60亿美金到3000亿美金的跳跃。在系统性机会中,谁能继续淘金是很关键的问题。”

中国企业级服务市场的系统性机会不止于此。美国企业级服务市场发展已经趋于成熟,现在如果想进入客服软件等行业基本没有太多机会,相应的中国市场则尚是一片蓝海,甚至竞争者有些荒芜。若具备强执行力、先发优势和资本支持,创业公司在这个新兴产业脱颖而出相对较容易,并且有可能成为小巨头。到此还远远没有结束。美国市场已经出现微软、Oracle、SAP等千亿美金的综合性软件公司,中国则不然。基于中国市场的规模和前景,垂直行业的小巨头通过横向扩张完全有机会成长为综合性软件巨头,媲美微软、Oracle等。“中国在1~2年内会出现一批垂直领域百亿人民币的公司,3~5年内会出现第一批千亿人民币的公司,出现中国自己的综合性软件巨头。”刘俊彦期待道。

中国企业级SaaS服务已然呈现出爆发式发展

在基础服务市场格局成型的情况下,垂直领域的先发优势是创业公司的竞争力所在。

综观国内外,作为万亿级的超级大市场,云服务市场很难出现一家独大的局面,而是有着比较明显的划界和分化的趋势。最底层IaaS层是整个云计算的基础,为企业提供基础的计算能力和计算架构。国外最具代表性的有AWS、微软等,国内阿里云、UCloud、青云等竞相崛起。基于IaaS层,众多企业搭建云计算的能力才得以实现,它是整个云计算生态圈的支撑。第二层是PaaS层,目前发展已相对成熟,出现明显的巨头化和分化趋势。IaaS厂商做到一定程度以后,可能不满足只做IaaS,而是往核心PaaS层面延伸。不管是AWS、亚马逊,还是国内的互联网巨头,均积极布局核心PaaS能力,包括大数据、人工智能、通信能力等。第三层则是SaaS层,经过2015年SaaS元年,中国企业级SaaS服务已然呈现出爆发式发展,绝大多数的创业企业和创业方向涌向SaaS领域,可谓百花齐放。与美国数千家SaaS企业在垂直领域蓬勃发展相比,中国SaaS市场尚处于初级阶段。

“就目前的中国市场而言,如果不具备先发优势,在IaaS层和PaaS层将很难和巨头竞争,但SaaS层因为和行业、业务贴合紧密,有天然的壁垒和深度,非常适合创业公司介入。”刘俊彦表示,尽管如此“在IaaS和PaaS层创业公司也不是绝对没有机会,巨头之外仍有一些做得很好的创业公司。这个世界永远不可能一家独统天下。”

人工智能聊天正成为新的入口和蓝海

人工智能和大数据技术将彻底改变整个软件行业,这是一个技术足以改变世界的时代。

阿尔法Go和人类的围棋大战将人工智能市场的关注度推上了高潮,在云服务市场,人工智能发展尚未普及和成熟。值得注意的是App市场已经趋于饱和,人工智能聊天正成为新的入口和蓝海。

“人工智能和大数据技术将彻底改变整个软件行业。”当被问及人工智能将为云服务市场带来哪些革命性影响时,刘俊彦坚定地答道。

环信已推出全媒体智能客服,通过完全自主或人机混合模式的智能机器人技术极大降低人工客服工作量。在这个市场若想实现可持续发展,一定不是陷入同质化产品的无谓竞争,而是基础IT能力的实力较量。一针见血。这个技术出身、目前仍自诩程序员的创业者,洞察市场和未来的时候却不失犀利。

领导层的战略眼光和志向所在往往决定着一个企业的成长和命运,这一点在刘俊彦和环信上有着充分的体现。
一个成立仅两年左右的企业,能够在高手如林的市场迅速成长为驰骋的黑马,多少让人在敬佩之余想一探究竟。在这样一个互联网巨头虎视眈眈的市场,尽管现在看来一片蓝海,却丝毫不可松懈,而是要时刻保持敏锐的市场嗅觉和果断抉择的气魄。从交谈期间刘俊彦多次提及机会、风口和速度或许可见一斑,更显著地渗透在环信成长之路的点点滴滴。在发布即时通信产品仅4个月的时候,环信正式决定进军SaaS方向,作为当时在PaaS行业尚未做深做透的初创公司,其魄力已初步显现, “环信天然在早期接触到了用户的真实需求,结合PaaS方向的价值积累和战略思考,决定进军SaaS行业。”

这个决定被事实证明是正确的,并且得益于PaaS领域的积累和导流以及先进的技术实力,环信很快建立起强大的竞争壁垒,成为SaaS客服软件行业唯一一家同时拥有PaaS和SaaS产品的公司。在国内SaaS新兴增量市场,要做好全媒体智能客服软件,即时通信技术必不可少,从PaaS到SaaS的拓展水到渠成。选择做正确的事,而不仅仅是正确地做事,不仅仅是眼光,更是智慧。

技术可以改变世界的前提,是人文思想

不要被短期的利益迷惑,忘记自己的身份和目标。

鉴于企业级服务千差万别,如何平衡定制化和标准化成为SaaS行业不可忽视的问题。对此,刘俊彦表示任何SaaS企业都需要分三步走,第一要从小做起,为小企业提供优质的标准化服务,第二有一定积累后再开始接触中大型企业,带动一些可定制模块的开发,第三是做PaaS平台,团结生态圈的企业,平台与生态圈企业合作共赢,满足各种定制化需求。谈及此,刘俊彦语重心长:“到底是做外包公司,还是做SaaS的产品公司,这一点好多公司都迷失了。千万不要被钱迷惑了。”在这样的坚守下,环信目前已经开始进入PaaS平台化阶段。

谈及创业初衷,他说:“我们还是比较‘朴素’,我们想让好的技术造福更多的人,改造中国社会,帮助中国社会更好地进步。”环信的几个创始人曾在大型外企研发中心做了多年的技术,被刘俊彦形容为“跨国公司的螺丝钉”,当时他们有很多好技术和产品跟公司大方向不吻合就被湮没了。“中国有一批非常优秀的技术人才,不应该只做跨国公司的螺丝钉。适逢创业大潮,以技术为驱动的创业机会越来越多。”

“我们希望环信能以身作则,为开源世界贡献一些有价值的代码,并且我们还在做自己的社区,尽量以开源的形式将一些好的技术反馈给社区,通过分享带来更广泛的进步,这是我们一直的愿景。现在回过头来看,当6万个App用我们的技术和产品,有3亿部手机用我们的服务每天发几亿条消息的时候,某种程度上我真的觉得我们帮助了创业者、改造了中国社会,还是比较有成就感的。”

一个企业的核心价值在于通过朴素的技术壁垒坚守足以改变世界的独特价值,无可替代

优秀的SaaS公司一定是全球化公司,未来中国将向全球输出数字商品。

环信对标的对象是微软、甲骨文这类国际性公司。中国作为制造大国,曾一度沦为全球制造和外包中心,以向全球输出工业制造品为主。近年来随着科技的发展,中国在互联网领域逐步与国际接轨甚至在某些细分领域遥遥领先,这样的转变无疑将带来中国输出能力的质的提升。现在猎豹、大疆等公司已经向海外输出移动APP工具、智能硬件等产品,而未来十年中国将会向全球输出数字商品,SaaS作为天然适合全球化的产品,将成为中国向全球输出数字化商品的重要领域。

“一个优秀的SaaS公司一定是全球化的公司。”在刘俊彦看来,这是最基本的理念。 收起阅读 »

云中黑客松

云中黑客松的颁奖典礼将在6月1日微软CEO Satya来北京的开发者活动日中进行。将会进行直播和获得媒体的最高关注。 报名时间  2016/04/15 00:00 — 2016/05/15 23:59 活动时间  2016/04/15 00:00 — 201...
继续阅读 »
云中黑客松的颁奖典礼将在6月1日微软CEO Satya来北京的开发者活动日中进行。将会进行直播和获得媒体的最高关注。


报名时间 
2016/04/15 00:00 — 2016/05/15 23:59
活动时间 
2016/04/15 00:00 — 2016/05/31 00:00
活动地址 
中华人民共和国
报名人数
无限 (27人已报名)


 
什么是云中黑客松?


2016微软“云中黑客松”即将启程!

此次在线黑客马拉松大赛身披“微软智能云”全新而来,时长一月,高能不断!微软智能云Azure,作为一个集混合云和SaaS服务的云平台,已成为企业发展、行业创新的利器。参加“云中黑客松”大赛,您将近距离感受微软智能云带给你的开源、开放体验,挖掘云上创业的智能、创新潜力!

云资源短缺 ?云技术匮乏?”云上黑客松“将为您一一扫清障碍。加入我们并秀出您的作品,微软助您迅速优化产品、精准定位市场、踏上干霄凌云之旅!

如果您拥有够硬的产品、够高潜质的团队,微软完善的生态服务系统和云生态、“微软创投加速器”将为您提供“人,财,策略,市场拓展”等全方位优质创投服务。 
 


谁可以参加?


 独立软件开发商ISV,包括:企业,中小微公司和创业团队
个人开发者只要您对云技术兴趣够浓、对产品信心够足,“云中黑客松”就是您的舞台!



黑客松日程



tDmv2ay.jpg



参赛流程


  1.  每位选手登录开放黑客松(hacking.kaiyuanshe.cn)平台, 进入“云中黑客松”,点击 “我要报名“;
  2. 每位选手报名之后,即刻就可在开放黑客松后台选择线上开发环境(Linux or Windows)进行体验或开发;
  3. 需要申请免费微软智能Azure云账号的团队,请在报名三日之内提交您的开发计划书(模板下载)。步骤:我的团队-> 下载开发计划书模板-> 填写并上传;
  4. 组委会将联系已提交开发计划书的团队,被通知的团队请进行实名认证,将实名认证的(姓名,联系电话,邮箱)发回给组委会邮箱(a-mali@microsoft.com),符合要求的团队将获得免费微软智能Azure云账号;
  5. 所有参赛团队或个人请上传你们的作品及其附属文档:项目计划书,功能描述文档, 幻灯片, 视频等, 作品展示部署链接。


奖项设置




V6a71sp.jpg


所有参赛团队均有机会获得:免费微软智能云Azure试用账号(价值1,500人民币);
微软创投加速器八期现已开放申请,优胜团队将进入招募绿色通道,直接与微软创投加速器团队交流。


可以开发什么?


基于微软智能Azure云开发或移植App,服务, SaaS应用等。

大赛不限云上开发主题或方向,尽情展现您的作品和创意。也可参考以下开发场景:

  • 认知服务(牛津计划 + 更多智能APIs)注册获取免费API
  • 大数据
  • 开放物联网 获得技术支持
  • Docker容器
  • 混合云
  • 其他
另外, 黑客松平台上将为每位参赛者提供一个云上的Linux或Windows虚拟开发环境, 让您秒级开启云上开发之旅。

评审标准

[list=1]

  • 您的项目必须是基于微软智能云Azure开发
  • 评选标准会考虑以下方面:商业, 产品,创新,技术等方面综合考虑
  • 提交物:
  • 前期
    • 报名完成提交《开发计划书》,初审通过之后会获得免费微软智能Azure云账号
    终期
    • 必选: 3分钟作品介绍视频
    • 必选: 幻灯片
    • 必选: 作品部署展示的链接
    • 可选: 文档说明
    • 可选: 代码 (提供一个代码托管地址) 


    版权说明


    作品版权属于作者,但主办方有权在文章或者PR宣传中使用您的作品。


    联系我们


    联系电话:021-61885153 
    联系邮箱:a-mali@microsoft.com
    QQ交流群:522180538 (Azure云中黑客松)


    qiIKE6g.png



    收起阅读 »

    2.x iOS SDK退出接口的isUnbind是什么意思?

    解除deviceToken绑定。   当您的APP进程被杀死的时候,环信是通过APNs机制给您发消息提醒的。 所以当您APP启动的时候,我们会把您的deviceToken传到环信服务器,我们称之为绑定deviceToken。 当您退出登录的时候,不需要再接收A...
    继续阅读 »
    解除deviceToken绑定。
     
    当您的APP进程被杀死的时候,环信是通过APNs机制给您发消息提醒的。
    所以当您APP启动的时候,我们会把您的deviceToken传到环信服务器,我们称之为绑定deviceToken。
    当您退出登录的时候,不需要再接收APNs了,就需要解除绑定,这个时候,需要您在调用退出函数时,将isUnbind设置为YES。
     
    什么情况可以设置为NO?
     
    1、 如果您当前的账号在其他设备登陆了,在它登陆的时候,就会把它的deviceToken绑定。所以这个时候,您不需要解绑,可以传NO。
    2、如果您是立刻要登陆新号的时候。如果您退出后立刻要登陆新的账号,可以传NO,因为你在登陆新账号的时候,环信会自动帮您结束之前的绑定关系。 收起阅读 »

    环信直播课堂第九期--2.xSDK项目平滑升级3.xSDK

    日期与时间:2016年4月28日15:00 持续时间:半小时 描述:环信发布3.xSDK已经有一段时间了,之前使用的2.x,该不该升级3.X,又该如何升级,其中会遇到什么问题,有什么需要注意的地方? 本期环信直播课堂将由环信IOS工程师shenc给大家详...
    继续阅读 »
    日期与时间:2016年4月28日15:00

    持续时间:半小时

    描述:环信发布3.xSDK已经有一段时间了,之前使用的2.x,该不该升级3.X,又该如何升级,其中会遇到什么问题,有什么需要注意的地方?

    本期环信直播课堂将由环信IOS工程师shenc给大家详细讲解2.xSDK项目平滑升级3.xSDK

    直播观看地址: http://www.imgeek.org/video/15 
     
    视频回放稍后会上传到视频模板http://www.imgeek.org/video/
     
    喜欢环信直播课堂,每周四下午三点老地方我们不见不散http://www.imgeek.org/video/15 收起阅读 »

    iOS集成环信3.0总结

     最近刚做完一个有即时通讯功能的APP , 之前我集成过环信2.0+3.0的UI ,这次想直接用一下环信3.0 , 不出所料 , 集成过程中还是出现很多困难 , 所以单单集成的过程 , 也需要捣腾个一天 ,当然也要感谢环信技术支持 , 虽然大多数时候他会直接甩...
    继续阅读 »
     最近刚做完一个有即时通讯功能的APP , 之前我集成过环信2.0+3.0的UI ,这次想直接用一下环信3.0 , 不出所料 , 集成过程中还是出现很多困难 , 所以单单集成的过程 , 也需要捣腾个一天 ,当然也要感谢环信技术支持 , 虽然大多数时候他会直接甩我一句:"自行百度就行" ,还有可以去https://www.imgeek.org/这个网站提问题 , 最好邀请一些经常回答问题的大神

    下面说正事 :

    一般用这些SDK , 第一件事就是看文档 , 但是因为环信框架大 , 文档里面说的不会太详细, 我按照文档里面的集成方法试了几次都不行 , 所以我们可以直接参考Demo ,首先看看效果:​我这里展示的只是会话和加好友,联系人功能哦



    005TbQ0Qgy70z6EnFDQ12.gif


    Demo运行 


    我下载的是IM2.x,里面包含​​2.0Demo , 3.0的Demo , 单独的EaseUI等等(还有红包功能哦)



    005TbQ0Qgy70z71TCZ99f.png


    IM2.x


    第一步:导入所需文件(因为文件里面有的会包括依赖库,所以我们先导入文件)

    1.导入SDK

    我是参照IM2.x里面的3.0Demo来导文件的 ,​ 所以我导入的SDK是EaseMobSDK(直接把整个SDK拉进去) ,而(IM3.x)的SDK是libHyphenateSDK

    注意:3.0Demo里面的EaseMobSDK的lib这个文件夹有两个 , 一个是libEaseMobClientSDK.a(语音) 一个是libEaseMobClientSDKLite.a(无语音)这两个同时存在的话会有冲突的,将你不需要的一个删了就行了 , 



    005TbQ0Qgy70z8UFukw68.png


    我用的是带语音的libEaseMobClientSDK.a


    ​拉完SDK之后要在Building Settilngs 里面的 Otherlinking 里面设置一下,如果你选之前选择了libEaseMobClientSDK.a , 就输入-ObjC ,此 -ObjC是配合libEaseMobClientSDK.a使用的,如果你之前选择了libEaseMobClientSDKLite.a, 就输入-force_load ,-force_load加静态库路径是配合libEaseMobClientSDKLite.a使用的



    005TbQ0Qgy70z91pvNv61.png


    设置​


    详细参考官方文档:

      2.导入EaseUI文件

    拖入EasyUI工程下的EaseUI文件夹、EaseUIResource里面的Resource文件夹、export文件夹里面的resources文件下的EaseUIResource.bundle​



    005TbQ0Qgy70z9hDM9u84.png



    3.导入 CahtDemo-UI3.0下的文件 

    注意:Class里面包含有AppDelegate文件,可以选择删除自己的AppDelegate文件 ,亦可以把里面的你需要代码复制到你的AppDelegate中



    005TbQ0Qgy70z9vDMS22e.png



     第一步所有的文件已导入了 , 如果你手痒可以试着运行下 ,但是肯定会报各种各样的错


    第二步:​ 我们需要一个PCH预编译头文件来全局引用某些文件

    你可以新建一个pch文件,确保路径正确,在pch文件里面添加EaseUI-Prefix.pch(里面导入的文件如果和ChatDemo-UI3.0-Prefix.pch导入的重复了,可以直接删了这个文件)、ChatDemo-UI3.0-Prefix.pch这两个文件里面的代码​, 如果你项目中不需要别的pch文件,你可以直接导入ChatDemo-UI3.0-Prefix.pch的路径就行了(因为这是一个Demo ,所以我是直接导入ChatDemo-UI3.0-Prefix.pch)

    ​pch文件导入的路径最好加上$(SRCROOT) , 在Building Settilngs 里面的 prefix header 里面设置, 详细的自行百度



    005TbQ0Qgy70zabjET791.png


    设置pch文件的路径


    第三步,导入依赖库

    现在开始导入依赖库 , 这是你会发现Linker Frameworks andLibraries里面已经有几个依赖库了 , 检查一下 , 把重复的先删了 ,如果里面同时发现了libEaseMobClientSDK.a和 EaseMobClientSDKLite.a ,就说明你第一步的步骤没完成 ,正常来说里面只有其中一个.a文件

    一开始 , 我按照3.0文档的依赖库添加 , 后来报这个错误



    4d63c3822c58c6146b1e103c726053ed.gif


    报错


    后来我核对了一下 ,  发现跟文档中导入的依赖库一样啊 , 后来我就去骚扰他们的客服 ,他让我再加一个依赖库libiconv.tbd(但是官方文档根本没有提到要加入这个库,坑!)

    我是直接参照Demo里面添加的依赖库添加的 , 这样比较安全一点 ,很多错误都是由于少导入某一两个依赖库造成的 , 再次检查需要耗时间 , 所以我直接全部添加​



    005TbQ0Qgy70zlDE41Kcc.png



    如果你遇到这个报错 , 里面带有parse twitter字眼的 ,大多数都是缺少导入parse的依赖库: 
    StoreKit.framework
    Bolts.framework
    Parse.framework
    Accounts.framework
    Social.framework
     



    005TbQ0Qgy70zlKHatae0.jpg



    步骤已经说完了, 如果现在运行当然还是会遇到很多报错.

    现在主要来总结一下报错原因:​

    如果报的错是 linkercommand failed with exit code 1 (use -v to seeinvocation)

    ​不能只看红色报错的哪一行 , 要看看上面的内容有什么比较明显的字眼​

    1. 如果有报错说的是关于BackupViewController的 ,直接把文件删除就好,这个文件是没用的



    005TbQ0Qgy70zma8NBA3e.png


    1


    2.​如果看到报错上面开头是duplicate的 , 说明你重复导入了某些文件 ,再看看字眼里面如果有VoiceConvert,  MBProgressHUD ,  在全局搜索里面搜出来, 把其中的一个删掉就行了 , 一般来说我是删除EaseUI里面的VoiceConvert文件夹和三方里面的MBProgressHUD 



    005TbQ0Qgy70zmd9gvK78.png


    2


    3.如果你项目里面用到MJRefresh , SDWebImage什么的起冲突了 是因为环信里面也有用到这些三方 ,删掉环信的就行了

    ​本篇集成文章由环信热心用户Scorpion_ZJ 发表在个人博客,博客地址Scorpion_ZJ 收起阅读 »

    从产品上网到服务上网 ——传统商业转型电商的客服升级之路

    垂直类电商无疑是当前互联网经济的耀眼明星。即使是在当前人人喊冷的资本寒冬中,找钢网、找塑料网这类垂直电商仍然被资本市场所青睐。今年1月,找钢网宣布完成E轮战略融资,融资额超10亿元人民币。资本市场真金白银投入的背后,是以“互联网+”、“分享经济”、“供给测改革...
    继续阅读 »
    垂直类电商无疑是当前互联网经济的耀眼明星。即使是在当前人人喊冷的资本寒冬中,找钢网、找塑料网这类垂直电商仍然被资本市场所青睐。今年1月,找钢网宣布完成E轮战略融资,融资额超10亿元人民币。资本市场真金白银投入的背后,是以“互联网+”、“分享经济”、“供给测改革”为导向的整体经济转型。可以预见,随着以天猫、京东为代表的互联网原生电商平台的流量饱和,以传统行业为主角的垂直类电商正坐在开往春天的列车上,成为电商行业“新常态”。

    从产品上网到服务上网

    传统行业触网电子商务,要解决的第一个问题,便是平台搭建。对垂直类电商而言,贯通上下游的供应链管理是运营重点,而天猫、京东这类以流量运营为主的综合电商平台显然不适合。因此自己搭建独立网店或交易平台,成为垂直类电商的首选。目前,国内提供电商平台解决方案的服务商已为数不少。例如,APP端有有赞、微店、微盟萌店、微猫,PC端有Shopex、ECShop、Shop++、Javashop、千米、筑云等等,这些厂商的模块化解决方案帮助传统行业快速实现了产品上网和在线交易,完成了“触网”的第一步。

    但我们看到,很多垂直电商虽然实现了“产品”上网,“服务”却迟迟没有上网。由于存在技术门槛,当前电商平台解决方案中,均没有集成客服功能,这导致很多垂直电商仍延用传统的客服方式。即使是规模较大的垂直电商,客户服务的渠道最多也就是营销QQ再加上400电话,这与电商的商业理念背道而驰。效率是电子商务对传统商业的重要革命,电商的本质即在于通过信息流动实现对供需关系的高效率匹配。电商的服务也应当像产品一样,进行效率革命。这也是“SaaS客服”、“全媒体服务”在互联网经济中蓬勃发展的重要原因。传统行业触网垂直电商,继产品上网之后,服务上网将是迈出的第二步。本文围绕服务效率和用户体验,为转型电子商务的传统行业商家介绍移动互联网下的客服形态——全媒体客服。

    什么是全媒体客服

    全媒体客服是指通过SaaS服务平台,客服人员可同时为来自各种渠道的用户提供实时和一致的服务,服务渠道包括APP、微信、微博、Web,以及传统呼叫中心等。用户和客服之间采用文字、图片、音视频等多媒体消息或是电话语音进行交互。通过覆盖多种渠道媒体,全媒体服务的理念是“用户在哪,服务在哪”。和传统客服相比,全媒体客服的核心价值在于能够大幅提高商家的服务效率,并且实现服务式营销,同时也为商家的客户带来了更好的体验。
    从部署角度来看,基于云端SaaS平台的全媒体客服是一种极轻量部署模式。用户无需采购任何专用硬件设备,只需要普通PC,通过浏览器即可提供专业客户服务。实现的方式也极其简单,对于APP,只需要集成厂商提供的SDK,对于网页,微信或微博,更是一行代码即可开启全媒体客服。
    环信是国内最早进军全媒体客服的厂商,根据易观智库的市场数据,在移动端SaaS客服市场,环信市场占有率高达77.4%(数据来源:易观智库《2015中国SaaS客服市场专题研究报告》)。本文即是基于市场发展趋势和环信的技术研究,介绍全媒体客服在提升效率和用户体验上的一些优势和关键技术。

    1:1 vs 1:N,集约化客户服务

    传统企业的客户服务通常以呼叫中心为主。用户需要服务时,拨打企业400/800电话,再由客服人员提供服务。由于一名客服同一时间只能服务于一名客户,因此这是一种1:1独占式服务方式。转型电商后,客户来的渠道多样化,有从网页端来的,有从APP端来的,或是从社交媒体,如微博、微信来的。这时候显然无法按呼叫中心1:1的配置方式,为不同渠道的客户提供单独服务。全媒体客服则是通过云端系统,打通不同渠道,对所有渠道来的用户进行统一排队和会话分配。每一位客服,都可同时服务于多个渠道来的客户,这是一种1:N式的集约化服务。在环信的案例——学而思的客户服务中心,每位客服人员最多时可同时服务20余名客户。在当前人口红利逐渐消失,人力成本急剧上升之时,任何对人力资源的高效率、集约化运用,都是在为企业创造效益。

    从IVR到ITR,可触摸的服务

    Gartner在3月份发布的《2020年前用户关系中心应关注的5项技术》中,把ITR(Interactive Touch Response,交互式触摸响应)技术列为关键技术之一。ITR是移动互联网时代,触摸化的IVR。客户不再需要听完所有的服务菜单,只需要在手机屏幕上,直接选择需要的服务内容,即可获得服务。例如,神州专车在给司机端提供的“微客服”服务,司机直接在手机屏幕上选择需要的服务内容,不需要打电话听完漫长的语音提示,再选择服务内容联系相应的客服。通过移动客服和ITR技术,整个服务过程时间节约了80%,既提升了用户体验,同时也降低了客服压力。


    图片_1.png



    自助式服务,让用户自己解决问题
        
    对于一些形式标准化,内容单一化的服务,可以通过自助式服务的方式,让用户自己解决问题。例如密码更改、订单管理、物流跟踪等等。自助式服务满足了用户碎片化的使用习惯。用户可以在车上闲暇时间,或是不方便电话沟通的场合即可完成服务,同时也降低了人工客服的需求量。
    提供自助式服务,需要企业将IT系统前置,通过一些开放式接口,让用户在商城应用程序的客户端中,直接调用相应接口,完成服务过程。这是和传统呼叫中心封闭式系统部署的不同之处。
    随着智能技术的成熟,在开放接口上还可进一步增加智能客服插件。客户通过自然语言,例如“我想修改订单送达地址”,经过自然语言处理技术进行语义分析后,客服机器人返回订单修改入口,用户完成自服务。这个过程,结合人机融合,可以提供无差错的高质量服务。


    图片_2.png



    人机融合,用智能提高服务效率

    虽然当前机器学习算法飞速发展,在客服领域也得到了广泛应用。但在自然语言处理,特别是中文语义的识别上,距离完全替代客服还有差距。用人机配合的方式,对于复杂问题,借助智能客服答复,辅以人工核验,既能提高效率,同时又保证了用户体验。
    环信在坐席工作台界面中,增加了一个推荐答案窗口。当用户发出客服请求时,智能机器人在推荐窗口提供了若干个备选答案。坐席人员从中选择一个最合适的答案,只需要点击“发送”,用户立刻就可得到回复。如果需要修改,也只需要在推荐答案的基础上经过少量编辑即可。通过人机配合,在保证准确度的情况下,大幅提高了坐席的工作效率。

    服务式营销,从成本中心到利润中心

    在传统行业,客户服务中心通常是一个成本中心,以解决售后问题为主,难以和营销挂钩。以信息流动为基础的电子商务则为服务式营销提供了机会,客服人员可以接触到客户资料、历史购买商品以及消费习惯等关键信息。但要实现营销,关键之处在于如何利用这些信息。
    服务式营销的关键技术之一是大数据分析能力,通过数据挖掘找到热门商品和潜在客户的最佳匹配关系。为此环信在全媒体客服产品提供了客户标签和热词分析功能。通过客户购买历史纪录、访问轨迹、基础资料等信息为客户打上标签,依靠热词分析及时发现热门商品,两者之间的匹配程度越高,商品销售的成功率越高。
    服务式营销的另一关键技术是无干扰的消息推送技术。电话外呼之所以日渐淘汰,除了缺乏精准性外,对用户干扰大、体验差也是重要原因。环信全媒体客服采用基于长连接的消息通知方式。客服人员的营销信息,以后台通知的方式发送到用户端。用户可以在闲暇时间打开,这种方式给客户充分的主动权,不会对客户体验带来影响。此外,长连接技术的另一优势是不会丢失消息。通过大数据分析和无干扰的消息推送,客服能前置到销售链前端,实施主动的服务式营销,从成本中心转变为利润中心。

    传统呼叫中心的升级之路

    转型电商的传统行业商家升级至全媒体客服,根据不同情况有两种可行解决方案。对于当前没有大规模呼叫中心的中小企业,可以采用直接切换的方式,一步到位,用全媒体客服替代传统电话客服。这种方案成本低廉,简单易行。
    对于已经建设了大规模呼叫中心的大型企业,以增量部署的方式,使全媒体客服嵌入呼叫中心,保护用户既有投资。这种方案通过开放式接口,可以实现“三统一”:

    • 统一排队与路由分配。不论何种渠道来的客户,统一进行排队,共享客服资源;

    • 统一话单记录。同一个客户,不论从哪种渠道访问,话单记录都只有一份,客服人员可查看完整历史会话信息;

    • 统一后台数据。通过开放式接口,可以对接现有CRM、工单系统、知识库,打通后台数据,消除数据壁垒。



    更加高效、更好体验、服务营销,这是全媒体客服对于传统呼叫中心的核心竞争力。转型全媒体客服,是传统呼叫中心的必然趋势。在这个过程中,既需要传统行业商家转变思路,拥抱移动互联网,更需要行业生态链的共建共赢。
    收起阅读 »

    【环信编程大赛优秀开源项目展示】公众号聊天助手--未认证的公众号与订阅用户之间的便捷联系

    APP运行效果图 项目功能: 个人自媒体公众号越来越多,然而由于个人公众号目前尚无法认证,没有客服接口权限,公众号主难以及时回复订阅用户的消息。通过这款公众号聊天助手,可以绕过微信官方接口,实现未...
    继续阅读 »
    APP运行效果图




    gong_zhong_hao_zhu_shou_1.jpg




    gong_zhong_hao_zhu_shou_2.jpg




    gong_zhong_hao_zhu_shou_3.jpg



    项目功能:


    个人自媒体公众号越来越多,然而由于个人公众号目前尚无法认证,没有客服接口权限,公众号主难以及时回复订阅用户的消息。通过这款公众号聊天助手,可以绕过微信官方接口,实现未认证的公众号与订阅用户之间的便捷联系。
    技术原理:
    通过对微信网页后台进行抓包分析,获取并破解了微信网页后台进行回复的接口。后端采用Python+tornado+requests开发。


    消息流程:


    1.公众号收到用户消息
    2.微信服务器通过回调通知聊天助手服务器,聊天助手服务器保存用户open_id
    3.聊天助手将消息转发至环信IM云
    4.公众号主人通过聊天助手收到消息
    5.公众号主进行回复
    6.助手服务器通过为订阅用户注册聊天账号并模拟登陆,通过轮询向环信拉取聊天信息(也可以通过收费的即时消息回调接口)
    7.将拉取到的聊天信息通过抓包分析得到的网页接口进行回复。


    心得:


    1.环信sdk是业界较为成熟的IM解决方案
    2.该工具在完善后会作为公益工具免费供外界使用,并注明Powered by EaseMob


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码下载:https://github.com/sunnylife/WechatMaster-backend
    APK下载体验↓↓↓ 收起阅读 »

    首届环信编程大赛颁奖典礼奖品

     特别感谢以下企业的大力支持: 义创空间提供颁奖场地   萌岛从自有形象库中授权一套价值12000元的表情包   Emokit赞助Apple Watch一台   猿圈全程提供技术评测   本次环信编程大赛分现金和实物奖励,现金奖励共1...
    继续阅读 »


    奖杯.jpg


     特别感谢以下企业的大力支持:


    义创空间提供颁奖场地
     
    萌岛从自有形象库中授权一套价值12000元的表情包
     
    Emokit赞助Apple Watch一台
     
    猿圈全程提供技术评测


     
    本次环信编程大赛分现金和实物奖励,现金奖励共15000元,具体如下


    一等奖8000元+Apple Watch/1+荣誉水晶杯/1+限量版瑞士军刀背包一个+价值12000元专属表情包
     
    二等奖5000元+限量版瑞士军刀背包/1+荣誉水晶杯
     
    三等奖2000+限量版瑞士军刀背包/1+荣誉水晶杯
     
    决赛前十颁发荣誉水晶杯一支+限量版瑞士军刀背包一个
     
    前五十可获得定制版精美T-shirt或卫衣


     
    来到颁奖典礼现场均可获得环信定制文件袋+多功能便携工具卡,现场还会随机抽取赠送由环信CEO签名的编程书籍
     
    颁奖典礼详情http://www.easemob.com/event/hackathon_party/ 收起阅读 »

    【环信编程大赛优秀开源项目展示】Cloud Developer--程序猿之间互相交流学习的平台

    APP运行效果图 项目简介 起初设想将此应用做成专门用于程序猿之间互相交流学习的一个平台,但是由 工作原因,没有充足的时间来投入到此次比赛中。只完成了部分功能。因为是个人开发, 没有美工和UI的配置,界面相对简陋,并且部...
    继续阅读 »
    APP运行效果图



    Cloud_Developer_01.png




    Cloud_Developer_02.png




    Cloud_Developer_03.png



    项目简介


    起初设想将此应用做成专门用于程序猿之间互相交流学习的一个平台,但是由 工作原因,没有充足的时间来投入到此次比赛中。只完成了部分功能。因为是个人开发, 没有美工和UI的配置,界面相对简陋,并且部分数据例如用户头像等采用随机数生成, 一些数据保存在本地UserDefault中,并且使用了环信内部的好友系统。工程主界面大部 分采用Storyboard完成,并且完成了界面适配,架构采用 MVVM 模式,结 合ReactiveCocoa来达到模块间的充分解耦。


     0x02使用到的第三方类库


    本工程没有使用到CocoaPod,所有用到的类库都位于工程中的Vendor文件夹
    1. ReactiveCocoa 2. DZNEnptyDataSet 3. IQKeyBoardManager 4. SVProgressHUD


    0x04其它扩展功能有


    3D Touch (手机桌面) Apple Pay(开通会员)


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    源码下载↓↓↓
     
    git源码地址https://github.com/FinderTiwk/CloudDeveloper 收起阅读 »

    【环信编程大赛优秀开源项目展示】宅男福利--美女直播应用

    APP运行效果图 项目介绍 本项目属于个人娱乐项目,做项目时主要想用环信的视频直播聊天,就临时改成和美女聊天的功能。目前只想到这些功能         1、浏览各种类型美女,单击放大和玩逗。(目前只实现放大查看)         2...
    继续阅读 »
    APP运行效果图



    286636562@qq.com宅男福利_02_.png




    286636562@qq.com宅男福利_03_.png



    项目介绍


    本项目属于个人娱乐项目,做项目时主要想用环信的视频直播聊天,就临时改成和美女聊天的功能。目前只想到这些功能
            1、浏览各种类型美女,单击放大和玩逗。(目前只实现放大查看)
            2、点击右下角美女的头像,进入和美女聊天玩逗,主要用了环信的小助手功能。



    项目用到的技术:


            1、用Kotlin和java混合编写。
            2、用了安卓最新效果(Fab,Snake,Recycler,CardView等)
            3、图片加载采用Glide
            4、网络加载采用Retrofit
            5、Activity和Fragment 的封装



    总结:


    由于最近只有晚上回来写项目,平时公司项目比较忙,还有很多想到还没有实现,大体框架实现了,具体功能只实现部分。后续完善。期待环信出很多有意思和好玩的功能。


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    源码下载↓↓↓
    源码git地址https://github.com/xusoku/EMDemo 收起阅读 »

    【环信编程大赛优秀开源项目展示】薅羊毛技术社区--针对于技术人员的社交软件

    APP运行效果图 项目简介 本app主要针对于技术人员的社交软件,技术开发者可以分享自己的文章,可以和其他技术人员聊天,平台也可以发一些文章。平时比较忙,偶尔有些功夫写一写,也对自己业余生活的一个补充...
    继续阅读 »
    APP运行效果图



    297165331@qq.com薅羊毛技术社区app_01_.jpg




    297165331@qq.com薅羊毛技术社区app_02_.jpg




    297165331@qq.com薅羊毛技术社区app_03_.jpg



    项目简介


    本app主要针对于技术人员的社交软件,技术开发者可以分享自己的文章,可以和其他技术人员聊天,平台也可以发一些文章。平时比较忙,偶尔有些功夫写一写,也对自己业余生活的一个补充,正好最近刚刚开发了一套新框架,顺便拿来使用。本app完全建立在服务端上。服务端提供数据和支持。


    -技术点
    客户端技术点


    1.系统中所有图标均采用字体图标(dileber框架中写的一套字体图标)
    2.图片采用.9图
    3.集成环信sdk,可以和服务器上的用户交流
    4.架构基于dileber(来源于DrCoSu工作室的开源mvp框架
    https://github.com/dileber/dileber(我个人开发的一套框架)mvp架构,代码清晰,代码简洁,层次分明
    5.json数据传输与解析
    6.框架自动生成(整个项目是采用一套配置文件生成的一套架构)
    7.本来想在项目资讯页面写一个瀑布流布局,貌似只有某些手机支持,不成功。
    8.采用下拉刷新
    9.自动登录



    服务器端技术点


    1.linux服务器,真实数据运行,api接口传输数据。
    2.服务器采用java web 架构为 mybatis+spring mvc+nginx
    3.数据均采用json包装
    4.数据库采用mysql



    该项目为环信编程大赛参赛项目。报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的技术大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码下载https://github.com/dileber/technology_community 收起阅读 »

    【环信编程大赛优秀开源项目展示】方圆十里--关注方圆十里内的人和事

    APP运行效果图 项目简介: 基于IM(环信通信云)+LBS(百度地图SDK+GeoHash距离算法)的社交APP 一、功能列表: 1、登录、注册(采用后端授权注册的方式绑定环信id,更安全)...
    继续阅读 »
    APP运行效果图



    fang_yuan_shi_li_1.jpg




    fang_yuan_shi_li_2.jpg




    fang_yuan_shi_li_3.jpg



    项目简介:


    基于IM(环信通信云)+LBS(百度地图SDK+GeoHash距离算法)的社交APP

    一、功能列表:
    1、登录、注册(采用后端授权注册的方式绑定环信id,更安全)
    2、用户资料:头像、昵称、性别、生日、地区(本地arrays.xml存储地区数据库)、个性签名
    3、Tab1.-“人”---- 百度地图中显示十公里内的人(如果用户位置集中或者人数过少会导致无法测试,因此“更远”选项,搜寻更多,并且本身的十公里概念也未进行筛选)
    4、Tab2.“事”----十公里内的动态--文字、图片、位置的动态(发布、回帖、通知提醒)
    5、Tab3.“聊”----IM会话
    6、Tab4. “友”---通讯录及好友申请
    7、我的动态
    8、二维码---通过扫二维码加好友
    二、特点备注:
    1、本项目中采用geohash算法编码用户的位置坐标,达到位置的粗分区,而后进行精确精算实现十公里之内的人和事的概念。
    2、创建自定义的百度地图标注。
    3、一套将环信IM系统和开发者自身的用户体系融合的解决方案(当前的解决方案在多个项目中得到检验,成熟稳定)。
    4、常见的发帖回复模块的处理解决。
    5、利用环信的透传消息进行用户的帖子发布、回复等提醒通知。(待更新)
    6、UI优化,如电话聊天背景的取自用户头像的毛玻璃特效--参考微信电话聊天背景(待更新)


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码下载https://github.com/huangfangyi/fangyuanshili 收起阅读 »

    微信浏览器webim报错

    报错信息 WebSocket connection to 'ws://im-api.easemob.com/ws/' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket...
    继续阅读 »
    报错信息 WebSocket connection to 'ws://im-api.easemob.com/ws/' failed: Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received 收起阅读 »

    【环信编程大赛优秀开源项目展示】文播--一款文字直播平台性的APP

    APP运行截图   功能: 本项目是一款基于环信sdk进行个性化改造的文字直播平台性的安卓app。 在参赛报名的时候,曾想过这样一个问题:一款完全为IM而生的sdk,到底能有如何的潜力?因此,另辟蹊径将环信提供的IM群...
    继续阅读 »
    APP运行截图




    wb1.jpg




    wb2.jpg




    wb3.jpg


     


    功能:


    本项目是一款基于环信sdk进行个性化改造的文字直播平台性的安卓app。
    在参赛报名的时候,曾想过这样一个问题:一款完全为IM而生的sdk,到底能有如何的潜力?因此,另辟蹊径将环信提供的IM群聊功能,通过重新设计,改造成了现在的文字直播的平台类型app。
    每个直播间,其实就是一个“只有群创建者才能发言”的IM群组或讨论组,再进行一些界面上的改造,就可以实现一款类似于从早期非智能机时代流行至今的纯文字直播的app。
    典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在《文播》里找到对应的频道。
    提交的该版本目前为纯游客端,主播端另行实现。



    技术:


    ·客户端使用DrCoSu工作室开源的dileber框架,MVP设计模式,整个项目冗余较低。
    ·融合环信SDK,并进行了个性化的改造。
    ·采用.9格式存储图片,ttf方式呈现界面与图标,各个机型兼容性较好。
    ·服务端采用Java(Spring),配合ngix和redis极大提升了访问响应速度。
    ·采用http通信和json、xml等数据格式,移植性和通用性好。



    心得


    重复造轮子虽然好,但是在实际开发中,往往可以使用更好的方式来加快你的节奏,从中获得更大的成就感。
    环信SDK在即时通讯云领域是一款足够优秀的SDK。配合JPush和好的创意,能实现无限多的可能性。
    创意是一款新型软件的核心竞争力。



    介绍


    文字的直播,一样精彩。


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    项目源码下载 ↓↓↓
      收起阅读 »

    【环信编程大赛开源优秀项目展示】图忆--一款基于位置信息的分享与社交应用

    APP运行截图   1.软件介绍 图忆是一款基于位置信息的分享与社交应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投...
    继续阅读 »
    APP运行截图




    图忆_01.png




    图忆_02.png




    图忆_03.png


     


    1.软件介绍


    图忆是一款基于位置信息的分享与社交应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投的好友,并且用户记事可以分享到公共社区平台,分享乐趣的同时也发现了更多的乐趣,社区推荐策略让用户发现更多有价值的乐趣。


    2.功能介绍


    【记录记忆】你可以记录自己的生活点滴在地图之上,可以公开给别人看,也可以保存为自己的私有记忆。
    【离线记录】没有网络也可以轻松保存离线记录,WIFI连接后直接批量上传,省心
    【地图附近】你将通过地图查看到附近用户公开的说有分享记录,当然是直接在地图上展示的哟,很直观的说,还有五个标签分类查询哟,就等你来发现了。
    【雷达】发现同时在附近开启雷达的小伙伴,自定义雷达显示的内容,让小伙伴更容易发现你
    【聊天圈子】与TA尽情畅聊,兴趣小伙伴建圈子一起聊。
    【图忆社区】点赞,评论,分享,收藏Ta的分享



    3.使用技术


    环信IM
    百度地图API
    有盟API



    4.作者心得


    IM正越来越得到开发者重视,也逐渐成为APP标配,绝大部分App中都集成了即时通讯功能。将APP的核心功能紧密与即时通讯良好结合,将更有利于APP的用户体验和留存。
    APP的多元发展中需要使用多功能的有机结合。而作为一个完整的SDK需要越少的干涉APP原本的逻辑,而不降低功能与体验,这些方面环信的IM SDK都做的挺好。


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
     
    git源码下载https://github.com/donlan/Tuyi
     
    作者演讲PPT下载↓↓↓ 收起阅读 »

    【环信编程大赛优秀开源项目展示】高仿微信--基于环信sdk高仿微信

    APP运行截图  实现功能点: 消息:订阅号、列表滑动删除、TitleBar弹出菜单 单聊,群聊(发送文字,语音,位置,视频,文件,语音电话,视频通话) 微信表情、 通讯录:按字母索引排序、添加手机通讯录好友、 发现:二维...
    继续阅读 »
    APP运行截图



    gao_fang_wei_xin_1.jpg



    gao_fang_wei_xin_2.jpg




    gao_fang_wei_xin_3.jpg



     实现功能点


    消息:订阅号、列表滑动删除、TitleBar弹出菜单
    单聊,群聊(发送文字,语音,位置,视频,文件,语音电话,视频通话)
    微信表情、
    通讯录:按字母索引排序、添加手机通讯录好友、
    发现:二维码扫描添加好友、二维码扫描加入群聊、扫码微信支付、生成自己二维码图片
    朋友圈、发布朋友圈信息、购物、游戏、设置
    WebView与Javascript交互、播放视频、加载HTML5页面
    FastJson Json转换神器、Imageloader图片加载库、android-async-http 网络请求库、图片缩放查看、加密解密


    技术心得


    本项目基本实现了微信的核心功能,对微信UI设计进行Redesign,提高用户体验
    另外在对接环信sdk的时候,没有直接采用官方的demo,而是基于环信官方文档,对聊天模块的功能自己来处理业务逻辑,对接起来比较麻烦,工作量也挺大。
    对于想要基于环信sdk进行二次开发时,前期还是要把官方demo和官方文档给摸熟,后面再进行功能扩展的时候,才能够得心应手。
     


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码下载https://github.com/motianhuo/wechat 收起阅读 »

    【环信编程大赛开源优秀项目展示】美肤GO--一款专注于个人海外代购及护肤咨询分享的APP

    APP运行截图 功能介绍 其中主要功能包含个人海外代购、美妆美肤课程、社区分享互动等,希望给爱美向往美的你一个全方位的秘密基地,也希望给想要做全职代购或者兼职代购甚至只是旅游顺便想代购挣回机票钱的我一...
    继续阅读 »
    APP运行截图



    1184986786@qq.com美肤GO_01_.png




    1184986786@qq.com美肤GO_02_.png




    1184986786@qq.com美肤GO_03_.png



    功能介绍


    其中主要功能包含个人海外代购、美妆美肤课程、社区分享互动等,希望给爱美向往美的你一个全方位的秘密基地,也希望给想要做全职代购或者兼职代购甚至只是旅游顺便想代购挣回机票钱的我一个平台,源于这个初衷,美肤GO提供高品质的代购机制和最亲民线下般体验的护肤资讯,只为让你更美丽! 


     
    技术方面


    android端使用的dileber框架,由我们大学的DrCoSu社团几位成员共同研发,达到了快速开发的效果。
    界面部分交互采用原生androidJava编写,MVP设计模式,整体结构清晰耦合度低。
    在用户交流处直接调用环信SDK,提供较好的社交聊天体验。
    兼容各个机型,不会出现拉伸或缩放问题。
    服务端采用Java实现,额外配置了redis,使持久层得到保障并有较好的接口响应速度。 心得方面,环信SDK还是一款很方便的IM体系,可以帮助小的开发者或小公司快速集成这方面的功能。


     
    心得建议
    如果进一步拓展开发类似于微信红包这样的功能,能吸引到更广泛的客户群 慢慢努力做最好的轻量护肤应用~ 
    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码下载:https://github.com/Rabbit00/MeifuGO

    源码  APK   下载↓↓↓ 收起阅读 »

    【环信编程大赛优秀开源项目展示】宅不住--发现城市精彩运动,认识周边潮人

    APP运行效果图 项目简介: 宅不住,发现城市精彩运动,认识周边潮人。类似与周末去哪儿 app, 同时加入了社交模块,即时聊天功能 主要功能模块有: 1.主页、附近的活动、附近的潮人 2...
    继续阅读 »
    APP运行效果图



    zhai_bu_zhu_1.jpg




    zhai_bu_zhu_2.jpg




    zhai_bu_zhu_3.jpg



    项目简介:


    宅不住,发现城市精彩运动,认识周边潮人。类似与周末去哪儿 app, 同时加入了社交模块,即时聊天功能


    主要功能模块有:


    1.主页、附近的活动、附近的潮人
    2.  发布活动、搜索活动
    3. 专题、及时通讯(环信SDK) 
    4. 登陆(个人主页)、设置模块


    技术介绍


    聊天页类: HKChatViewController
    聊天列表类: HKChatListViewController


    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    源码下载↓↓↓

    http://pan.baidu.com/s/1sl7oSnN 收起阅读 »

    【环信编程大赛优秀项目展示】咚咚--一款高效团队沟通的移动客户端

    APP运行效果图 基于环信平台进行开发,旨在打造的高效团队沟通的移动客户端,供企业内部协作使用、适应移动办公需要,提升企业沟通协同效率,增强企业办公管理效率。(当前仅实现了用户登入登出功能、通讯功能、投票功能。) 具体功能...
    继续阅读 »
    APP运行效果图



    dong_dong_1.jpg




    dong_dong_2.jpg




    dong_dong_3.jpg



    基于环信平台进行开发,旨在打造的高效团队沟通的移动客户端,供企业内部协作使用、适应移动办公需要,提升企业沟通协同效率,增强企业办公管理效率。(当前仅实现了用户登入登出功能、通讯功能、投票功能。)
    具体功能说明
    一、用户登录注册功能
    1.系统登录界面
    2系统注册界面
        注册功能实现:限制账号长度必须为11位,出生日期选择,头像选择(从系统自带头像中选择)
    3.系统首页
        登陆成功,即进入系统主页面
    4.个人信息查看及修改
        进入主页面后点击个人信息查看,即可查看相关信息,并对其进行修改
        主界面
        (1)头像修改功能实现
        (2)名字修改功能实现
        (3)部门修改功能实现
        (4)性别选择功能实现
        (5)个性签名修改功能实现
    二、通讯功能
        主界面
        功能实现:群组聊天,单对单私人聊天,查看好友列表,查看好友详情,查看群组详情
    三、投票功能
        主界面
        功能实现:展示用户发起的投票列表,新增投票,投票提交
    总结与心得
        基于环信平台的开发,使得项目的难点得于轻松解决,例如在平台上可以使用即时通讯功能以及用户好友管理、群组管理功能。让项目得于快速开发成型。
     
    该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
     
    git源码地址:https://github.com/caisiyi/SYTeamApp 收起阅读 »

    环信助力传统商业转型电商的客服升级之路:从产品上网到服务上网

    垂直类电商无疑是当前互联网经济的耀眼明星。即使是在当前人人喊冷的资本寒冬中,找钢网、找塑料网这类垂直电商仍然被资本市场所青睐。今年1月,找钢网宣布完成E轮战略融资,融资额超10亿元人民币。资本市场真金白银投入的背后,是以“互联网+”、“分享经济”、“供给测改革...
    继续阅读 »
    垂直类电商无疑是当前互联网经济的耀眼明星。即使是在当前人人喊冷的资本寒冬中,找钢网、找塑料网这类垂直电商仍然被资本市场所青睐。今年1月,找钢网宣布完成E轮战略融资,融资额超10亿元人民币。资本市场真金白银投入的背后,是以“互联网+”、“分享经济”、“供给测改革”为导向的整体经济转型。可以预见,随着以天猫、京东为代表的互联网原生电商平台的流量饱和,以传统行业为主角的垂直类电商正坐在开往春天的列车上,成为电商行业“新常态”。



    5300003a1e906fe42e6.jpg




    从产品上网到服务上网 
     
    传统行业触网电子商务,要解决的第一个问题,便是平台搭建。对垂直类电商而言,贯通上下游的供应链管理是运营重点,而天猫、京东这类以流量运营为主的综合电商平台显然不适合。因此自己搭建独立网店或交易平台,成为垂直类电商的首选。目前,国内提供电商平台解决方案的服务商已为数不少。例如,APP端有有赞、微店、微盟萌店、微猫,PC端有Shopex、ECShop、Shop++、Javashop、千米、筑云等等,这些厂商的模块化解决方案帮助传统行业快速实现了产品上网和在线交易,完成了“触网”的第一步。

    但我们看到,很多垂直电商虽然实现了“产品”上网,“服务”却迟迟没有上网。由于存在技术门槛,当前电商平台解决方案中,均没有集成客服功能,这导致很多垂直电商仍延用传统的客服方式。即使是规模较大的垂直电商,客户服务的渠道最多也就是营销QQ再加上400电话,这与电商的商业理念背道而驰。效率是电子商务对传统商业的重要革命,电商的本质即在于通过信息流动实现对供需关系的高效率匹配。电商的服务也应当像产品一样,进行效率革命。这也是“SaaS客服”、“全媒体服务”在互联网经济中蓬勃发展的重要原因。传统行业触网垂直电商,继产品上网之后,服务上网将是迈出的第二步。本文围绕服务效率和用户体验,为转型电子商务的传统行业商家介绍移动互联网下的客服形态——全媒体客服。

    什么是全媒体客服?
     
    全媒体客服是指通过SaaS服务平台,客服人员可同时为来自各种渠道的用户提供实时和一致的服务,服务渠道包括APP、微信、微博、Web,以及传统呼叫中心等。用户和客服之间采用文字、图片、音视频等多媒体消息或是电话语音进行交互。通过覆盖多种渠道媒体,全媒体服务的理念是“用户在哪,服务在哪”。和传统客服相比,全媒体客服的核心价值在于能够大幅提高商家的服务效率,并且实现服务式营销,同时也为商家的客户带来了更好的体验。 
     
    从部署角度来看,基于云端SaaS平台的全媒体客服是一种极轻量部署模式。用户无需采购任何专用硬件设备,只需要普通PC,通过浏览器即可提供专业客户服务。实现的方式也极其简单,对于APP,只需要集成厂商提供的SDK,对于网页,微信或微博,更是一行代码即可开启全媒体客服。
     
     环信是国内最早进军全媒体客服的厂商,根据易观智库的市场数据,在移动端SaaS客服市场,环信市场占有率高达77.4%(数据来源:易观智库《2015中国SaaS客服市场专题研究报告》)。本文即是基于市场发展趋势和环信的技术研究,介绍全媒体客服在提升效率和用户体验上的一些优势和关键技术。
     
     1:1 vs 1:N,集约化客户服务
     
    传统企业的客户服务通常以呼叫中心为主。用户需要服务时,拨打企业400/800电话,再由客服人员提供服务。由于一名客服同一时间只能服务于一名客户,因此这是一种1:1独占式服务方式。转型电商后,客户来的渠道多样化,有从网页端来的,有从APP端来的,或是从社交媒体,如微博、微信来的。这时候显然无法按呼叫中心1:1的配置方式,为不同渠道的客户提供单独服务。全媒体客服则是通过云端系统,打通不同渠道,对所有渠道来的用户进行统一排队和会话分配。每一位客服,都可同时服务于多个渠道来的客户,这是一种1:N式的集约化服务。在环信的案例——学而思的客户服务中心,每位客服人员最多时可同时服务20余名客户。在当前人口红利逐渐消失,人力成本急剧上升之时,任何对人力资源的高效率、集约化运用,都是在为企业创造效益。 
     
    从IVR到ITR,可触摸的服务



    52e00039e3744d94a91.jpg




    Gartner在3月份发布的《2020年前用户关系中心应关注的5项技术》中,把ITR(Interactive Touch Response,交互式触摸响应)技术列为关键技术之一。ITR是移动互联网时代,触摸化的IVR。客户不再需要听完所有的服务菜单,只需要在手机屏幕上,直接选择需要的服务内容,即可获得服务。例如,神州专车在给司机端提供的“微客服”服务,司机直接在手机屏幕上选择需要的服务内容,不需要打电话听完漫长的语音提示,再选择服务内容联系相应的客服。通过移动客服和ITR技术,整个服务过程时间节约了80%,既提升了用户体验,同时也降低了客服压力。

    自助式服务,让用户自己解决问题 
     
    对于一些形式标准化,内容单一化的服务,可以通过自助式服务的方式,让用户自己解决问题。例如密码更改、订单管理、物流跟踪等等。自助式服务满足了用户碎片化的使用习惯。用户可以在车上闲暇时间,或是不方便电话沟通的场合即可完成服务,同时也降低了人工客服的需求量。
     



    52c00039bec2249d142.jpg




    提供自助式服务,需要企业将IT系统前置,通过一些开放式接口,让用户在商城应用程序的客户端中,直接调用相应接口,完成服务过程。这是和传统呼叫中心封闭式系统部署的不同之处。 
     
    随着智能技术的成熟,在开放接口上还可进一步增加智能客服插件。客户通过自然语言,例如“我想修改订单送达地址”,经过自然语言处理技术进行语义分析后,客服机器人返回订单修改入口,用户完成自服务。这个过程,结合人机融合,可以提供无差错的高质量服务。 
     
    人机融合,用智能提高服务效率
     
    虽然当前机器学习算法飞速发展,在客服领域也得到了广泛应用。但在自然语言处理,特别是中文语义的识别上,距离完全替代客服还有差距。用人机配合的方式,对于复杂问题,借助智能客服答复,辅以人工核验,既能提高效率,同时又保证了用户体验。 
     
    环信在坐席工作台界面中,增加了一个推荐答案窗口。当用户发出客服请求时,智能机器人在推荐窗口提供了若干个备选答案。坐席人员从中选择一个最合适的答案,只需要点击“发送”,用户立刻就可得到回复。如果需要修改,也只需要在推荐答案的基础上经过少量编辑即可。通过人机配合,在保证准确度的情况下,大幅提高了坐席的工作效率。 
     
    服务式营销,从成本中心到利润中心 
     
    在传统行业,客户服务中心通常是一个成本中心,以解决售后问题为主,难以和营销挂钩。以信息流动为基础的电子商务则为服务式营销提供了机会,客服人员可以接触到客户资料、历史购买商品以及消费习惯等关键信息。但要实现营销,关键之处在于如何利用这些信息。 
     
    服务式营销的关键技术之一是大数据分析能力,通过数据挖掘找到热门商品和潜在客户的最佳匹配关系。为此环信在全媒体客服产品提供了客户标签和热词分析功能。通过客户购买历史纪录、访问轨迹、基础资料等信息为客户打上标签,依靠热词分析及时发现热门商品,两者之间的匹配程度越高,商品销售的成功率越高。 
     
    服务式营销的另一关键技术是无干扰的消息推送技术。电话外呼之所以日渐淘汰,除了缺乏精准性外,对用户干扰大、体验差也是重要原因。环信全媒体客服采用基于长连接的消息通知方式。客服人员的营销信息,以后台通知的方式发送到用户端。用户可以在闲暇时间打开,这种方式给客户充分的主动权,不会对客户体验带来影响。此外,长连接技术的另一优势是不会丢失消息。通过大数据分析和无干扰的消息推送,客服能前置到销售链前端,实施主动的服务式营销,从成本中心转变为利润中心。 
     
    传统呼叫中心的升级之路 
     
    转型电商的传统行业商家升级至全媒体客服,根据不同情况有两种可行解决方案。对于当前没有大规模呼叫中心的中小企业,可以采用直接切换的方式,一步到位,用全媒体客服替代传统电话客服。这种方案成本低廉,简单易行。
     
     对于已经建设了大规模呼叫中心的大型企业,以增量部署的方式,使全媒体客服嵌入呼叫中心,保护用户既有投资。这种方案通过开放式接口,可以实现“三统一”: 
    • 统一排队与路由分配。不论何种渠道来的客户,统一进行排队,共享客服资源; 
    • 统一话单记录。同一个客户,不论从哪种渠道访问,话单记录都只有一份,客服人员可查看完整历史会话信息; 
    • 统一后台数据。通过开放式接口,可以对接现有CRM、工单系统、知识库,打通后台数据,消除数据壁垒。 

     更加高效、更好体验、服务营销,这是全媒体客服对于传统呼叫中心的核心竞争力。转型全媒体客服,是传统呼叫中心的必然趋势。在这个过程中,既需要传统行业商家转变思路,拥抱移动互联网,更需要行业生态链的共建共赢。
      收起阅读 »

    Android V3.1.2 release

    新功能: 1.视频通话增加切换摄像头API:EMClient.getInstance().callManager().switchCamera(); 2.新增消息搜索API:conversation.searchMsgFromDB(); 3.支持设置和获取lo...
    继续阅读 »
    新功能:
    1.视频通话增加切换摄像头API:EMClient.getInstance().callManager().switchCamera();
    2.新增消息搜索API:conversation.searchMsgFromDB();
    3.支持设置和获取long类型的扩展字段;
    4.加快app从后台切到前台时的重连速度;
    5.优化GCM推送;


    Bug fix:
    1.修复某些手机发送系统表情时对方接到为乱码或空白的bug;
    2.修复上一个版本发送图片消息时,如果是小图会删除原图的bug;
     
    版本历史:Android sdk 更新日志
    下载地址:SDK下载 收起阅读 »

    iOS sdk 3.1.2 release

    新功能: 增加消息搜索功能,可以根据消息类型或者关键字搜索 优化绑定deviceToken逻辑 bug fix: Fix 修复发送系统表情时对方接到为乱码或空白的问题   SDK下载http://www.easemob.com/download
    新功能:

    增加消息搜索功能,可以根据消息类型或者关键字搜索
    优化绑定deviceToken逻辑
    bug fix:

    Fix 修复发送系统表情时对方接到为乱码或空白的问题
     
    SDK下载http://www.easemob.com/download

    3.0 Demo ,单聊 ,群聊

    环信很给力:写这个就是方便集成,做个简单的Demo  还有的细节需要自己慢慢修改3.0  - (NSArray *)loadMoreMessagesFromId:(NSString *)aMessageId                          ...
    继续阅读 »
    环信很给力:写这个就是方便集成,做个简单的Demo 
    还有的细节需要自己慢慢修改3.0  - (NSArray *)loadMoreMessagesFromId:(NSString *)aMessageId

                                  limit:(int)aLimit

                              direction:(EMMessageSearchDirection)aDirection;
    这个方法和以前不一样,若进去聊天界面获取历史消息EMMessageSearchDirectionUp 用这个
    moreMessages = [weakSelf.conversation loadMoreMessagesFromId:messageId limit:(int)count direction:EMMessageSearchDirectionUp];
     http://community.easemob.com/article/825307736   
    进入这个连接就是单聊的Demo 
    如果需要改为群聊,
    - (void)creatpUsh

    {

            EMError *error = nil;

            EMGroupOptions *setting = [[EMGroupOptions alloc] init];

            setting.maxUsersCount = 500;

            setting.style = EMGroupStylePublicOpenJoin;// 创建不同类型的群组,这里需要才传入不同的类型

            EMGroup *group = [[EMClient sharedClient].groupManager createGroupWithSubject:@"老鸭粉丝汤2" description:@"想吃的来来来2" invitees:nil message:@"邀请您加入群组" setting:setting error:&error];

            if(!error){

                NSLog(@"创建成功 -- %@",group.groupId);

                self.grouldID = group.groupId;

            }
    }
    这里创建一个群组/*!
      self.grouldID = group.groupId; 获取群组的ID,
    首先加入群组,不同的方法加入不同群组
    EMError *error = nil;

        [[EMClient sharedClient].groupManager joinPublicGroup:@"1461034891668" error:&error];

       我这里就一EMGroupStylePublicOpenJoin这个类型,其他的看文档有,
    在跳转页面的时候传入
        ChatViewController *chatVC = [[ChatViewController alloc]initWithConversationChatter:@"1461034891668" conversationType:EMConversationTypeGroupChat];

        chatVC.title = @"老鸭粉丝汤";

        [self.navigationController  pushViewController:chatVC animated:YES];
    这样跳转过去就基本可以聊天了
    更多问题可以加环信群 :165331879 收起阅读 »

    关于环信im用户ID大小写问题

    情况是如果某个用户的名字是包含大写的,如 51170219870626850X,但是在另一个用户的 EMMessageListener 中的 onMessageReceived 中接收到的 username 是 51170219870626850x,在源码中的...
    继续阅读 »
    情况是如果某个用户的名字是包含大写的,如 51170219870626850X,但是在另一个用户的 EMMessageListener 中的 onMessageReceived 中接收到的 username 是 51170219870626850x,在源码中的比较方法是 equals(String) 这时就会判断失误,判断结果是false,修正办法是将 equals 换成 equalsIgnoreCase 即可,下面附上图 收起阅读 »

    企业即时通讯系统二次开发

    即时通讯系统的出现让企业感受到了现代化信息管理的力量,可真正解决企业的问题。 其实企业即时通讯系统也是通过延伸统一通信的理念研发而来的,溯本追源,企业即时通讯也是属于统一通信的范畴,只不过它更有针对性、具体性的解决了企业的信息堵塞和效率低下的问题。 企业即时通...
    继续阅读 »

    即时通讯系统的出现让企业感受到了现代化信息管理的力量,可真正解决企业的问题。
    其实企业即时通讯系统也是通过延伸统一通信的理念研发而来的,溯本追源,企业即时通讯也是属于统一通信的范畴,只不过它更有针对性、具体性的解决了企业的信息堵塞和效率低下的问题。
    企业即时通讯系统具备文字、语音、视频、文件共享等多样化沟通方式,用户可以通过最合适的媒体实现通信和业务协作。不要质疑信息传递的及时性和安全性,节约时间和降低成本的同时,提高了生产率和竞争实力。
    信贸通即时通讯软件。专为企事业单位和政__府部门设计,通过对政企机构内部现有信息和应用系统的一体化集成整合,快速实现企业内部即时通讯、分权限组织架构管理、一站式协同办公等功能,能够有效的缩短内部沟通距离,快速提高政企内部工作效率。
    企业在发布公告的时候可以通过电子公告或者实时消息广播实现,这种方式样式新颖能吸引大家的注意力,有助于企业信息的传播。另外,通过组织架构树或者模糊查询,能够迅速查找到联系人,通过文字会话、语音视频、等方式企业内外通讯完全统一无障碍,可以帮助企业加快信息流转。
      收起阅读 »

    有关昨天在集成EasseUI报错问题解决

      在集成EaseUI环信的时候回出现各种不同的错误,当导入的时候,最基本的报错就是头文件的报错,这种的报错是非常常见,所以这个时候,我们需要静下心来,进行报错的排除和检查,当然你需要,这个时候需要的是耐心.       1.在你集成环信的时候,新建的工程项...
    继续阅读 »
     
    在集成EaseUI环信的时候回出现各种不同的错误,当导入的时候,最基本的报错就是头文件的报错,这种的报错是非常常见,所以这个时候,我们需要静下心来,进行报错的排除和检查,当然你需要,这个时候需要的是耐心.

          1.在你集成环信的时候,新建的工程项目我就不说了,最关键的就是你集成到成型的app中的时候,估计报错会比较多一点.

          2.你要先将你想要的demo里面的东西集成好了,这是最重要的的前提,下面就是我遇到报错:
     "_OBJC_CLASS_$_SKPayment", referenced from:
    objc-class-ref in Parse(PFPurchase.o)
    "_OBJC_CLASS_$_SKPaymentQueue", referenced from:
    objc-class-ref in Parse(PFPurchase.o)
    "_OBJC_CLASS_$_SKProductsRequest", referenced from:
    objc-class-ref in Parse(PFPurchase.o)
    ld: symbol(s) not found for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

     错误截图:​



    屏幕快照_2016-04-19_下午7.02_.55_.png


     


      解决的方法:
     


        一:在http:stackoverflow.com上面也有很多遇到过,你也可以将错误进行贴上去,上面说是少了Accounts,Social库,还有一个就是parse库,这个在parse库在xcode里面没有,最近也在研究这个parse,希望有啥好的东西,贴出来,期待!

        二:删除在你的工程有关parse,在demo中3rdparty中parse文件,在这里需要建议的是,我们在删除的时候,该删的就删,不该删不要删除,

         三:当然在不同的工程中,报错会有不同新的报错,这个时候我们需要,有耐心查找排错,希望有新的方法进行交流互动,建议和解决的思路贴上去,当然在这里也得感谢环信的技术的提醒.


    收起阅读 »

    环信直播课堂第八期--3.xSDK头像昵称的实现

    日期与时间:2016年4月21日15:00 持续时间:半小时 描述:集成环信即时通讯的时候,相信大家都有问过这个问题,昵称头像怎么实现? 本期环信直播课堂将由环信IOS工程师FUccc给大家详细讲解ios3.xSDK头像昵称的实现 直播观看地址: ht...
    继续阅读 »
    日期与时间:2016年4月21日15:00

    持续时间:半小时

    描述:集成环信即时通讯的时候,相信大家都有问过这个问题,昵称头像怎么实现?

    本期环信直播课堂将由环信IOS工程师FUccc给大家详细讲解ios3.xSDK头像昵称的实现

    直播观看地址: http://www.imgeek.org/video/15
     
    视频回放地址:http://www.imgeek.org/video/25 收起阅读 »

    Android开发集成环信SDK3.x教程

    前言 环信已经发部了SDK3.x版本,SDK3.x相对于SDK2.x来说是整个进行了重写,API变化还是比较大的,已经熟悉SDK2.x的开发者在使用新的SDK3.x还是会遇到不少问题的,不过还好官方给出了SDK2.x升级SDK3.x指南,已经熟悉SDK2.x...
    继续阅读 »
    前言

    环信已经发部了SDK3.x版本,SDK3.x相对于SDK2.x来说是整个进行了重写,API变化还是比较大的,已经熟悉SDK2.x的开发者在使用新的SDK3.x还是会遇到不少问题的,不过还好官方给出了SDK2.x升级SDK3.x指南,已经熟悉SDK2.x开发者可以根据文档了解SDK3.x的变化,新集成的开发者可以直接参考SDK3.x进行集成;
    这里简单的实现了sdk的初始化以及注册登录和收发消息,不过ui上没有没有去做很好的处理
     
     
    先看效果图​



    ec-demo.gif




    提供一些地址

    当前项目地址,可以直接 clone 运行
    EaseChat Github

    AndroidStudio下载
    Android官方下载
    国内提供 AndroidDevTools

    模拟器 Genymotion下载
    Genymotion 官网

    环信官方文档
    SDK3.x 文档
    SDK3.x API 文档
    SDK2.x 升级 SDK3.x 文档
     
    ###说下我当前开发环境
    这里并不是一定要按照我的配置来,只是说下当前项目开发运行的环境,如果你的开发环境不同可能需要自己修改下项目配置build.gradle文件


    AndroidStudio 2.0
    Gradle 2.10(跟随AndroidStudio 一起更新)
    Android SDK Tool 25.1.1
    Android Build-tools 23.0.2
    Android Support 最新
    Genymotion 2.6


    如果你还是用的Eclipse,可以下载AndroidStudio尝试下,如果你上不了Android官网,不懂怎么翻墙可以找下国内开发提供的一些地址
     
    开始集成

    这次要实现 SDK的初始化、SDK端的注册登录、消息的发送和监听这三步

    SDK的初始化

    这个初始化时在Application里进行的,这里定义了一个方法去初始化环信的SDK,并在其中进行了一些设置
    package net.melove.demo.easechat;

    import android.app.ActivityManager;
    import android.app.Application;
    import android.content.Context;
    import android.content.pm.PackageManager;

    import com.hyphenate.chat.EMClient;
    import com.hyphenate.chat.EMOptions;

    import java.util.Iterator;
    import java.util.List;

    /**
    * Created by lz on 2016/4/16.
    * 项目的 Application类,做一些项目的初始化操作,比如sdk的初始化等
    */
    public class ECApplication extends Application {

    // 上下文菜单
    private Context mContext;

    // 记录是否已经初始化
    private boolean isInit = false;

    @Override
    public void onCreate() {
    super.onCreate();
    mContext = this;

    // 初始化环信SDK
    initEasemob();
    }

    /**
    *
    */
    private void initEasemob() {
    // 获取当前进程 id 并取得进程名
    int pid = android.os.Process.myPid();
    String processAppName = getAppName(pid);
    /**
    * 如果app启用了远程的service,此application:onCreate会被调用2次
    * 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
    * 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process name就立即返回
    */
    if (processAppName == null || !processAppName.equalsIgnoreCase(mContext.getPackageName())) {
    // 则此application的onCreate 是被service 调用的,直接返回
    return;
    }
    if (isInit) {
    return;
    }
    /**
    * SDK初始化的一些配置
    * 关于 EMOptions 可以参考官方的 API 文档
    * http://www.easemob.com/apidoc/android/chat3.0/classcom_1_1hyphenate_1_1chat_1_1_e_m_options.html
    */
    EMOptions options = new EMOptions();
    // 设置Appkey,如果配置文件已经配置,这里可以不用设置
    // options.setAppKey("lzan13#hxsdkdemo");
    // 设置自动登录
    options.setAutoLogin(true);
    // 设置是否需要发送已读回执
    options.setRequireAck(true);
    // 设置是否需要发送回执,TODO 这个暂时有bug,上层收不到发送回执
    options.setRequireDeliveryAck(true);
    // 设置是否需要服务器收到消息确认
    options.setRequireServerAck(true);
    // 收到好友申请是否自动同意,如果是自动同意就不会收到好友请求的回调,因为sdk会自动处理,默认为true
    options.setAcceptInvitationAlways(false);
    // 设置是否自动接收加群邀请,如果设置了当收到群邀请会自动同意加入
    options.setAutoAcceptGroupInvitation(false);
    // 设置(主动或被动)退出群组时,是否删除群聊聊天记录
    options.setDeleteMessagesAsExitGroup(false);
    // 设置是否允许聊天室的Owner 离开并删除聊天室的会话
    options.allowChatroomOwnerLeave(true);
    // 设置google GCM推送id,国内可以不用设置
    // options.setGCMNumber(MLConstants.ML_GCM_NUMBER);
    // 设置集成小米推送的appid和appkey
    // options.setMipushConfig(MLConstants.ML_MI_APP_ID, MLConstants.ML_MI_APP_KEY);

    // 调用初始化方法初始化sdk
    EMClient.getInstance().init(mContext, options);

    // 设置开启debug模式
    EMClient.getInstance().setDebugMode(true);

    // 设置初始化已经完成
    isInit = true;
    }

    /**
    * 根据Pid获取当前进程的名字,一般就是当前app的包名
    *
    * @param pid 进程的id
    * @return 返回进程的名字
    */
    private String getAppName(int pid) {
    String processName = null;
    ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
    List list = activityManager.getRunningAppProcesses();
    Iterator i = list.iterator();
    while (i.hasNext()) {
    ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
    try {
    if (info.pid == pid) {
    // 根据进程的信息获取当前进程的名字
    processName = info.processName;
    // 返回当前进程名
    return processName;
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    // 没有匹配的项,返回为null
    return null;
    }
    }

     主界面

    app启动后默认会进入到ECMainActivity,不过在主界面会先判断一下是否登录成功过,如果没有,就会跳转到登录几面,然后我们调用登录的时候,在登录方法的onSuccess()回调中我们进行了界面的跳转,跳转到主界面,在主界面我们可以发起回话;
    看下主界面的详细代码实现:
    package net.melove.demo.easechat;

    import android.content.Intent;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.Toast;

    import com.hyphenate.EMCallBack;
    import com.hyphenate.chat.EMClient;

    public class ECMainActivity extends AppCompatActivity {

    // 发起聊天 username 输入框
    private EditText mChatIdEdit;
    // 发起聊天
    private Button mStartChatBtn;
    // 退出登录
    private Button mSignOutBtn;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 判断sdk是否登录成功过,并没有退出和被踢,否则跳转到登陆界面
    if (!EMClient.getInstance().isLoggedInBefore()) {
    Intent intent = new Intent(ECMainActivity.this, ECLoginActivity.class);
    startActivity(intent);
    finish();
    return;
    }

    setContentView(R.layout.activity_main);

    initView();
    }

    /**
    * 初始化界面
    */
    private void initView() {

    mChatIdEdit = (EditText) findViewById(R.id.ec_edit_chat_id);

    mStartChatBtn = (Button) findViewById(R.id.ec_btn_start_chat);
    mStartChatBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    // 获取我们发起聊天的者的username
    String chatId = mChatIdEdit.getText().toString().trim();
    if (!TextUtils.isEmpty(chatId)) {
    // 获取当前登录用户的 username
    String currUsername = EMClient.getInstance().getCurrentUser();
    if (chatId.equals(currUsername)) {
    Toast.makeText(ECMainActivity.this, "不能和自己聊天", Toast.LENGTH_SHORT).show();
    return;
    }
    // 跳转到聊天界面,开始聊天
    Intent intent = new Intent(ECMainActivity.this, ECChatActivity.class);
    intent.putExtra("ec_chat_id", chatId);
    startActivity(intent);
    } else {
    Toast.makeText(ECMainActivity.this, "Username 不能为空", Toast.LENGTH_LONG).show();
    }
    }
    });

    mSignOutBtn = (Button) findViewById(R.id.ec_btn_sign_out);
    mSignOutBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    signOut();
    }
    });
    }

    /**
    * 退出登录
    */
    private void signOut() {
    // 调用sdk的退出登录方法,第一个参数表示是否解绑推送的token,没有使用推送或者被踢都要传false
    EMClient.getInstance().logout(false, new EMCallBack() {
    @Override
    public void onSuccess() {
    Log.i("lzan13", "logout success");
    // 调用退出成功,结束app
    finish();
    }

    @Override
    public void onError(int i, String s) {
    Log.i("lzan13", "logout error " + i + " - " + s);
    }

    @Override
    public void onProgress(int i, String s) {

    }
    });
    }
    }

     SDK端的注册登录

    SDK初始化做完之后,就是需要进行环信的登录了,登录了才能使用环信的功能,才能收发消息,有不少人经常问,不注册账户能使用么,这是聊天sdk,不注册账户你拿什么聊天呢!
    登录调用EMClient.getInstance().login(username, password, callback);此方法是一个异步方法,所以需要设置EMCallback回调来接收登录结果;
    注册调用EMClient.getInstance().createAccount(username, password);此方法是同步方法,需要自己创建新线程去调用,不能放在UI线程直接调用;
    因为只是个简单的demo,这边把登录和注册都卸载了LoginActivity类里,这个方法中对调用环信sdk的方法返回错误值做了一些判断,具体错误信息可以参考官方文档:
    环信SDK3.x EMError
    package net.melove.demo.easechat;

    import android.app.ProgressDialog;
    import android.content.Intent;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.Toast;

    import com.hyphenate.EMCallBack;
    import com.hyphenate.EMError;
    import com.hyphenate.chat.EMClient;
    import com.hyphenate.exceptions.HyphenateException;

    public class ECLoginActivity extends AppCompatActivity {

    // 弹出框
    private ProgressDialog mDialog;

    // username 输入框
    private EditText mUsernameEdit;
    // 密码输入框
    private EditText mPasswordEdit;

    // 注册按钮
    private Button mSignUpBtn;
    // 登录按钮
    private Button mSignInBtn;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);

    initView();
    }

    /**
    * 初始化界面控件
    */
    private void initView() {
    mUsernameEdit = (EditText) findViewById(R.id.ec_edit_username);
    mPasswordEdit = (EditText) findViewById(R.id.ec_edit_password);

    mSignUpBtn = (Button) findViewById(R.id.ec_btn_sign_up);
    mSignUpBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    signUp();
    }
    });

    mSignInBtn = (Button) findViewById(R.id.ec_btn_sign_in);
    mSignInBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    signIn();
    }
    });
    }

    /**
    * 注册方法
    */
    private void signUp() {
    // 注册是耗时过程,所以要显示一个dialog来提示下用户
    mDialog = new ProgressDialog(this);
    mDialog.setMessage("注册中,请稍后...");
    mDialog.show();

    new Thread(new Runnable() {
    @Override
    public void run() {
    try {
    String username = mUsernameEdit.getText().toString().trim();
    String password = mPasswordEdit.getText().toString().trim();
    EMClient.getInstance().createAccount(username, password);
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    if (!ECLoginActivity.this.isFinishing()) {
    mDialog.dismiss();
    }
    Toast.makeText(ECLoginActivity.this, "注册成功", Toast.LENGTH_LONG).show();
    }
    });
    } catch (final HyphenateException e) {
    e.printStackTrace();
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    if (!ECLoginActivity.this.isFinishing()) {
    mDialog.dismiss();
    }
    /**
    * 关于错误码可以参考官方api详细说明
    * http://www.easemob.com/apidoc/android/chat3.0/classcom_1_1hyphenate_1_1_e_m_error.html
    */
    int errorCode = e.getErrorCode();
    String message = e.getMessage();
    Log.d("lzan13", String.format("sign up - errorCode:%d, errorMsg:%s", errorCode, e.getMessage()));
    switch (errorCode) {
    // 网络错误
    case EMError.NETWORK_ERROR:
    Toast.makeText(ECLoginActivity.this, "网络错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    // 用户已存在
    case EMError.USER_ALREADY_EXIST:
    Toast.makeText(ECLoginActivity.this, "用户已存在 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    // 参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册
    case EMError.USER_ILLEGAL_ARGUMENT:
    Toast.makeText(ECLoginActivity.this, "参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    // 服务器未知错误
    case EMError.SERVER_UNKNOWN_ERROR:
    Toast.makeText(ECLoginActivity.this, "服务器未知错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    case EMError.USER_REG_FAILED:
    Toast.makeText(ECLoginActivity.this, "账户注册失败 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    default:
    Toast.makeText(ECLoginActivity.this, "ml_sign_up_failed code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
    break;
    }
    }
    });
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }).start();
    }

    /**
    * 登录方法
    */
    private void signIn() {
    mDialog = new ProgressDialog(this);
    mDialog.setMessage("正在登陆,请稍后...");
    mDialog.show();
    String username = mUsernameEdit.getText().toString().trim();
    String password = mPasswordEdit.getText().toString().trim();
    EMClient.getInstance().login(username, password, new EMCallBack() {
    /**
    * 登陆成功的回调
    */
    @Override
    public void onSuccess() {
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    mDialog.dismiss();

    // 加载所有会话到内存
    EMClient.getInstance().chatManager().loadAllConversations();
    // 加载所有群组到内存,如果使用了群组的话
    // EMClient.getInstance().groupManager().loadAllGroups();

    // 登录成功跳转界面
    Intent intent = new Intent(ECLoginActivity.this, ECMainActivity.class);
    startActivity(intent);
    finish();
    }
    });
    }

    /**
    * 登陆错误的回调
    * @param i
    * @param s
    */
    @Override
    public void onError(final int i, final String s) {
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    mDialog.dismiss();
    Log.d("lzan13", "登录失败 Error code:" + i + ", message:" + s);
    /**
    * 关于错误码可以参考官方api详细说明
    * http://www.easemob.com/apidoc/android/chat3.0/classcom_1_1hyphenate_1_1_e_m_error.html
    */
    switch (i) {
    // 网络异常 2
    case EMError.NETWORK_ERROR:
    Toast.makeText(ECLoginActivity.this, "网络错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 无效的用户名 101
    case EMError.INVALID_USER_NAME:
    Toast.makeText(ECLoginActivity.this, "无效的用户名 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 无效的密码 102
    case EMError.INVALID_PASSWORD:
    Toast.makeText(ECLoginActivity.this, "无效的密码 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 用户认证失败,用户名或密码错误 202
    case EMError.USER_AUTHENTICATION_FAILED:
    Toast.makeText(ECLoginActivity.this, "用户认证失败,用户名或密码错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 用户不存在 204
    case EMError.USER_NOT_FOUND:
    Toast.makeText(ECLoginActivity.this, "用户不存在 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 无法访问到服务器 300
    case EMError.SERVER_NOT_REACHABLE:
    Toast.makeText(ECLoginActivity.this, "无法访问到服务器 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 等待服务器响应超时 301
    case EMError.SERVER_TIMEOUT:
    Toast.makeText(ECLoginActivity.this, "等待服务器响应超时 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 服务器繁忙 302
    case EMError.SERVER_BUSY:
    Toast.makeText(ECLoginActivity.this, "服务器繁忙 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    // 未知 Server 异常 303 一般断网会出现这个错误
    case EMError.SERVER_UNKNOWN_ERROR:
    Toast.makeText(ECLoginActivity.this, "未知的服务器异常 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    default:
    Toast.makeText(ECLoginActivity.this, "ml_sign_in_failed code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
    break;
    }
    }
    });
    }

    @Override
    public void onProgress(int i, String s) {

    }
    });
    }
    }

     消息的发送和监听

    实现消息的接收需要添加EMMessageListener消息监听接口,我们在需要监听的地方要实现这个接口,并实现接口里边的几个回调方法:


    onMessageReceived(List list)新消息的回调
    onCmdMessageReceived(List list)新的透传消息回调
    onMessageReadAckReceived(List list)消息已读回调
    onMessageDeliveryAckReceived(List list)消息已发送回调
    onMessageChanged(EMMessage message, Object object)消息状态改变回调


    下边是聊天界面消息监听与发送的完整实现,代码注释比较详细,不再一一解释
    package net.melove.demo.easechat;

    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.text.method.ScrollingMovementMethod;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.TextView;

    import com.hyphenate.EMCallBack;
    import com.hyphenate.EMMessageListener;
    import com.hyphenate.chat.EMClient;
    import com.hyphenate.chat.EMCmdMessageBody;
    import com.hyphenate.chat.EMConversation;
    import com.hyphenate.chat.EMMessage;
    import com.hyphenate.chat.EMTextMessageBody;

    import java.util.List;

    public class ECChatActivity extends AppCompatActivity implements EMMessageListener {

    // 聊天信息输入框
    private EditText mInputEdit;
    // 发送按钮
    private Button mSendBtn;

    // 显示内容的 TextView
    private TextView mContentText;

    // 消息监听器
    private EMMessageListener mMessageListener;
    // 当前聊天的 ID
    private String mChatId;
    // 当前会话对象
    private EMConversation mConversation;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_chat);

    // 获取当前会话的username(如果是群聊就是群id)
    mChatId = getIntent().getStringExtra("ec_chat_id");
    mMessageListener = this;

    initView();
    initConversation();
    }

    /**
    * 初始化界面
    */
    private void initView() {
    mInputEdit = (EditText) findViewById(R.id.ec_edit_message_input);
    mSendBtn = (Button) findViewById(R.id.ec_btn_send);
    mContentText = (TextView) findViewById(R.id.ec_text_content);
    // 设置textview可滚动,需配合xml布局设置
    mContentText.setMovementMethod(new ScrollingMovementMethod());

    // 设置发送按钮的点击事件
    mSendBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    String content = mInputEdit.getText().toString().trim();
    if (!TextUtils.isEmpty(content)) {
    mInputEdit.setText("");
    // 创建一条新消息,第一个参数为消息内容,第二个为接受者username
    EMMessage message = EMMessage.createTxtSendMessage(content, mChatId);
    // 将新的消息内容和时间加入到下边
    mContentText.setText(mContentText.getText() + "\n" + content + " -> " + message.getMsgTime());
    // 调用发送消息的方法
    EMClient.getInstance().chatManager().sendMessage(message);
    // 为消息设置回调
    message.setMessageStatusCallback(new EMCallBack() {
    @Override
    public void onSuccess() {
    // 消息发送成功,打印下日志,正常操作应该去刷新ui
    Log.i("lzan13", "send message on success");
    }

    @Override
    public void onError(int i, String s) {
    // 消息发送失败,打印下失败的信息,正常操作应该去刷新ui
    Log.i("lzan13", "send message on error " + i + " - " + s);
    }

    @Override
    public void onProgress(int i, String s) {
    // 消息发送进度,一般只有在发送图片和文件等消息才会有回调,txt不回调
    }
    });
    }
    }
    });
    }

    /**
    * 初始化会话对象,并且根据需要加载更多消息
    */
    private void initConversation() {

    /**
    * 初始化会话对象,这里有三个参数么,
    * 第一个表示会话的当前聊天的 useranme 或者 groupid
    * 第二个是绘画类型可以为空
    * 第三个表示如果会话不存在是否创建
    */
    mConversation = EMClient.getInstance().chatManager().getConversation(mChatId, null, true);
    // 设置当前会话未读数为 0
    mConversation.markAllMessagesAsRead();
    int count = mConversation.getAllMessages().size();
    if (count < mConversation.getAllMsgCount() && count < 20) {
    // 获取已经在列表中的最上边的一条消息id
    String msgId = mConversation.getAllMessages().get(0).getMsgId();
    // 分页加载更多消息,需要传递已经加载的消息的最上边一条消息的id,以及需要加载的消息的条数
    mConversation.loadMoreMsgFromDB(msgId, 20 - count);
    }
    // 打开聊天界面获取最后一条消息内容并显示
    if (mConversation.getAllMessages().size() > 0) {
    EMMessage messge = mConversation.getLastMessage();
    EMTextMessageBody body = (EMTextMessageBody) messge.getBody();
    // 将消息内容和时间显示出来
    mContentText.setText(body.getMessage() + " - " + mConversation.getLastMessage().getMsgTime());
    }
    }

    /**
    * 自定义实现Handler,主要用于刷新UI操作
    */
    Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case 0:
    EMMessage message = (EMMessage) msg.obj;
    // 这里只是简单的demo,也只是测试文字消息的收发,所以直接将body转为EMTextMessageBody去获取内容
    EMTextMessageBody body = (EMTextMessageBody) message.getBody();
    // 将新的消息内容和时间加入到下边
    mContentText.setText(mContentText.getText() + "\n" + body.getMessage() + " <- " + message.getMsgTime());
    break;
    }
    }
    };

    @Override
    protected void onResume() {
    super.onResume();
    // 添加消息监听
    EMClient.getInstance().chatManager().addMessageListener(mMessageListener);
    }

    @Override
    protected void onStop() {
    super.onStop();
    // 移除消息监听
    EMClient.getInstance().chatManager().removeMessageListener(mMessageListener);
    }
    /**
    * --------------------------------- Message Listener -------------------------------------
    * 环信消息监听主要方法
    */
    /**
    * 收到新消息
    *
    * @param list 收到的新消息集合
    */
    @Override
    public void onMessageReceived(List<EMMessage> list) {
    // 循环遍历当前收到的消息
    for (EMMessage message : list) {
    if (message.getFrom().equals(mChatId)) {
    // 设置消息为已读
    mConversation.markMessageAsRead(message.getMsgId());

    // 因为消息监听回调这里是非ui线程,所以要用handler去更新ui
    Message msg = mHandler.obtainMessage();
    msg.what = 0;
    msg.obj = message;
    mHandler.sendMessage(msg);
    } else {
    // 如果消息不是当前会话的消息发送通知栏通知
    }
    }
    }

    /**
    * 收到新的 CMD 消息
    *
    * @param list
    */
    @Override
    public void onCmdMessageReceived(List<EMMessage> list) {
    for (int i = 0; i < list.size(); i++) {
    // 透传消息
    EMMessage cmdMessage = list.get(i);
    EMCmdMessageBody body = (EMCmdMessageBody) cmdMessage.getBody();
    Log.i("lzan13", body.action());
    }
    }

    /**
    * 收到新的已读回执
    *
    * @param list 收到消息已读回执
    */
    @Override
    public void onMessageReadAckReceived(List<EMMessage> list) {
    }

    /**
    * 收到新的发送回执
    * TODO 无效 暂时有bug
    *
    * @param list 收到发送回执的消息集合
    */
    @Override
    public void onMessageDeliveryAckReceived(List<EMMessage> list) {
    }

    /**
    * 消息的状态改变
    *
    * @param message 发生改变的消息
    * @param object 包含改变的消息
    */
    @Override
    public void onMessageChanged(EMMessage message, Object object) {
    }
    }

     界面布局

    界面的实现也是非常简单,这里直接贴一下:
    activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="net.melove.demo.easechat.ECMainActivity">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
    android:id="@+id/ec_edit_chat_id"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="对方的username"/>

    <Button
    android:id="@+id/ec_btn_start_chat"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="发起聊天"/>

    <Button
    android:id="@+id/ec_btn_sign_out"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="退出登录"/>
    </LinearLayout>
    </RelativeLayout>
    activity_login.xml
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="net.melove.demo.easechat.ECLoginActivity">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
    android:id="@+id/ec_edit_username"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="username"/>

    <EditText
    android:id="@+id/ec_edit_password"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="password"/>

    <Button
    android:id="@+id/ec_btn_sign_up"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="注册"/>

    <Button
    android:id="@+id/ec_btn_sign_in"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="登录"/>
    </LinearLayout>
    </RelativeLayout>
    activity_chat.xml
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="net.melove.demo.easechat.ECChatActivity">

    <!--输入框-->
    <RelativeLayout
    android:id="@+id/ec_layout_input"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true">

    <Button
    android:id="@+id/ec_btn_send"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:text="Send"/>

    <EditText
    android:id="@+id/ec_edit_message_input"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_toLeftOf="@id/ec_btn_send"/>
    </RelativeLayout>

    <!--展示消息内容-->
    <TextView
    android:id="@+id/ec_text_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_above="@id/ec_layout_input"
    android:maxLines="15"
    android:scrollbars="vertical"/>
    </RelativeLayout>
    结语

    代码结束,Coding不止!Coding - Coding - Coding —
    OK了,一个简单的注册登录以及收发消息的小demo就算完成了,可以用自己的环境编译运行下试试
     
    本篇Android3.0集成教程由环信Android工程师lzan13编写,已同步发表到个人博客,博客地址lzan13 收起阅读 »

    SDWebImage下载网络图片之提升用户体验

    Number one 描述: 使用UIImage+ webImageCache下载网络图片, 自带内存缓存, 以及沙盒缓存; 基本方法: [self.imageView sd_setImageWithURL: placeholderImage:];方法...
    继续阅读 »
    Number one

    描述: 使用UIImage+ webImageCache下载网络图片, 自带内存缓存, 以及沙盒缓存;
    基本方法:
        [self.imageView sd_setImageWithURL: placeholderImage:];
    方法描述:

    1.根据提供的url下载网络图片,并设置到imageView; 此时内存中会有一份缓存, 沙河中也会写入备份;

    2. 当再次用到该图片时, 首先去内存中获取该图片, 内存中没有则去沙盒中加载, 如果都没有就会去网络下载;

    注意:该方法底层会首先取消imageView之前的任务, 防止数据错乱.
      // 取消iamgeView之前的下载任务
    [self.imageView sd_cancelCurrentImageLoad];

    解析: 因为该方法有一个完成回调block, 当有网络延时时,
    下载图片太慢, imageview循环利用显示其他图片, 但此时前面的下载任务下载好了,
    就会拿到imageView直接设置图片, 会导致数据错乱;

    故在之前调用该方法, 可以防止数据引用;
    方法总结

    1.0 取消当前imageView之前关联的请求
    2.0 设置占位图片到当前的imageview
    3.0 如果缓存(内存, 沙盒)中有图片则直接设置, 不使用占位图片
    4.0 如果没有,则发送请求下载图片

    Number two
    怎么实现缓存,以及获取对应的图片


    无论是内存缓存, 还是沙盒缓存都是以字典的形式保存图片, 因为SDImageCache使用了NSCache类
    key : 图片的url; value: 下载的图片

    Number three
    代码实现:

    核心概念:

    根据不同网络,下载不同图片(省流量)

    内存缓存中, 只保存当前显示的图片

    其他只保存到沙盒, 用到时才会缓存到内存 (提高内存性能)

    1.0 监听用户网络状态

    AFNetworkReachabilityManager
    // 程序启动的时候调用
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 监控网络状态
    [[AFNetworkReachabilityManager sharedManager] startMonitoring];

    return YES;
    }
    2.0 根据不同网络状态, 下载不同图片
    // 占位图片
    UIImage *placeholder = nil;

    // 1.0 从缓存中获得原图
    // 该方法会首先去内存找, 再去沙盒
    UIImage *originalImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:originalImageUrl];

    if (originalImage) { // 如果缓存有原图,那么就直接显示原图(不管现在是什么网络状态)
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:originalImageUrl] placeholderImage:placeholder];

    } else {
    // 2.0 缓存没有原图, 根据网络下载
    AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager]
    // 在使用Wifi, 下载原图
    if (mgr.isReachableViaWiFi) {

    [self.imageView sd_setImageWithURL:[NSURL URLWithString:originalImageUrl] placeholderImage:placeholder];

    // 在使用手机自带网络
    } else if (mgr.isReachableViaWWAN) {
    // 下载小图
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:thumbnailmageUrl] placeholderImage:placeholder];
    } else {
    // 3.0 没有网络
    UIImage *thumbnailImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:thumbnailImageUrl];
    if (thumbnailImage) { // 缓存中有小图
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:thunmnailImage] placeholderImage:placeholder];
    } else {// 没有小图
    [self.imageView sd_setImageWithURL:nil placeholderImage:placeholder];
    }
    }
    }
    3.0 处理缓存图片, 内存飙升问题
    首先:

    SDImageCache怎么处理内存问题?
    // 清空沙盒过期图片 (七天为限)
    - (void)cleanDisk;

    // 清空沙盒内所有图片
    - (void)clearDisk;

    // 清空内存缓存所有图片
    - (void)clearMemory;
    什么时候处理
     // 获取自动清理缓存对象
    _memCache = [[AutoPurgeCache alloc] init];

    // 当接收到内存警告, 才会处理缓存
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    只有内存警告时才会处理?

    为了用户体验故需我们手动释放:
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    // 滚动时, 清除缓存
    [[SDImageCache sharedImageCache] clearMemory];
    }
    解析: 为什么缓存中所有图片都清空了, imageView还能显示图片?

    图片字典的形式保存在缓存中, NSCache有保存所有的key, 每个key指向一个图片对象

    [self.memCache removeAllObjects]会把指向图片的key清掉, 则没有强引用的图片会被释放;

    但是当把图片设置到imageview上时, image指针会强引用图片, 故还能显示图片

    图示:



    1710913-53919159cb9bb0e5.png



    另外一个问题:
    由于使用模拟器测试,发现在滚动过程中, 释放内存造成有时卡顿效果, 故在具体使用时, 还得各自思量;

    tips: 建议把该方法抽取为(UIImageView)一个分类, 以便代码复用;

    有问题, 请留言... 收起阅读 »

    JS区分浏览器中页面刷新与关闭标签页

    文章摘要:js刷新当前页面,Web开发者在系统开发中经常要面对产品经理各式各样的需求,当然,大部分对产品体验还是有帮助的,例如我们今天提到的刷新页面,前进后退,关闭浏览器标签时,为了避免用户误操作,需给出二次确认提示框,这个相信大家都非常熟悉了,采用浏览器提供...
    继续阅读 »
    文章摘要:js刷新当前页面,Web开发者在系统开发中经常要面对产品经理各式各样的需求,当然,大部分对产品体验还是有帮助的,例如我们今天提到的刷新页面,前进后退,关闭浏览器标签时,为了避免用户误操作,需给出二次确认提示框,这个相信大家都非常熟悉了,采用浏览器提供的BOM事件机制就可以...

       Web开发者在系统开发中经常要面对产品经理各式各样的需求,当然,大部分对产品体验还是有帮助的,例如我们今天提到的刷新页面,前进后退,关闭浏览器标签时,为了避免用户误操作,需给出二次确认提示框,这个相信大家都非常熟悉了,采用浏览器提供的BOM事件机制就可以解决,使用window对象的onbeforeunload事件即可,如果产品经理只提出这样的需求,那确实无可厚非,然而其需要的不仅仅是这些...

    例如,我们一次项目开发中,产品经理就针对我们的实现提出了“改进方案”:
    1. 你们这弹出框太丑了,跟系统整体风格不搭调啊,不能使用咱们自己组件库中的Dialog吗?很好的问题...我只想说,you can you up...
    2. 你们这刷新和关闭标签页中展示的文案一样啊,需要区分对待下,刷新提示XXX,关闭时提示SSS,这样用户才能更明确。恩,考虑到了用户的体验,很好,我还是想说,you can you up...其实,浏览器在关闭和刷新时,本身已经区别对待了,提示是不同的,只不过我们自定义的部分并不能显示不同的文案而已;当然,也有一些hack的方法,但是很难适应多个浏览器,各浏览器内部对于关闭标签页和刷新的实现机制会有所不同;
    3. 你们每次登录进来,为什么要延时10秒,才让坐席签入电话系统啊(我们做的是客服系统)?能不能把这个限制去掉啊,用户体验太不好了!我们也想去掉啊,但是电话系统频繁签入签出会有问题,用户刷新了浏览器,再次签入,如果相隔时间很短的话,电话系统会出现故障,为了避免这个问题,我们才加上了这个限制,但是回过头来思考,就可以进入我们今天讨论的主题了;



    区分刷新与关闭标签页

    我们无法根据浏览器事件区分刷新还是关闭标签页,进而在相应动作触发前,执行不同的动作,但是对于上文中产品提出的第三点意见,其实还是可以考虑优化一下的,就是只有在刷新的时候延时10秒,新登录或关闭标签页一段时间之后再进来时不延时;

    要做到这点其实也很简单,使用浏览器的本地存储机制就可以实现,例如cookie,LocalStorage等,这里就不能使用SessionStorage了,因为本次回话结束后,该缓存就失效了;由于在cookie中存储会增加cookie的字节数,每次请求中相应的网络传输量会增加,因此,我们采用了LocalStorage;其操作很简单,我们使用的前端框架是AngularJS,具体如下:
    const MAX_WAIT_TIME = 10;
    const currentDate = new Date().getTime();
    const lastestLeaveTime = parseInt(this.$window.localStorage.getItem('lastestLeaveTime'), 10) || currentDate;
    this.secondCounter = Math.max(MAX_WAIT_TIME - Math.ceil((currentDate - lastestLeaveTime) / 1000), 0);
    if (this.secondCounter > 0) {
    this.logoutTimeInterval = this.$interval(()=> {
    this.secondCounter--;
    this.$scope.$digest();
    }, 1000, this.secondCounter, false).then(() => {
    this.updateByStatus(this.AvayaService.status.OFFLINE);
    });
    } else {
    this.updateByStatus(this.AvayaService.status.OFFLINE);
    }
    上面代码主要作用是,进入系统后,会先去LocalStorage中获取上次退出时的时间,再获取当前时间,两个时间进行减法,如果值小于10秒,我们就认为这是刷新,如果值大于10秒,我们认为是关闭标签页或新登录,进而可以执行不同的方法,让客服有更好的体验,不用每次进入系统都要等待10秒才能签入电话系统了,产品经理还是很重要的,吼吼,要不是他的疑问,可能我们也不会来优化这个地方了...当然,其实RD也要逐渐培养这种用户体验至上的思维,哪怕有一点可提升客服效率的地方,都值得我们花时间来优化;

    下面把相关退出的代码也贴一下吧,前面忘说了,不管是刷新,还是关闭标签页,只要是页面销毁,我们都会去执行登出电话系统的操作,所以每次进来后需要重新签入;
    //刷新页面或者关闭页面
    $window.onbeforeunload = () => {
    return '操作将会导致页面数据清空,请谨慎操作...';
    };
    //每次页面unload时,设置LocalStorage时间;
    $window.onunload = () => {
    $window.localStorage.setItem('lastestLeaveTime', new Date().getTime());
    };
    我们可能还注意到一些问题,那就是刷新,关闭页面,前进后退,你需要跳出浏览器默认二次确认框,但是用户点击退出系统按钮,则必须弹出自己组件库中的Dialog了,还必须不能两个都弹出,具体代码如下:
    onStatusClick(index, name) {
    if (name === '退出') {
    this.mgDialog.openConfirm({
    showClose: false,
    template: 'app/header/logoutDialog.html',
    controller: 'HeaderDialogController as dialog',
    data: {
    'title': '您确定要退出系统吗?'
    }
    }).then(() => {
    this.$window.location.href = '/logout';
    this.$window.onbeforeunload = null;
    });
    } else {
    // 内部操作,大家不用管
    ...
    }
    }
    收起阅读 »

    一乐 :煎饼果子与架构模式

    编者按:本文由一乐在高可用架构群分享,转载请注明来自高可用架构「 ArchNotes 」。 一乐是我,也是梁宇鹏。现任环信首席架构师兼 IM 技术总监,负责即时通讯云平台的整体研发和管理。曾任新浪微博通讯技术专家,负责微博通讯系统的设计与研发。一直专...
    继续阅读 »



    640.webp_.jpg



    编者按:本文由一乐在高可用架构群分享,转载请注明来自高可用架构「 ArchNotes 」。



    640.webp_(1)_.jpg

    一乐是我,也是梁宇鹏。现任环信首席架构师兼 IM 技术总监,负责即时通讯云平台的整体研发和管理。曾任新浪微博通讯技术专家,负责微博通讯系统的设计与研发。一直专注在即时通讯领域,熟悉 XMPP 协议和相关的开源实现(包括 Jabberd2、EJabberd、Openfire)。关注分布式系统和高性能服务,关注 Erlang、Golang 和各种浪。



    ​煎饼的故事


    有一段时间住在花园路,最难忘的就是路边的煎饼果子。老板每天晚上出来,正好是我加班回去的时间。

    一勺面糊洒在锅上,刮子转一圈,再打一个蛋,依然刮平。然后啪的一下反过来,涂上辣酱,撒上葱花。空出手来,剥一根火腿肠。最后放上薄脆,咔咔咔三铲子断成三边直的长方形,折起来正好握在手中。烫烫的,一口咬下去,蛋香、酱辣、肠鲜,加上薄脆的声音和葱花的惊喜,所有的疲劳都一扫而光。





    640.webp_(2)_.jpg




    这种幸福感让我如此迷恋,以至于会在深宅的周末,穿戴整齐跑出去,就为了吃上一个。也因为理工科的恶习,我也情不自禁地开始思考这份执迷的原因,直到最后,我发现了它的秘密。
     
    作为街头小吃的杰出代表,能够经历众口的挑剔而长盛不衰的秘密是什么?


     
     所有的一切全因为其模式。


    而这模式与大多数互联网服务的架构如出一辙,那就是分层架构。

    分层的设计意味着,每一层都独立承担单一的职责。

    这在根本上降低了制作的难度。做饼的时候专心控制火候,做酱的时候专注在味道。每层职责的单一化也让优化变得简单,因为它是自然可伸缩的。你要是想多吃点蛋就多加一个,你要多吃点肠就多加一根,完全取决于你的胃口。
     
    它又是可以扩展的。你可以不要蛋,你可以加根肠,你可以不要薄脆,你可以加上辣酱。而且每一层又是可定制的。葱花可以少一点,辣酱可以多一点,肠可以要两根,鸡蛋可以加三个。

    你也可以把面饼换成面包,把鸡蛋换成煎蛋,把辣酱换成甜酱。你已经知道这是什么了吧?是的,你好,这里是赛百味,请问你要什么口味的三明治?

    煎饼果子和三明治,其实本质上是相通的。而加一个蛋更香,也不是因为对蛋的追求,在根本上是因为煎饼果子模式的强大。



    因为这种模式,一千个人可以有一千种煎饼果子。


    而有了对模式的理解,对小吃的评估也就变得更加容易。比如肉夹馍只有馍和肉,二维切换单调不可长久;比如烤冷面,干脆就是满嘴的热烈混在一起,没有煎饼这样的表现层,整个面都散发着原始的不讲究。

    这种对比上的简化,也让我们有了新的选择,保存宝贵的精力,并且可以随时放弃对细节的追究。




    640.webp_(1)_.jpg


    就像在我们谈论女人时,

    (抱歉博主是男人)

    我们在意胸、

    在意腿、

    在意风情、

    在意温柔,

    因为女人是有区别的。

    当我们欣赏电影的时候,

    我们在意男人、

    在意女人、

    在意老人、

    在意孩子,

    因为角色是有区别的。

    当我们走在路上的时候,

    我们在意行人、

    在意车辆、

    在意商店、

    在意餐厅,

    因为物体是有区别的。



    我们在设计和优化系统的时候,其中的每个服务都是自行运转,做着自己份内的事,但是在不同的维度里,作用却变得不尽相同。

    这也是我们讲优化要分层次和级别,架构、算法、库和 OS,而讲架构的时候,我们首先讲的是整体的模式,然后是具体的权衡,实现的细节则是最不重要的。



    架构的模式


    谈起这个,是因为 Mark Richards 写了一本架构模式的书《Software Architecture Pattens》。

    书中总结对比了五种模式的优缺点,包括了 Layered、Event-Driven、Microkernel、Microservices、Space-Based。

    书写得简单精致,推荐大家去阅读,地址见文末。

    还有一种模式,因为在越来越多的系统中用到,是书中没有的(与 Space-Based 有所区别),但我觉得也有必要专门介绍下。我们开始在群发系统中实现,后来的抢购、红包和火车票的场景中也屡屡看到它的身影。


    2013 年的时候,我们在微博做粉丝服务平台,一个类似微信公众号的群发系统。然而比后者更困难的是,当时在产品设计上并没有像微信一样新建用户体系,而是直接基于微博的粉丝关系,这就意味着一篇文章要能能在很短时间内支持亿级的用户推送。这个数量级的订阅用户,即使看今天的微信公众号依然是难以想象的。
    当时有一套老的群发系统,都是基于 MySQL 的收件箱设计,在更换了 SSD 硬盘,又批量化数据库操作之后,整体写入性能依然只在每秒几万的级别,这就意味着一亿用户只能在 17 分钟内发完,我们意识到这套系统需要进行重新设计。

    最终我们我们使用了一种新的架构方式,达到了每秒百万级别的速度,而且还可以更高。这种模式就是单元化架构。

    下文介绍我参照了架构模式的说明方式,希望能够让大家有个对比,喜欢你可一定要说好!



    单元化架构


    如前所述,我们选择单元化的一个重要目的是为了性能,为了极高的性能。这比起一般的分层架构来讲,会获得更经济的结果,但也因此,牺牲了分层架构的一些特性,因为它的容量取决于单元的大小(关于单元等名词介绍,我会在下文介绍)。

    虽然它支持按照单元扩容,但在单元内基本上每层的性能都是固定的。这更适合容量可预期的场景,比如大多数已经趋于稳定的业务。像前面的粉丝服务平台,虽然他下发消息量级巨大,但是在整体层面,使用平台的用户由于是 VIP 用户,其规模基本在在数百万级别,而粉丝量级也不太可能过亿。

    重要的是,基于当时的业务数据,我们已经知道平均粉丝数在什么量级。而业务数据是架构选型的重要依据。商品秒杀、火车抢票等等都是一样。
     
    当然也有例外。因为单元化架构作为一种思想,它不会局限在一台机器,一个机架,它也适用一个机房。当它的层次变大时,单元内自然就可以有变化的空间。每一层服务都可以分开伸缩。而到了这个层面,它的追求可能就完全不一样了。像阿里的双十一服务改造,会为了流量的分离,像 QQ 的聊天,会为了接入的速度。它们的基本思想是一致的。

    至于单元化架构和煎饼果子的关系,我会在文后回答。




    640.webp_(3)_.jpg



    核心概念 Key Concepts


    分区(Shard)是整体数据集的一个子集。如果你用尾号来划分用户,那么相同尾号的用户可以认为是同一分区。

    单元(Cell)是满足某个分区所有业务操作的自包含的安装。我们从并行计算领域里借鉴了这个思想,也就是计算机体系结构里的 Celluar Architecture,在那里一个 Cell 是一个包含了线程部件、内存以及通讯组件的计算节点。 https://en.m.wikipedia.org/wiki/Cellular_architecture

    单元化(Cellize)这是我的自造词,描述一个服务改造成单元架构的过程。



    模式描述 Patten Description


    单元架构最重要的概念,就是单元和单元的自治

    你可以将其想象成细胞,如之前所述,每个细胞都是自成一体,功能明确。你也可以将其想象成小隔间,就像你去了一个按摩院,每个隔间里都有技师和所有设备。我没有用前面那个名字,因为其有太强的生物学含义,也没有用后者,因为其有太多的服务性暗示。但是如果你有足够的想象力,其实什么名字都可以的。

    说到单元的自治,即单元的自我协调和之间的隔离。单元既然做到了自包含,那么其中的所有组件,不管是否在物理上分离成了独立的服务,都是在一个单元内互相支持的,也就是跟其他单元内的同类和非同类组件都不会有任何交流。这也是跟基于空间的架构的重要区别,后者的处理单元之间还是会互相通信并同步信息。

    这里的挑战就在于分区的算法。一个单元内的组件会很多,如果业务复杂,涉及到的数据也会很多,为了隔离,每一个组件都要能按照同样的算法进行分区。

    本质上每个单元都是相似的,单元之间的区别或者取决于请求,或者取决于数据。而且越到大的层面,区分度越低,用户甚至是可以在不同单元间漫游的。



    模式动力学 Patten Dynamics


    单元架构的最典型目的,还是为了极高的性能,为了获得经济的高速度。这里一方面,是因为我们发现其他架构实际上是浪费了很多资源,每一层服务都运行在单独的操作系统上,而且都要通过局域网或者城域网中转。

    与此同时,传统的互联网服务还是希望用一堆计算能力普通的节点来服务大量用户,而随着摩尔定律的推进,单机性能越来越高,网络通讯的成本随之变得耗费显著。这使得我们有机会也有动力在垂直方向进行扩展。

    当你把更多的组件放在同一个地方的时候,你也在物理上获得了计算本地化的优势。这是我们获得性能提升的根本原因。

    服务分成了很多单元,但总要跟外界通讯,这个事情是交给协调者 Coordinator 的。你可以在内部增加存储、缓存,增加队列和处理机,这些所有不交互的组件,理论上都不是外部资源可以访问的。

    前面我们提到,单元化过程也是分区算法的应用过程。而这个分区算法放在哪里就是个问题。

    我们可以封装运行库交给客户端,也可以做个代理层,内置算法。也有一些服务因为业务需要,请求需要复制到每个单元去。这就是典型的 Scatter-Gatter 模型,那么你还可能需要一个作业管理系统。这些都是可选择的使用方式。



    模式分析 Patten Analysis


    总体敏捷度低,易部署性低,可测试性高,性能高,伸缩性高,易开发性低。

    基于篇幅原因,不再详述每一个方面,相信大家都能自行分析。唯一需要强调的是运维要求比较高。

    单元化之后,所有的服务放在一起,在请求失败的情况下需要快速定位某个单元,这跟分层排除的思路是不一样的。如果运维团队不够高效,面对这样集群数量的暴涨(每个单元的服务数量相当于原来一个集群的服务数量),有可能是会被大量的工作压垮;如果运维团队分离比较明显,每种组件都是专门的团队来维护(这是我们在微博遇到的),那就会有排异反应的风险,因为每一个团队都有自己的权限和服务管理习惯,这里需要相当的协调工作来防止相互干扰。



    后记

    前面留的一个问题,煎饼果子跟单元化架构的关系。答案说起来很简单,你问问煎饼摊就知道了。

    煎饼是分层的,煎饼摊是单元的。消息发送服务是单元的,但是索引维护是分层的。看模式要确定系统的范畴,从不同角度看,同样的东西是有不同意义的。这也是架构师要做的思考。

    其实 IT 系统千百万,模式肯定不会止于这几种。但有了基础的模式,了解它们之间的相似和区别,对于我们设计自己的系统,思考其中的权衡都是有帮助的。



    Q & A

    1. 单元化设计与 Docker 的容器化思路是否相通?又有何差异呢?

    一乐:应该是关注的点不一样,但不冲突。Docker 一般是在微服务架构下会使用的措施,但并不意味着不能用在其他架构上。一个单元内的各种组件,使用什么样的技术,都是新的选择。用 Docker 不错。

    2. 对于分布式服务,单元化架构可能会带来数据一致性问题,这个一般如何解决?

    一乐:可能我没有理解你场景,单元化一般不会带来一致性问题。因为 Sharding 之后,一块分区数据相当于完全属于一个单元,其他单元是被隔离访问的。


    3. 单元化架构最小情况是单台机部署整体服务,资源方面如何规划?

    一乐:这方面就要计算了,也就是进行容量规划,相信大家在这方面都很熟悉。一个需要注意的点是,单元化的架构应用在可预期的总体容量上时会省很多事。鉴于分区算法的固定,扩容方面,其实可以通过预先规划,在单机上再进行多单元混部的方式。

    4. 单元与 app cluster(总服务器)之间是怎么进行关联的,是通过注册服务还是按照 hash 分派的,如果是 hash 分派,那么挂了怎么接回的,如果是注册服务,是怎么对应分派服务呢?

    一乐:简单做就是 hash 分派,高级点就是注册服务,可以直接参照成熟的 Sharding 算法。任务只要到了单元内,就是单元自己的事了。如果你的 Job 有阶段性,可能要考虑 Job 状态记录以及请求处理的幂等性

    5. 你觉得单元化架构的问题是什么?如果让你重新设计你会做哪些改进?

    一乐:这个问题太聪明了,谢谢!单元化架构的问题,如之前所说,有一个扩容难题,有一个运维难题,有一个单点问题。扩容问题刚才说了,在三年前我们还没有 Docker 的时候,我们的服务隔离难度很大,现在已经今非昔比。一刀(Docker)在手,天下我有!单元的单点问题,你得考虑单元级别的主从同步,这方面常见的互联网技术就行。

    6. 分布式架构经常讲究服务器无状态,这样可以单台服务器异常对整体服务无影响。但单元化意味着服务是有状态,如何保障高可用?

    一乐:无状态只能是业务层,涉及到数据的不会无状态,因为数据就是状态的记录。保障高可用嘛,前面说了,先把主从做了吧。

    7. 单元细胞内依然采用分层架构设计还是 ALL IN ONE 即可?

    一乐:在讲单元化的时候,其实不要求单元内的组织模式,所以回答是都行。

    8. “每秒百万级的推送” 除了采用这种单元化的架构模式使之成为可能对于基础的中间件如 队列 db 等的架构如何规划的?还要额外考虑哪些技术点或问题?

    一乐:队列主要用来防峰,在高速服务里,如果再想扩容,肯定先走批量的路子。db 的规划其实是重点,单元化架构实际上算是以数据为中心的一种模式。额外的考虑其实跟之前都很像,不过要做一些特殊的处理,比如冷热数据的分离。



    参考阅读

    一个单元化架构的例子 http://t.cn/RqM5Ns0

    Software Architecture Patterns http://www.oreilly.com/programming/free/software-architecture-patterns.csp

    校长:技术成长四个阶段需要的架构知识
     
    支撑微博千亿调用的轻量级RPC框架:Motan

    Upsync:微博开源基于Nginx容器动态流量管理方案



    小编:一乐最近也开了公众号,内容非常有特色,将复杂的道理说得如煎饼这么通透,所以别忘了识别二维码关注,错过这次小编琢磨最短都要再等一年。



    640.webp_(4)_.jpg



    本文策划庆丰,编辑王杰,想讨论更多架构设计,请关注公众号获取进群机会。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。



    640.webp_(5)_.jpg



    收起阅读 »

    iOS 消息扩展

    iOS 消息扩展 好友列表中通过自定义扩展消息来传好友的头像和昵称。     发送之前把头像和昵称通过EMMessage传递给对方(发送消息不光发给对方消息,并且把头像和昵称发给对方),接收的时候解析出来 EaseSDKHelper.m这个类里有一个发送方法 ...
    继续阅读 »
    iOS 消息扩展
    好友列表中通过自定义扩展消息来传好友的头像和昵称。
     
     
    发送之前把头像和昵称通过EMMessage传递给对方(发送消息不光发给对方消息,并且把头像和昵称发给对方),接收的时候解析出来
    EaseSDKHelper.m这个类里有一个发送方法 发送消息的时候通过消息扩展传给对方
    #pragma mark - send message
    + (EMMessage *)sendTextMessage:(NSString *)text

                                to:(NSString *)toUser

                       messageType:(EMMessageType)messageType

                 requireEncryption:(BOOL)requireEncryption

                        messageExt:(NSDictionary *)messageExt{
    ....
     
     message.ext = @{@"logo":@"头像",@"name":@"名字"};
     
    ....
    }
    消息页面的头像和昵称
    EaseConversationCell.h
    - (void)setModel:(id<IConversationModel>)model{
    头像换成message.ext[@"logo"]
    昵称换成message.ext[@"name"]
     
      if (self.model.isSender) {
            [self.avatarView sd_setImageWithURL:[NSURL URLWithString:@"http://img5.imgtn.bdimg.com/it/u=159694920,2166605543&fm=21&gp=0.jpg"] placeholderImage:model.avatarImage];
            _nameLabel.text = @"自己的昵称";
        }
        else{

            [self.avatarView sd_setImageWithURL:[NSURL URLWithString:@"对方头像地址"] placeholderImage:model.avatarImage];
            _nameLabel.text = @"对方的昵称";
        }
    }
    消息详情的头像和昵称(同理)
    EaseMessageCell.h
    - (void)setModel:(id<IMessageModel>)model{
    头像换成message.ext[@"logo"]
    昵称换成message.ext[@"name"]
     
    }
    做的时候花了好多时间研究  ,希望对后人有所帮助。 收起阅读 »

    环信:全媒体智能客服时代的最佳实践

    随着企业信息化需求的复杂多变,客户服务和体验理念的人性化和智能化,企业通信以及呼叫中心一直是ICT市场上不容忽视的领域。4月14日-15日,由CTI论坛主办的“2016中国呼叫中心及企业通信大会”在北京盛大召开,本次会议以“联络中心的数字化DNA”为主题,围绕...
    继续阅读 »

    随着企业信息化需求的复杂多变,客户服务和体验理念的人性化和智能化,企业通信以及呼叫中心一直是ICT市场上不容忽视的领域。4月14日-15日,由CTI论坛主办的“2016中国呼叫中心及企业通信大会”在北京盛大召开,本次会议以“联络中心的数字化DNA”为主题,围绕企业通信、呼叫中心、下一代通信架构等内容为话题,继续推动ICT产业发展。包括环信等40余家领先的行业供应商展出了其最新产品和服务。同时,环信CEO刘俊彦受邀发表主题演讲,用环信的真实用户案例来诠释什么才是全媒体智能客服时代的最佳实践!



    4840007ba6db56ef75e.jpg


    环信移动客服特装展台人头攒动,现场签约成功数单




    4830007860cb78fb188.jpg


    环信CEO主题演讲,全程无尿点



    随着云计算、大数据、移动互联网以及社交媒体的兴起,传统通信模式、企业和客户的交互方式和沟通形态,以及服务理念正面临着前所未有的问题和挑战,新的行业格局也正在形成。2015年作为中国企业级服务元年,同时也是SaaS服务元年,尤其在客服领域诞生了一大批以产品体验和技术创新驱动的SaaS公司,而以全媒体、智能化、移动化为主打的环信移动客服更是其中的翘楚,不到一年时间内服务了1万多家企业,并且在融资额和市场份额上也遥遥领先。

    什么是全媒体客服? 
     
    环信CEO总结到:“来自不同媒体的服务请求均可以统一接入,一键回复,打造跨网、跨界、跨平台的极致客户服务体验。”链家自如客使用环信实现了全媒体客服,环信帮助链家自如客打通了来自App端的客服入口+网页端客服入口+微信端客服入口,不仅可以统一接入回复且后台数据打通共享。帮助链家自如客优化了客服团队,极大的提升了效率,节省了成本。



    4840007bb08434213c5.jpg


    链家自如客使用环信实现全媒体客服




    48600077a10fe4bb80d.jpg


    同时,环信还提供“一体化”客服工作台,支持从APP、微信公众号、微博、网页、呼叫中心等渠道接入,且每个不同渠道均有不同标识进行识别。



    从传统呼叫中心到全媒体客服,一场“效率”革命 
     
    随着人口红利消失,呼叫中心的升级转型将越来越普遍,2015年中国劳动力规模由2012年的9.37亿降至9.11亿人。中国劳动力人口连续4年绝对值下降企业客服面临的“用工荒”将持续扩大,运营成本将越来越高,越来越多的企业将复制环信客户“学而思”的客服转型之路,从语音呼叫中心为主转而采用全媒体客服,拥抱移动互联网。 
     
    学而思以前部署有4套客服产品包括:1,呼叫中心。2,网页客服。3,新媒体客服。4,APP客服。同时对应4个客服团队支持,相互数据不打通,尤其自开通微信客服后,咨询量增长明显,由用户数据不打通带来的用户投诉增多。而且每年10—11月是教育行业的交费季,以前主要靠呼叫中心外呼,工作量巨大,效果不满意。学而思在2015年集成环信移动客服以后,服务模式改为APP内缴费,并在APP内提供客服支持。整个2015年缴费季,APP客服部门数十人完美解决了往年数百个语音客服的工作。环信CEO刘俊彦认为:“APP客服相比电话客服大幅度提供服务效率,全渠道客服也已经成为企业刚需。”
     
     IVR进入触摸时代,自助服务的未来,智能机器人将大显神通 
     
    传统IVR,用户需要听完所有菜单再做选择。而现在主流的ITR导航,用户只需在手机上直接选择关注的问题,简单方便。其中神州租车就采购环信移动客服,其中的智能ITR大大缓解了人工客服压力。Gartner预测到2020年,ITR将完全取代IVR全面进入触摸时代。



    483000786baec35b6f9.jpg


    神州租车使用环信智能ITR缓解人工客服压力



    近期李世石1:4不敌AlphaGo的事件又将人工智能推上了风口浪尖。而环信是客服行业少数自主开发智能应答机器人产品的公司,环信知识库+智能聊天机器人可以帮助人工坐席挡住80%的常见问题。同时具有以下特性:1,灵活可定制的智能会话、自定义菜单导航功能。2,预置的行业知识库,行业相关的常见问答可以一键拥有。3,与现有知识库系统对接,机器学习,智能优化知识库。4,人机无缝配合,更少的成本,更好的客户体验 。



    4850007c51a7cfd3781.jpg


    环信首推的人机混合服务极大提高客服应答效率

    环信CEO表示“人机混合服务”将是现阶段最适合也是最具效率的客户服务方式。



    服务式营销,从成本中心转向利润中心 
     
    随着客服中心不断的被新时代赋予新的含义,传统的客服中心也正逐渐从成本中心向营销中心和利润中心转化。其中移动电商标杆企业楚楚街就使用环信APP IM长连接技术实现了精准商品推荐。楚楚街精准营销四步走:1,通过“客户标签”功能+“大数据分析”找到目标群体。2,通过环信移动客服的精准营销推送接口,将富媒体商品信息定向推送给目标客户。3,只要APP没被卸载,哪怕APP在后台,用户手机都能收到消息推送。4,客户选择咨询或直接购买。



    4870007c4fb88cecca9.jpg


    移动互联网的电话外呼——金融界为电话销售配置环信APP主动营销平台




    483000787311ce78569.jpg




    近期,环信还上线了业界首个“客户声音”产品,可以通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,来帮助企业更好的来倾听客户的声音。 
     
    最后,环信CEO预测:“随着国内SaaS客服产品的逐渐成熟完善,中小企业将全面拥抱SaaS客服,建设全媒体客户关系中心。而传统大型企业也将增量部署全媒体客服,保护已有投资,拥抱移动互联网。”SaaS客服也将逐渐成长为一个千亿级市场。同时,环信CEO认为未来远程办公,移动办公和众包客服将解决客服行业人力资源不足的问题,而环信移动客服的手机端工作后台将提供很大的助力。 
     
    环信移动客服简介
     
    环信移动客服是全球首创的全媒体智能云客服平台。支持全媒体接入,包括网页在线客服、社交媒体客服(微博、微信)、移动端客服和呼叫中心等多种渠道。环信移动客服基于环信业界领先的IM长连接技术保证消息必达,并通过强大的智能机器人技术极大降低人工客服工作量。 
     
    环信移动客服于2014年12月上线,截至2015年底,环信移动客服共服务了12000家企业用户,现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、楚楚街、随手记、海尔、51talk,链家自如客等众多互联网和传统企业。根据易观国际发布的《中国SaaS客服市场专题研究报告2015》显示:截至2015年第三季度,环信移动客服在SaaS移动端客服用户覆盖占比为77.4%,以绝对优势稳居行业第一。


      收起阅读 »

    客户世界专访环信CEO:选择长赛道、主打全媒体、拥抱黑科技

    SaaS(Software as a Service)技术在国内呼叫中心领域已兴起多年,但直到近两年才形成风暴式的发展。当前,不仅新型互联网企业愿意采用这种布署灵活、低成本、高效率的云端技术,大量传统企业也在服务升级的推动下向SaaS模式的客服系统过渡。据行业...
    继续阅读 »

    SaaS(Software as a Service)技术在国内呼叫中心领域已兴起多年,但直到近两年才形成风暴式的发展。当前,不仅新型互联网企业愿意采用这种布署灵活、低成本、高效率的云端技术,大量传统企业也在服务升级的推动下向SaaS模式的客服系统过渡。据行业估测国内客服软件市场需求在200亿人民币左右,未来几年中国SaaS市场将保持30%以上的年复合增长率。2015年以来,随着2B市场被资本引爆,客服行业涌现出一批专注于多渠道、智能化一体的SaaS型企业,在激烈角逐的同时也为行业带来更多创新活力。




    484000662ee615d61f3.jpg




    环信创立于2013年,经过不到三年的发展,其产品已从最初的即时通迅云平台,发展到智能移动客服系统、全渠道智能客服平台。2015年,环信的移动客服产品荣获工信部颁发“最具成长性APP应用奖”;同年10月,荣获第十一届“金耳唛杯”中国最佳客户中心技术产品奖。2015年底,在易观发布的《2015中国SaaS客服市场专题研究报告》中,环信移动客服在SaaS客服移动端用户覆盖占比77.4%,居国内市场第一。目前环信已经获得四轮,总计2200万美元的融资,成为即时通迅云和SaaS客服领域融资最快、资金最充裕的平台。 
     
    近期,《客户世界》杂志对环信即时通信云CEO刘俊彦进行了专访,刘俊彦先生畅谈了环信过去3年的成长历程及对未来市场的展望。 
     
    一、创造风口与把握机遇 
     
    2013年,现在已更名为中关村创业大街的海淀图书城里有一家远近闻名的“车库咖啡”,是当时IT创业者的聚集地,环信即起步于此。
     
     2013年陌陌上市前后,社交软件的需求一时间变得非常之火,刘俊彦是这个领域的专家,当时找他咨询如何为APP开发聊天功能的人特别多,而单独开发一套APP聊天功能非常费时费力,于是他想到将即时通讯功能做成云服务的形式让用户自主集成,这样可以高效地帮助创业者发展。 
     
    “即时通迅云”作为即时通迅领域的创新概念由环信首次提出,产品推出后,随即获得了创业者和众多企业的认可和追捧,很短的时间内平台上就发展出上万家企业用户。“风口”的效应其实是无意间被创造出来的。
     
     据刘俊彦介绍,环信的四位联合创始人都是技术出身,他本人曾就职于RedHat(红帽)公司,有着多年开源社区项目的开发经验。创业团队平均年龄在四十岁左右,算是中年创业,因为之前都在外企工作收入比较高,所以在创业之初没有太多经济压力,也非常清楚自己的优势。选择好了产品方向,大家就本着务实的原则向前努力,最初的办公地点就在“车库咖啡”,“七八个人占两张桌子,每天早晨来上班还要占座。”
     
     2014年5月,车库咖啡对面开了一家叫“36氪”的孵化器。开业的前一天刘俊彦和伙伴儿们过去转了转,就碰到氪空间负责人,一番交流后对方马上邀请他们入驻“氪空间”。他们周一搬家刚入驻,下午就来了两拔VC,其中就有经纬的投资人,聊了两个小时后,第二天就签署了协议。
     
     “第一次融资可以说是一个非常偶然、随意的过程,”刘俊彦说,“我们在创业的时候并没有融资的想法,也没想到以后要怎么挣钱,会有什么样的商业模式。唯一可以想象的是,如果有几万个APP在用我们的产品,每天有几亿人连接到我们的服务器上,用我们的聊天功能谈论社会热点,交友,购物,到那个时间点价值一定会显现出来。”
     
     在资本的支持下,公司很快走上正轨。2014年10月起,环信投入大量资源开发移动客服产品,也即从原有的PasS(PlatformasaService)平台向上延伸,做SaaS产品。 “从即时通迅云上线的第一天起,就不断有用户来找我们,希望把聊天功能做成像淘宝旺旺那样的可以嵌入到APP里的客服工具。因此,移动端的客服产品可以说是在用户真实需求的趋动下自然而然地开发出来的。开始的时候你其实不知道商业模式,当你每天接触几百个用户,有几千个公司、几万个人在用你的产品的时候,商业模式自然而然就形成了。” 
     
    2015年4月环信移动客服上线,在当时的时间点上,APP中有内置客服的产品只有“淘宝旺旺”和京东的“叮咚”。通过把PaaS平台的老用户成功转化为SaaS平台的新客户,环信在自己创造的风口上再次成为了移动端客服软件的领跑者。2015年5月环信获得B轮1250万美金融资,签约付费客服席位4万个,典型用户包括国美在线、58到家、楚楚街9块9等上百家互联网知名企业。 
     
    2016年伊始,环信基于当前客服市场对统一解决方案的需求再次推出全渠道智能客服产品。
     
     二.以机器人、大数据确立竞争优势
     
    据2015年“双十一”天猫的销售统计数据,接近70%的订单来自移动客户端,这意味着客户服务未来会越来越多地向移动端倾斜。同时,社交媒体的发展不仅连接了人与人、人与商业,未来还将连接一切,因此这种连接的价值一定要在未来的客服软件中体现出来,而客户不仅仅是通过微博、微信等社交工具向企业寻求帮助,还会有更多的用户之间的互动与互助需求需要被满足。刘俊彦认为移动化、社交化、智能化与个性化是未来客服软件产品应该具备的特性。未来客服工具不仅要具备全渠道接入、质检、统计、知识库查询等基础功能,还需要智能化和大数据的能力来帮助客户更好地提升服务效率并进行精准的二次营销,帮助企业客服中心成为真正的利润中心和价值中心。此外,SaaS客服软件具有的定制化优势可以很好地满足客户个性化需求,企业用户可根据自己的特点和需求自定义设置系统模块。
     
     针对未来客服产品的发展趋势,环信产品目前除了拥有完善的基础功能外,还在机器人研发及大数据分析方面做了大量的投入和部署。据刘俊彦介绍,目前人机混合的智能机器人问答、客户画像等先进技术已进入生产内测环节。未来环信产品的差异化优势除了原有IM长连接技术外,将更多体现在商业化智能分析、智能机器人问答、大数据分析与挖掘等方面。黑科技领域的技术优势将在下半年的竞争中逐渐显现出来。 
     
    “我们现在做人机混和,当机器人回答不了客户问题时,会转人工座席,人工座席回答的同时,我们会把会话同时‘喂’给机器人,机器人不断的自动学习人工坐席的回答,并根据知识库和以前的知识积累,自动提示座席回答内容,座席看到提示靠谱,只要点击一下就可以发送给客户,这对工作效率是极大的提高。”
     
     刘俊彦说:“资本投入上的差异是非常关键的,研发上的投入一定能从产品上体现出来。我们花了大力气和大价钱找到最优秀的人才加入我们。像人工智能、机器学习领域的顶尖人才,说服他们加入,光靠好的待遇是不够的,更重要的是用愿景来打动他们。”环信目前拥有180名员工,其中90是研发人员,是一支技术导向型的团队。 
     
    统计资料显示,中国目前企业总数5000万家,而IT普及率不及5%。对标美国市场,美国的企业总数不及中国企业数量多,但IT普及率达到50%。在企业服务领域,美国有四五家几千亿美金市值的巨头公司,如微软、SAP、ORACLE等,但在中国SaaS领域还没有大型公司出现。当前中国大量的企业在做“互联网+” ,谁来满足这批用户的需求,就有可能成为中国企业级服务市场的巨头。“这是一个非常大的增量市场,我们能吃下十个亿就是一个很好的公司了。”刘俊彦对市场未来充满信心。 
     
    有人把未来世界的产业格局划分为三个维度,一维:传统产业;二维:互联网产业;三维:智能科技产业。而将二维产业升级为三维产业的关键力量是大数据的崛起。未来一切竞争归结到最后都是数据的竞争。当前,智能客服机器人和大数据技术是带动整个服务产业升极的核心技术。
     
     从用户需求出发,选择长赛道、拥抱尖端技术,做市场的引领者是环信始终的选择。
     
     三.访谈实录 
     
    Q:目前的很多大型传统型企业已经有了成型的传统呼叫中心平台,您如何看待他们对全渠道平台的引入?
     
    A:这些企业之前自建投入非常大,我们的策略是让他们在继续保留这些投资的同时,帮助他们拥抱移动互联网。我们提供一套在线客服系统,让他们可以连接微博微信网页和APP,不仅有KPI考核、统计等常用功能,还有先进的机器人客服,可以和原有呼叫中心无缝打通,比如一个人以前通过电话找过你,再通过微信,网页等渠道进来,你依然可以识别他,并且知道之前的交互记录,数据是完全互通的。而且还能做统一排队,比如一个服务交互在微信上没有解决问题,客服人员提示客户电话接入,这时电话再次接入到的一定是刚才服务过你的那个座席。因此客户可以在投入不大的情况下就可完成“互联网+”的升级。
     
     Q:您提到统一排队的概念,我想深入问一下,这也是我在很多客户那里遇到的问题 。因为在线渠道是即时的,语音渠道是实时的,比如一个提供三种服务的座席,也叫全技能座席,正在接听一通电话,这时有一个实时的微信推送过来。正常来讲排队系统应该能识别到座席的状态不能接入这个业务,对于这个问题,您是怎么解决的? 
     
    A:这是很多做客服软件的厂家遇到的一个特别大的困扰。我们针对大型用户的解决方案一般会把语音座席和即时座席分开,因为实时和即时不可能做到统一排队,这是两个互斥的行为。我们提供一种最佳解决方法,软件是一个全技能的座席软件,座席也是一个全技能的座席,但是状态可以切换,比如企业准备在晚上6点多做一个促销活动,预测6点半从微信、APP渠道会接入大量客户请求,那么这个企业6点25分把十个座席从语音状态全部切换到在线座席的状态,同一组人、同一台PC、同一个软件实现渠道间的切换。我们认为做到这点就很好了。
     
     Q:您的 SaaS平台,如何保障客户的数据安全? 
     
    A:我们从以下几个层面保护:首先我们和每个用户都有签署严格的法律协议,公有云上客户的数据全部是客户自己的,我们不会碰,客户有完全的处理权。第二从技术架构上进行运维监控,我们内部的运维人员分层,公司里目前只有两个人,不包括我在内能进入到数据库,其他运维人员只有服务的运维权限没有数据权限。而他们也和公司签定了严格的法律协定,任何一举一动都会被系统监控。出了未经授权的问题会承担法律责任。第三,我们是一个多租户系统,租户间的数据都是完全隔离的。第四我们长期聘请第三方安全公司对公有云平台做扫描,进行服务升级,确保不出现任何安全上的漏洞。 
     
    Q:关于“客户画像”,你们在具体服务中是帮助客户做一些分析还只是把数据给到客户让他自己去分析?
     
     A:用户画像是主动营销的核心技术,举个例子,我们有个规模很大的电商用户,在后台做了一个大数据挖掘,找到2万名妈妈,打上标签,然后再从中找到所有2-4岁小孩的妈妈2千名打上标签,通过我们客服软件的群发接口给目标用户推送儿童汽车坐椅的消息。客户点击后并不是进入一个广告页面,而是进入一个和真人对话的一对一的聊天页面。这种营销方式比企业之前毫无差异地群发广告提高了20多倍的客户转化率。这项技术有两个核心点,一是基于大数据的用户精准画像和标签;二是APP主动营销,这是对过去外呼时代呼叫中心主流营销模式一个质的改变。可以理解为它是一种APP的外呼技术,代替了以往的电话通道,但对用户干扰非常小。
     
     Q:我们的产品可否满足运营人员对于管理工具的定制化需求?在使用中客制化是否方便、速度如何?
     
     A:我理解这个问题的核心是报表和统计,再往上说是数据和挖掘。以前的软件产品会提供很多报表,但这个时代过去了,因为对于太多的报表客户经常会不知道怎么用,而且用户永远都有层出不穷的新报表需求,那种写死的报表无法与时俱进。我们现在所有的数据分析和报表都是基于发现式的BI和大数据挖掘。以前的报表改不了,而发现式BI则是根据大数据引擎不断挖掘出各种结果。用户可以自行灵活配制界面,你需要什么指标,什么图都可以自行配制。新时代的大数据公司全是做这种发现式的报表,这是一项非常核心的技术。
     
     Q:我们能满足企业自建平台的需求吗?
     
     A:会满足,因为中国现状是这样,尤其是投资、金融、印刷等行业有严格规定,数据就是不能出公司。我们也是可以做私有云。私有云客单价高。
     
     Q:我看到环信经常发布一些交流活动信息,比如组织iGEEK GAMP沙龙、APP运营沙龙?这些活动举办的初衷是什么?影响如何?
     
     A:我们的PaaS产品(即时通迅云)为我们的SaaS产品贡献了很多优质客户流量,我们需要用这种社区的方式来维护和构建深度的开发者关系。我们认为,未来客户管理产品将是一个生态体系中的产品,SaaS产品只是一个基础软件,上边会长出很多小的分支,比如一些定制化需求,由其它的第三方团队来做,我们给他们提供API接口,进行定制化开发。我们通过环信的这些社区活动聚集了一批开发者团队,这也是为我们未来生态圈的布局做积累,做蓄水池,这是其他行业竞品所不具备的。 从产品角度来说,做客服软件最大的痛点就是刚才提到的高级和大型用户的复杂定制化需求,如何解决,就是通过环信的PaaS平台,把基础功能开放出来,不仅做自己的产品也让合作伙伴在这个平台上做二次开发,比如开发者可以在我们平台上做一个专门的报表模块。这应该是所有的SaaS厂商的必由之路,但这需要底层的平台有很好的扩展性和开放性,这在技术上的门槛非常高。 
     
    这实际就是生态圈的玩法。生态圈相当于一个核武器,超越产品是有可能的,但想超越生态圈难度太大了。

      收起阅读 »

    关于昵称头像用户信息存储讨论

    这是一篇很正经的讨论帖,关于昵称头像的讨论。 ---------割--------- 写在前面的话: 集成环信即时通讯的时候,相信大家都有问过这个问题,“昵称头像怎么实现?”   如果你的你的昵称头像已经实现,请直接跳到文章末尾进入讨论!   在回答这个...
    继续阅读 »
    这是一篇很正经的讨论帖,关于昵称头像的讨论。

    ---------割---------

    写在前面的话:

    集成环信即时通讯的时候,相信大家都有问过这个问题,“昵称头像怎么实现?
     
    如果你的你的昵称头像已经实现,请直接跳到文章末尾进入讨论!
     
    在回答这个问题前先简单介绍下环信对于昵称头像的设计:
     环信只是即时通讯的消息引擎。环信本身不提供用户体系,环信既不保存任何APP业务数据,也不保存任何APP的用户信息。比如说, 你的APP是一个婚恋交友APP,那么你的APP用户的头像,昵称,身高,体重,注册电话号码,注册邮箱等信息是保存在你自己的APP业务服务器上,我们服务器端不保存任何用户具体信息。 
        环信这样设计的目的有2个:
        1. 尽量少的侵入开发者自己APP的业务数据和用户体系。用户体系是一个APP的最最核心的数据,在当前中国的环境下,部分开发者会比较难信赖一个第三方厂商,把自己最关键的用户体系信息托管到一个第三方平台上。
        2. 大多数APP都有自己的服务器后台,有自己的用户体系。所以环信要尽力做好的环节是尽可能方便开发者把环信和自己的用户体系集成,而不是为开发者提供他们可能并不会用的用户体系。

    不知道大家看明白了没有,如果有疑问接着往下看


    集成环信即时通讯在实现昵称头像方面推荐两种做法
     
    方法一 从APP服务器获取昵称和头像
     
    方法二 从消息扩展中获取昵称和头像
     
    关于两种方案的实现思路与对比优缺点在连接后面也有阐述。


    思路给出来,大家就按照着做吧,在做的过程中也收到了一些小伙伴们的吐槽,能不能有现成的参考呀?于是环信在后来的demo中就实现的昵称和用户信息,小伙伴们又可以愉快的敲代码了!

     关于demo 的昵称头像需要给大家说明的,这里非常重要,请接着往下看:


    demo使用的的存储使用了parse的服务,有一些开发者可能会直接copy了demo中的代码也使用了parse来存储昵称和用户信息,parse的服务快要到期了,还有两个月就会迁走,我们想统计下有哪些用户用了parse的服务?


     
    今天要讨论的事情就这么多,大家如果使用了parse请告诉我们,作为环信的用户肯定是要对大家负责的,会给出一个解决方案,或者直接提供一套这样的服务。
      收起阅读 »

    没有表情呢

    大神,帮我看看这个问题怎么解决,提前谢谢,里边没有表情,我用的是模拟器
    大神,帮我看看这个问题怎么解决,提前谢谢,里边没有表情,我用的是模拟器

    环信直播课堂第七期--3.xSDK实时音视频的集成

    日期与时间:2016年4月14日15:00 持续时间:半小时 描述:教你如何从零开始,用环信ios sdk实现即时视频和聊天 本期环信直播课堂将由环信IOS工程师fudh给大家详细讲解集成3.0 SDK实时音视频 直播观看地址: http://www....
    继续阅读 »
    日期与时间:2016年4月14日15:00

    持续时间:半小时

    描述:教你如何从零开始,用环信ios sdk实现即时视频和聊天

    本期环信直播课堂将由环信IOS工程师fudh给大家详细讲解集成3.0 SDK实时音视频

    直播观看地址: http://www.imgeek.org/video/15 

    视频回放地址:http://www.imgeek.org/video/24 收起阅读 »

    环信CEO:从传统呼叫中心到全媒体客服,一场“效率”革命

    2016年4月8日,由江苏智恒发起的《企业通信与呼叫中心技术发展论坛》在江苏南京圆满落幕。环信做为全媒体智能云客服倡领者受邀参加了本次行业盛会,环信CEO刘俊彦在大会与现场的数百位行业精英一起分享了题为《全媒体智能客服时代的最佳实践》的主题演讲。 本次论...
    继续阅读 »


    kefu.jpg


    2016年4月8日,由江苏智恒发起的《企业通信与呼叫中心技术发展论坛》在江苏南京圆满落幕。环信做为全媒体智能云客服倡领者受邀参加了本次行业盛会,环信CEO刘俊彦在大会与现场的数百位行业精英一起分享了题为《全媒体智能客服时代的最佳实践》的主题演讲。

    本次论坛活动旨在促进新兴通信技术在企业通信呼叫中心领域的推广、提高企业的通信能力、提高企业部署通信和客户服务平台时的策划水平。包括开源软交换技术,基于浏览器模式的实时通信技术,视频直播技术,呼叫中心领域中的智能化和多渠道客户体验技术。

    2015年作为中国企业级服务元年,同时也是SaaS服务元年,尤其在客服领域诞生了一大批以产品技术驱动的SaaS公司,而以全媒体、智能化、移动化为主打的环信移动客服更是其中的翘楚,不到一年时间内服务了1万多家企业,并且在融资额和市场份额上也遥遥领先。

    从传统呼叫中心到全媒体客服,一场“效率”革命

    随着人口红利消失,呼叫中心的升级转型将越来越普遍,2015年中国劳动力规模由2012年的9.37亿降至9.11亿人。中国劳动力人口连续4年绝对值下降企业客服面临的“用工荒”将持续扩大,运营成本将越来越高,越来越多的企业将复制环信客户“学而思”的客服转型之路,从语音呼叫中心为主转而采用全媒体客服,拥抱移动互联网。

    学而思以前部署有4套客服产品包括:1,呼叫中心。2,网页客服。3,新媒体客服。4,APP客服。同时对应4个客服团队支持,相互数据不打通,尤其自开通微信客服后,咨询量增长明显,由用户数据不打通带来的用户投诉增多。而且每年10—11月是教育行业的交费季,以前主要靠呼叫中心外呼,工作量巨大,效果不满意。学而思在2015年集成环信移动客服以后,服务模式改为APP内缴费,并在APP内提供客服支持。整个2015年缴费季,APP客服部门数十人完美解决了往年数百个语音客服的工作。环信CEO刘俊彦认为:“APP客服相比电话客服大幅度提供服务效率,全渠道客服也已经成为企业刚需。”

    IVR进入触摸时代,自助服务的未来,智能机器人将大显神通

    传统IVR,用户需要听完所有菜单再做选择。而现在主流的ITR导航,用户只需在手机上直接选择关注的问题,简单方便。其中神州租车就采购环信移动客服,其中的智能ITR大大缓解了人工客服压力。Gartner预测到2020年,ITR将完全取代IVR全面进入触摸时代。

    近期李世石1:4不敌AlphaGo的事件又将人工智能推上了风口浪尖。而环信是客服行业少数自主开发智能应答机器人产品的公司,环信知识库+智能聊天机器人可以帮助人工坐席挡住80%的常见问题。同时具有以下特性:1,灵活可定制的智能会话、自定义菜单导航功能。2,预置的行业知识库,行业相关的常见问答可以一键拥有。3,与现有知识库系统对接,机器学习,智能优化知识库。4,人机无缝配合,更少的成本,更好的客户体验 。

    最后,环信CEO预测:“随着国内SaaS客服产品的逐渐成熟完善,中小企业将全面拥抱SaaS客服,建设全媒体客户关系中心。而传统大型企业也将增量部署全媒体客服,保护已有投资,拥抱移动互联网。”SaaS客服也将逐渐成长为一个千亿级市场。

    环信移动客服简介

    环信移动客服是全球首创的全媒体智能云客服平台。支持全媒体接入,包括网页在线客服、社交媒体客服(微博、微信)、移动端客服和呼叫中心等多种渠道。环信移动客服基于环信业界领先的IM长连接技术保证消息必达,并通过强大的智能机器人技术极大降低人工客服工作量。

    环信移动客服于2014年12月上线,截至2015年底,环信移动客服共服务了12000家企业用户,现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、楚楚街、随手记、海尔、51talk,链家自如客等众多互联网和传统企业。根据易观国际发布的《中国SaaS客服市场专题研究报告2015》显示:截至2015年第三季度,环信移动客服在SaaS移动端客服用户覆盖占比为77.4%,以绝对优势稳居行业第一。 收起阅读 »