注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【新手快速入门】集成环信常见问题+解决方案汇总

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。   ios篇 APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录...
继续阅读 »
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
 Android篇昵称头像篇 直播篇[list=1]
  • 一言不合你就搞个直播APP
  •  客服集成[list=1]
  • IM-SDK和客服SDK并存开发指南—Android篇
  • IM-SDK和客服SDK并存开发指南—iOS篇
  •  开源项目
     
    持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
      收起阅读 »

    环信CEC(客户互动云):“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表...
    继续阅读 »
        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表示:“中国客户中心发展经历了从呼叫中心(Call Center)到接触中心(Contact Center)的蜕变,再到互动中心(Engagement Center)的变革,全媒体客服已经从传统客户服务形态的终点逐渐转化成了SaaS客户互动形态的起点。随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)大幕即将拉开,整个SaaS客服行业将被重构和赋能。”

    ][F6S4LQU291EE7O[}HHPKN.png


    环信荣膺《客户世界》全媒体客服标杆品牌
     
       AI技术经过整整60年的发展在包含客服在内的一些特定的业务场景实现了突破。技术应用的准确率达到大规模商业应用的要求,开始广泛在一些特定的领域进入实际应用阶段。消费者连接技术、接触技术和体验技术的不断创新正深刻改变着客服行业。随着全新的人机智能时代的到来,深度学习、认知计算、服务机器人、增强现实空间又在持续改变客户交互的方式、能力与体验,以致冲击客户中心现有的运营方式。共享经济理念、区块链技术等甚至会对服务与管理范式带来更巨大的革命。
     
       《中国客户中心现状与变革报告(2017)》研究项目以技术为前导,关注客服场景下各种新渠道、新触点、新运营、新分析;关注新技术潮流推动下客户中心的日常管理、团队建设、组织变革、人才培养等方面的变革和趋势。面向超过200家本领域甲方企业客服高级负责人(客服总经理/呼叫中心总经理/运营总监/技术总监)发放调研问卷,搜集反馈及评价意见。环信凭借其稳定先进的产品技术能力,优秀的服务水平以及大客户优势获得了调研的一致肯定。
     
    《中国客户中心现状与变革报告(2017)》对环信评价

       环信是国内最早提供全渠道客服解决方案的厂商之一,产品结构完整,其即时通信云产品、移动客服产品、智能客服机器人产品和营销云产品构成从客户互动渠道,到客户服务,到主动营销的客户互动中心完整解决方案。环信服务大客户能力强,在电商、保险、证券、汽车等主要行业的龙头企业客户中拥有成熟的案例。环信独具优势的IM长连接技术,可以提供具备更高可靠性的全渠道客户互动体验;环信一直致力于推动人工智能和大数据在客服行业的落地,其智能客服机器人产品技术先进,有众多大型企业成功实施案例,其“客户声音”产品具有行业前瞻性,值得期待!

    )DLD4)GOJZZT{_H((IYOM.png


       环信CEO刘俊彦给与会行业技术领袖作了关于《人工智能驱动的客户互动云(Customer Engagement Cloud)》的主题分享。他在接受年度推荐全媒体客服标杆品牌奖项时表示 :“环信一直致力于推动整个SaaS客服行业的蓬勃发展,随着全媒体客服的完善以及AI的逐渐成熟, 环信预测“云通讯+服务云+智能营销”将构成从用户服务到用户营销的完整闭环。因此,2017年 ,环信整合旗下即时通信云、移动客服、智能客服机器人和主动营销产品线,推出环信CEC (Customer Engagement Cloud),向企业提供从客户互动渠道,到客户服务,到精准客户营销 的客户互动全流程解决方案。”同时,刘俊彦认为:“全媒体客服已经从传统客户服务形态的终点转化成了SaaS客户互动形态的起点,随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)即将来临,整个SaaS客服行业将被重构和赋能。”

    QQ图片20170524105853.png


    环信CEC:“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

    QQ图片20170524105924.png


    环信眼中的客服行业智能变革:客户服务全面人工智能化

    QQ图片20170524105957.png


    环信行业标杆客户全面覆盖

    环信CEC(客户互动云)矩阵

    QQ图片20170524110032.png


    客户互动云(Engagement Cloud)核心特性

    1、全渠道客户互动:全面支持网页、微信、微博、APP/IM、工单和呼叫中心等主流客户互动渠道。其中,环信业界领先的IM长连接技术支持千万级并发,保证消息必达,助力企业打造极致的移动端客户服务体验。所有渠道支持双向互动,如主动回呼,多渠道统一推送,基于用户行为的自动营销等,真正将服务通道与营销通道融合,实现客户中心从成本中心向利润中心的升级。

    2、视频客服:实时双向视频客服,支持Android、iOS、Pad及主流PC和手机浏览器等多平台接入,低延迟,1080P高清,支持客户端和服务器端录制,可控灵活。

    3、全渠道客服:环信移动客服作为业内广泛使用的客户中心系统,囊括多项行业主流大奖,拥有多项国际PCT专利和国内专利,深受客户好评。环信移动客服产品成熟可靠,功能完善,全面覆盖了全渠道接入管理,客户服务与客户互动管理,运营与运维管理,工单系统,现场管理,智能报表,质检等客户中心功能。

    4、客户声音:环信客户声音是基于人工智能和大数据挖掘的客户体验透析产品。对来自多个渠道的非结构化客服会话数据进行自然语言解析,主题聚类和情感度建模,挖掘和分析热点话题,发现服务运营问题,寻找畅销或问题产品,洞察销售机会。客户声音系统可以帮助企业识别和改善客户旅程的各个阶段。

    5. 智能客服机器人:环信智能客服机器人不仅在常见的单轮对话能力上表现优异,预装多种行业知识库,还可以快速开发多轮对话,支持人机协作以便在复杂场景下对人工客服提供全面AI辅助支持。同时,环信智能客服机器人的自动学习能力极大的降低了机器人知识库的维护成本。

    6、精准营销及自动化营销:大数据和AI驱动的营销功能,如自动化消息模板和自动化规则管理及A/B测试,营销计划管理,基于用户行为轨迹、用户画像和用户会话内容的自动化消息和访客CTA(Call To Action)等。 收起阅读 »

    环信Android/ios V3.3.2 SDK 已发布,新增群组、聊天室群公告及群文件功能

     Android V3.3.2 2017-05-18 增加群、聊天室公告相关API群组支持上传及下载共享文件群组支持设置扩展属性EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceVi...
    继续阅读 »

    QQ图片20170522115322.png


     Android V3.3.2 2017-05-18
    1. 增加群、聊天室公告相关API
    2. 群组支持上传及下载共享文件
    3. 群组支持设置扩展属性
    4. EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceView
    5. Demo及EaseUI改成纯Android Studio结构,不再支持Eclicpse导入
    6. easeui没有包含SDK的jar和so, 使用需要自己拷贝libs下的库文件,或者执行copyLibs.sh完成拷贝。

     
     iOS V3.3.2 2017-05-18
     
    新功能:
    1. 新增:修改获取群公告,上传下载删除群共享文件,修改群扩展信息接口(接口详情请查看文档群组管理
    2. 新增:修改获取聊天室公告(接口详情请查看文档聊天室管理
    3. 新增:批量设置群组免打扰接口


    修复:
    1. 修复有时调用getAllConversations时返回为空的bug
    2. 修复获取已加入群组超时的bug

     
     
    版本历史:AndroidSDK 更新日志  ios SDK更新日志
    下载地址:SDK下载
    收起阅读 »

    环信Android消息回撤

    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1. 1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项.     撤回点击之后处理为:  发送撤回消息!!!!!!cmdMsg = EMMe...
    继续阅读 »
    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1.
    1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项. 
       撤回点击之后处理为:  发送撤回消息!!!!!!
    cmdMsg = EMMessage.createSendMessage(EMMessage.Type.CMD);
    // 如果是群聊, 设置chatType, 默认是单聊
    if(chatType == Constant.CHATTYPE_GROUP){
    cmdMsg.setChatType(ChatType.GroupChat);
    }
    String action = "REVOKE_FLAG";
    EMCmdMessageBody cmdBody=new EMCmdMessageBody(action);
    // 设置消息body
    cmdMsg.addBody(cmdBody);
    // 设置要发给谁, 用户username 或者群聊 grouid
    cmdMsg.setTo(toChatUsername);
    // 通过扩展字段添加要撤回消息的iD
    cmdMsg.setAttribute("msgId", msgid); // 长按的时候, 获取本信息的message的Id
    // long aa = cmdMsg.getMsgTime(); // 获取这个消息的发送时间
    // 获取当前系统的时间
    long time = new Date().getTime();
    long minite = (time - aa - 6000)/1000; // 1s = 1000
    if(minite <= 120){
    EMClient.getInstance().chatManager().sendMessage(cmdMsg);
    cmdMsg.setMessageStatusCallback(new EMCallBack() {

    @Override
    public void onSuccess() {
    conversation.removeMessage(msgid);
    handler.sendEmptyMessage(1);
    }

    @Override
    public void onProgress(int arg0, String arg1) {
    }

    @Override
    public void onError(int arg0, String arg1) {
    // TODO Auto-generated method stub
    String a = "";
    // conversation.removeMessage(msgid);
    }
    });

    }else{
    ToastUtils.ToastShortMessage(getActivity(), "发送时间超过2分钟的消息!不能被撤回!");
    }
    break;





    此处handler.sendEmptyMessage(1);中的内容是:         
    ToastUtils.ToastShortMessage(getActivity(), "消息已撤回!");
    messageList.refresh();





    2. 环信在获取CMD消息监听有三个地方: 分别为, EaseChatFragment, MainActivity, DemoHelper(此处为App后台运行时, 消息撤回的处理)
    在EMMessageListener下的onCmdMessageReceived()中处理接受到的CMD消息, 首先贴上的为EaseChatFragment里面的: 
    for(EMMessage emMessage : messages){
    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
    String action = cmdMessageBody.action();
    if(action.equals("REVOKE_FLAG")){
    try {
    msgId = emMessage.getStringAttribute("msgId");
    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
    if(emMessage.getChatType() == ChatType.GroupChat){
    messageList.refreshSelectLast(); //刷新UI
    }else{
    handler.sendEmptyMessage(1);
    }
    } catch (HyphenateException e) {
    e.printStackTrace();

    }

    }
    }
    此处handler.sendEmptyMessage(1)中的内容是:
    // 删除表示撤销
    conversation1.removeMessage(msgId);
    messageList.refreshSelectLast();





    3. MainActivity里面的处理方式:
        for(EMMessage emMessage : messages){
    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
    String action = cmdMessageBody.action();
    if(action.equals("REVOKE_FLAG")){
    try {
    msgId = emMessage.getStringAttribute("msgId");
    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
    if(emMessage.getChatType() == ChatType.GroupChat){
    refreshUIWithMessage(); // 刷新UI
    }else{
    handler.sendEmptyMessage(1);
    }

    } catch (HyphenateException e) {
    e.printStackTrace();

    }

    }
    }
     
    此时handler.sendEmperymessage(1)中: 
    conversation1.removeMessage(msgId);
    refreshUIWithMessage();
    4. DemoHelper里面的处理方式:
    for (final EMMessage message : messages) {
    // 获取消息body
    EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
    final String action = cmdMsgBody.action();// 获取自定义action
    // 发送一个透传消息
    if(action.equals("REVOKE_FLAG")){
    try {
    if(message.getChatType() == ChatType.GroupChat){ // 群组处理方式
    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getTo(), EaseCommonUtils.getConversationType(2), true);
    }else{
    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getFrom());
    }
    msgId = message.getStringAttribute("msgId");

    handler.sendEmptyMessage(1);
    } catch (HyphenateException e) {
    e.printStackTrace();
    }
    }
    }
    此时handler.sendEmpertyMessage(1)中的方法是: 
    conversation1.removeMessage(msgId);
    至此,环信消息回调完成, 没有去做撤回回调处理,直接删除不好,如果想做的请自行处理.谢谢,本文纯属原创,如果有问题,可与我联系,QQ邮箱: 277667430@qq.com.本人姓氏: 侯 收起阅读 »

    【客户世界·洞察者】智能客服机器人是下一代客服的核心驱动力(附Gartner报告全文)

        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的...
    继续阅读 »
        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的矛盾日益尖锐,如何用有限的客服资源服务不断增长的海量客服请求需要一个颠覆型的技术来解决。相比人工客服,智能客服机器人将提供极大的效率优势。
     
       Gartner报告指出智能客服机器人(VCA-virtual customer assistance)的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。
     
       环信作为智能客服企业的先行者,基于自然语言处理和机器学习技术推出了环信智能客服机器人,辅助或代替人工客服精准回答常见或高频问题,降低企业客服人力成本。目前,环信在客服领域已经服务了58541家标杆客户,积累了人工智能在客户服务行业落地的大量最佳实践。
     
    视频观看地址:点击观看
     
    3.1,智能客服机器人在客服场景下的最佳实践:
     
    3.1.1,无需人工标记和人工维护的机器人单轮会话,极大降低客服机器人的维护成本。

       一些问题是不依赖于对话历史,仅根据当前句子就能给出答案,难点在于机器能否理解同一语义的不同表达方法。环信智能机器人采用自然语言处理技术和深度学习技术建立对话模型,使用海量数据对模型进行训练,并借助客服系统中访客和客服的实时反馈来增强学习,精准识别用户意图,帮助人工客服回答各种问题。相比基于关键词匹配和人工定义规则大量标注数据的传统问答技术,环信智能机器人无需人工标记和人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

    001.png


    图1示例:环信机器人无需人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

    3.1.2机器人多轮会话,支持更多复杂业务,进一步拓展机器人使用场景。

       而另一些问题则由于缺少足够信息或者过于模糊,需要通过多轮对话的方式来明确用户的需求。比如用户想查物流,但是缺少订单号等信息,机器人需要引导用户提供这些数据。这和单轮的问答相比,多轮对话的技术难点更多,比如指代的理解,句子的省略,用户状态的维护等等。

       环信智能机器人支持上下文语义和多轮会话,并预装多行业的领域知识,如电商行业的物流状态查询模型,产品保修会话模型等。这种基于行业领域和业务模型的多轮会话能力,相比单轮会话,进一步扩展了客服机器人对复杂客服业务场景的自动支撑能力。

    002.png


    图2示例:环信机器人通过多轮会话支持查询物流状态,并和企业业务系统做集成,真正意义上节省人工。

    3.1.3无缝人机协作体验,复杂场景下最佳用户体验的客服模式。

       在一些比较复杂和特殊的服务场景,比如高客单价的金融行业售前咨询,机器人客服不能完全理解客户的个性化咨询要求的时候,我们可以无缝进入人机混合模式。在人机混合模式下,环信智能客服机器人向人工客服推荐备选答案,人工客服起到了保证答案质量充当专家客服的角色,这样既保证了客服的响应速度又提高了问题的回答准确性,同时降低了人工客服的工作量。

    3.1.4,智能质检,准确率达到替代人工质检水平。

       环信机器人还提供自动智能质检功能,可以对全部客服会话进行实时或离线质检。 智能质检是基于环信在线客服各个领域的海量用户对话,提取出数百个客服对话特征,并用这些特征训练得到的一个通用质检模型。智能质检的准确率达到替代人工质检水平。

    3.1.5. 支持智能自主学习,更高效的知识库

       环信智能机器人可以快速高效搭建知识库。既支持批量导入FAQ或用户手动维护问答知识,也支持智能自主学习。智能自主学习是指客服机器人自主学习人工客服的会话,自动生成新知识规则。相比手动维护问答知识,智能自主学习能力显著降低了客服机器人的维护成本,提高了知识库的准确性和时效性。环信智能机器人可预装多个领域的行业知识库。

    附录:Gartner研究——虚拟客户助手(智能客服机器人) 分析师:布莱恩·玛纳萨马

    定义:虚拟客户助理(VCA),代表公司进行模拟对话以传递信息和/或代表客户采取行动并执行交易。 VCA由四部分组成:

       ■接收请求和传递回应的用户界面■用于文本和语音的自然语言处理引擎■可以检索知识和内容数据存储库的搜索和知识引擎■用于分析意图的上下文引擎一些VCA还具有机器学习功能。

       定位和市场接受速度:IBM Watson,Microsoft Cortana,Next-IT,Creative Virtual和其他VCA供应商的工作正在提高人们对作为实用工具的虚拟助理(VA)技术的认知。 VCA的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。VCA技术有望在两至五年内成为主流。随着移动优先的用户体验转向,许多VCA都亟待更新,以支持多渠道客户与统一知识库的互动,特别是支持客户手机。

       用户建议:确定客户服务平台的当前状态和所需状态。今天您将使用什么样的客服方式和客服工具?您是否使用自然语言处理技术来确定客户到底咨询了什么?是将这个呼叫咨询分派给正确的客服接线员还是让客服机器人提供自动回复?VCA将成为支持多个客服渠道的起点。 VCA未来将可能改变你的日常生活;它可以是一个帮助你在移动设备上购买新健身设备的向导,也可以是一个帮助你开设银行帐户的虚拟客服人员。

       市场正在发生一种变化,对虚拟客服助手的逐渐重视以及使用频次的减少,这个现象已不如以前那么明显。在数字渠道中提供拟人化体验的驱动力正在发生改变。随着客户逐渐适应和接受与计算机的互动,其对具有拟人化情感的3D图像的需求正在减少。公司部署虚拟客服助手时他们发现有对于提升品牌的附加价值,而非仅仅模拟一种店内体验。VCA不仅是面向客户的,而是越来越多地部署为面向员工——帮助客户服务中心减少人工坐席操作时间以及保障客户咨询回复的一致性。

       将一组简单的串行项目与一个复杂的大型项目进行比较,以满足所有确定的需求。找到构成完整呼叫的最高频简单对话,以简单的方式实现自动化和提升客户满意度。然后,识别下一组完整的呼叫:在一段时间,技术与人工可进行合作处理这组呼叫,即当技术检测到问题(如技术储备的知识不足、客户声音难以辨别、或客户通过正确的操作明确要求由人类进行对话)时,人工操作员将接管此组呼叫。

       业务影响:VCA是狭义的、具有特殊用途的VA,用于销售、客户服务和数字商务,且具有独特的目标。VCA的商业案例有三方面。其解决了以下需求:

       ■满足客户对网络和移动渠道中客户支持的期望——更高的互动频率;全天候、即时的聊天可用性■将互动转向价格更低的客户自助服务渠道,更快获取解决方案;降低服务成本■提供积极的建议和参与,培养忠诚度和客户满意度。

        VCA的有效使用便于组织衡量其——特别是联络中心的——参与数量。在数字亭或自动取款机上使用启用语音的VCA可降低对类型化干预的需求,且有助于为非传统受众提供有趣的互动。

    好处评级:高

    市场渗透率:目标受众的5%至20%

    成熟度:未成熟
     
    点击查看Gartner报告全文 收起阅读 »

    拍照闪退

    在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了 参考: http://blog.csdn.net/ganshenml/article/details/72315636
    在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了
    参考:
    http://blog.csdn.net/ganshenml/article/details/72315636

    【视频教程+源码】基于环信IM做一个仿微信APP-更新ing

    我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。     大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务...
    继续阅读 »
    我只是一个普通人,做人要谦虚。
    我不是大神,我也不是很厉害的。
    天外有天,人外有人。
    老师引入门,修行靠个人。
    希望能帮助大家,谢谢。

        大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
     
    本人现状况:
      
       在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

     
    郭永峰IT教育工作室于2017年4月12日成立!
     
    成立原因:

    希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
     

    进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
    1. 4月12号成立工作室,现在18号,过了一个星期
    2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
    3. 环信的教程视频主要是针对有开发经验者
    4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
    5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
    6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
    7. 同时能希望大家在面试时,在即时通讯这块不在陌生

      持续更新

    第一阶段:即时通讯的了解和微信APP开发前的准备!

    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
     
    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

    【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

    【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

    【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
     
    【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
     
    整个项目源码,git地址https://github.com/mayaole/fWeiXin

     微信打赏


    微信.png



    支付宝打赏


    支付宝.png



    谢谢大家的支持,个人微信号清扫描下面张图


    个人.png



     
    郭永峰IT交流QQ群请加:596441895 收起阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
    继续阅读 »
    接上篇

    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)
    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)
    【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK
    【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版
    【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建
    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能
    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能
    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录
    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
     
     
    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。​
     
    15.微信-在其它设备登录


    收起阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
    继续阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解)

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
    继续阅读 »
    接上篇
    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)
     
    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
     
    04.环信简介(了解)
     





    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
     
    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
     
    打赏二维码请看第一篇。

      收起阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)   我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任...
    继续阅读 »
    接上篇
    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
     
    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
     
    03.XMPP实现即时通信的准备工作(了解)​



     
    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
     
    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
     
    打赏二维码请看第一篇、,如果你有心的话。 收起阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

      接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。我是郭永峰,本套课程基于环信IM教大家如...
    继续阅读 »
     
    接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
    我只是一个普通人,做人要谦虚。
    我不是大神,我也不是很厉害的。
    天外有天,人外有人。
    老师引入门,修行靠个人。
    希望能帮助大家,谢谢。
    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。

    02.XMPP简介(了解)​
     



     
     
    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
     
    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
     
    打赏二维码在上一篇文章,如果你有心的话。 收起阅读 »

    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

     我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃...
    继续阅读 »
     
    我只是一个普通人,做人要谦虚。
    我不是大神,我也不是很厉害的。
    天外有天,人外有人。
    老师引入门,修行靠个人。
    希望能帮助大家,谢谢。
    大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃,最终选择了环信这样的即时通讯提供商。
     
    郭永峰IT教育工作室于2017年4月12日成立!
     
    成立原因:
    希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
     
    本人现状况:
    在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不强求了。

    进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
    1. 4月12号成立工作室,现在18号,过了一个星期
    2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
    3. 环信的教程视频主要是针对有开发经验者
    4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
    5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
    6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
    7. 同时能希望大家在面试时,在即时通讯这块不在陌生

     
    我们先来看第一个视频--01.即时通讯简介(了解) 







     

    微信打赏

    QQ图片20170515172743.png


    支付宝打赏

    QQ图片20170515172833.png


    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信


    QQ图片20170515172529.png


     
    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
      收起阅读 »

    环信移动客服v5.18已发布,增加机器人新手引导及机器人知识库测试功能

    客服模式 支持根据会话类型筛选历史会话 历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。 呼入:客户主动发起的会话;回呼:客服手动回呼的会话。 管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基...
    继续阅读 »
    客服模式

    支持根据会话类型筛选历史会话


    历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。
    • 呼入:客户主动发起的会话;
    • 回呼:客服手动回呼的会话。

    001.png

    管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基础信息、自动回复、知识规则、自定义菜单进行设置,并通过知识库测试对知识规则和自定义菜单进行优化。该新手引导用于帮助客户更好地使用机器人功能,对初次使用移动客服系统的客户和开通多机器人功能的客户均有效。 

    002.png

    机器人知识库测试新增机器人知识库测试功能。管理员可以随时与机器人对话,测试机器人的回复,快速优化知识规则和自定义菜单。知识库测试方法:1. 进入“管理员模式 > 智能机器人 > 机器人设置 > 知识规则”tab页签。 

    003png.png

    2. 点击右上角的“知识库测试”按钮,开始与机器人对话。对话窗口右侧显示机器人回复所匹配的知识规则或菜单,以及匹配率。 

    004.png

    3. 点击匹配到的规则,可以进入知识规则页面,对该条规则进行优化。当未匹配到知识规则时,可以快速添加知识规则。 

    005.png

    支持为待接入超时提示语添加留言按钮新增“为待接入超时提示语添加留言按钮”开关。当客户处于排队状态,无法及时得到接待时,允许客户发起留言(同时结束会话)。进入“管理员模式 > 设置 > 系统开关”页面,依次打开“待接入超时提醒”和“为待接入超时提示语添加留言按钮”开关,并设置超时提示语等。 

    006.png

    客户在网页聊天窗口和H5网页收到的超时提示语如下图所示。点击“留言”按钮后,即可进入留言页面进行留言。同时,会话自动结束。 

    007.png

    权限管理支持设置数据权限在权限管理页面,可以为管理员、自定义角色设置管理员模式下页面的数据权限,分为租户和技能组。
    • 赋予角色某个页面“租户”级别的数据权限时,该角色对应的管理员可以查看并操作该页面的所有数据,包括所有技能组的数据。
    • 赋予角色某个页面“技能组”级别的数据权限时,该角色对应的管理员只可以查看并操作该页面中自己所属技能组的数据。
    管理员模式的以下页面支持设置数据权限:客户中心、历史会话、当前会话。 

    008.png

    Web插件(访客端)当前版本:v47.9在消息中显示留言按钮网页访客端支持在待接入超时提示语中显示“留言”按钮,方便等待中的客户主动发起留言(点击留言按钮后,会话自动结束)。前提:管理员在移动客服系统中“管理员模式 > 设置 > 系统开关”页面,打开“为待接入超时提示语添加留言按钮”开关。支持全屏显示图片网页访客端支持全屏显示图片。客户与客服聊天时,如果收到图片消息:
    •  
    • 桌面聊天窗口:点击图片,可将图片放大至浏览器全屏查看;
    • H5网页:点击图片,可将图片放大至手机全屏查看。


    第二通道支持图片

    网页访客端内置第二通道功能,当IM消息通道(第一通道)出现短暂的消息发送失败的情况时,自动调用第二通道将客户消息发送至移动客服系统,确保客户的所有消息均能准时送达。

    在之前的版本,web插件的第二通道仅支持发送文本消息;从该版本开始,第二通道支持发送文本、图片消息。

    显示客服输入状态

    网页访客端支持显示客服输入状态。客户通过桌面聊天窗口或H5网页与客服聊天时,可以查看客服的输入状态。

    “显示客服输入状态”为增值服务,如需开通,请提供租户ID并联系环信商务经理。 

    009.png


    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.18 

    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

    环信聊天游客身份和正常用户身份的切换

         最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。      1.APP用户的注册,也就注册环信,APP的登录返...
    继续阅读 »
     
       最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。
     
       1.APP用户的注册,也就注册环信,APP的登录返回的有用户ID,这个时候并没有让他登录环信,只是保存了返回的ID,下面就是用ID来判断该用户是否注册过环信的依据
     
    大致说明一下,代码中用到一个类来保证uuid不会改变的状态,为防止app卸载后uuid的改变,我们把他存储到钥匙串里面来保存

    下面用图来表示
    我们先来看下整个身份切换实现的逻辑图

    4861502-fa2f7d87d00c78d7.jpg



    下面就上代码了,第一步从图中第一步来说判断userID是否存在

    这个地方是在点击聊天按钮开始判断的
     
    -(void)releaseInfo:(UIButton*)sender{
    NSString*Hxusername=[userdic objectForKey:@"useid"];//获取保存的userID
    NSString*phonestr=  [[NSUserDefaults standardUserDefaults]objectForKey:@"phonenum"];
    NSString*chatid=[[phonestr md5String]substringFromIndex:16];//这个是获取客服的欢信ID
    //单例里面处理用户是否登录,以及游客随机分配uuid来注册环信IM号
    DataManager*datamage= [DataManager shareDataManager];
    //判断用户ID是否存在,也就证明是否注册过环信
    if (Hxusername.length>0) {
    if ([datamage loginKefuSDK])//判断用户是否登录
    {//单聊
    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
    }
    }else{
    //游客身份的判断
    if ([datamage customelogin]) {
    //单聊
    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
    }
    }
    }上面这是按钮方法里面的数据下面来说,DataManager*datamage= [DataManager shareDataManager];这个单利的方法
    DataManager.h
    @interface DataManager : NSObject
    -(BOOL)customelogin;//判断游客之前是否有登录
    -(void)requestchattphone;//获取美容院客服聊天的对象电话
    @end
    DataManager.m


    @implementation DataManager
    +(instancetype)shareDataManager{
    static DataManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    manager = [[DataManager alloc] init];
    });
    return manager;
    }
    //userID存在的时候 登录IM
    - (BOOL)loginKefuSDK {
    NSDictionary*userdic=[[NSUserDefaults standardUserDefaults]objectForKey:@"userMessage"];//接受用户是否登录
    NSString*loguser=[NSString stringWithFormat:@"%@",[userdic objectForKey:@"useid"] ];
    EMClient *client = [EMClient sharedClient];
    //用户已经登录
    if (client.isLoggedIn) {
    if ([loguser isEqualToString:client.currentUsername])//当前登录用户的ID和即将要登录人的ID是否一样
    {
    return YES;
    }else
    {
    EMError *error = [[EMClient sharedClient] logout:YES];
    if (!error) {
    NSLog(@"退出成功");
    }
    }
    }//这里APP用户登录环信的密码统统是123456
    EMError *error = [[EMClient sharedClient] loginWithUsername:loguser password:@"123456"];
    if (!error) { //IM登录成功
    return YES;
    } else { //登录失败
    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
    return NO;
    }
    return NO;
    }
    //游客身份的登录方法
    -(BOOL)customelogin
    {
    EMClient *client = [EMClient sharedClient];
    //用户已经登录
    if (client.isLoggedIn) {
    return YES;
    }//该用户没有注册,来用改设备UUID来给用户注册环信,并登录环信
    if (![self registerIMuser]) {
    return NO;
    }
    EMError *error = [[EMClient sharedClient] loginWithUsername:self.Hxusername password:@"123456"];
    if (!error) { //IM登录成功
    return YES;
    } else { //登录失败
    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
    return NO;
    }
    return NO;
    }
    - (BOOL)registerIMuser { //举个栗子。注册建议在服务端创建环信id与自己app的账号一一对应,\
    而不要放到APP中,可以在登录自己APP时从返回的结果中获取环信账号再登录环信服务器
    EMError *error = nil;
    NSString *newUser = [self getrandomUsername];
    self.Hxusername = newUser;
    error = [[EMClient sharedClient] registerWithUsername:newUser password:@"123456"];
    if (error &&  error.code != EMErrorUserAlreadyExist) {
    NSLog(@"注册失败;error code:%d,error description :%@",error.code,error.errorDescription);
    return NO;
    }return YES;
    }
    //创建一个随机的用户名,这里是设备UUID来代替的​
    - (NSString *)getrandomUsername {
    //第一种方法:
    /*NSString *username = nil;
    UIDevice *device = [UIDevice currentDevice];//创建设备对象
    NSString *deviceUID = [[NSString alloc] initWithString:[[device identifierForVendor] UUIDString]];
    if ([deviceUID length] == 0) {
    CFUUIDRef uuid = CFUUIDCreate(NULL);
    if (uuid)
    {
    deviceUID = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
    CFRelease(uuid);
    }
    username = [deviceUID stringByReplacingOccurrencesOfString:@"-" withString:@""];
    username = [username stringByAppendingString:[NSString stringWithFormat:@"%u",arc4random()0000]];
    return username;*/
    //第二种方法
    //加上build ID是为了保证设备的唯一性,如果这里的buildID换了,设备的uuid也会变,这里的解决办法也就是放倒了钥匙串里面,不会因卸载程序,程序升级设备的标识会改变
    NSString *SERVICE_NAME = NAVI_TEST_BUNDLE_ID;//最好用程序的bundle id
    NSString * str =  [SFHFKeychainUtils getPasswordForUsername:@"UUID" andServiceName:SERVICE_NAME error:nil];  // 从keychain获取数据
    if ([str length]<=0)
    {
    str  = [[[UIDevice currentDevice] identifierForVendor] UUIDString];  // 保存UUID作为手机唯一标识符[SFHFKeychainUtils storeUsername:@"UUID"   andPassword:str    forServiceName:SERVICE_NAME updateExisting:1  error:nil];  // 往keychain添加数据
    }
    str = [str stringByReplacingOccurrencesOfString:@"-" withString:@""];
    return str;
    }在这里用到了一个类来处理的UUID不变(APP卸载后不会改变)
    SFHFKeychainUtils.h
    #import@interface SFHFKeychainUtils : NSObject
    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
    @end
    SFHFKeychainUtils.m​
    #import "SFHFKeychainUtils.h"
    static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
    @interface SFHFKeychainUtils (PrivateMethods)
    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
    @end
    #endif
    @implementation SFHFKeychainUtils
    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { if (!username || !serviceName) { *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; return nil; }      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];      if (*error || !item) { return nil; }
    // from Advanced Mac OS X Programming, ch. 16
    UInt32 length;
    char *password;
    SecKeychainAttribute attributes[8];
    SecKeychainAttributeList list;
    attributes[0].tag = kSecAccountItemAttr;
    attributes[1].tag = kSecDescriptionItemAttr;
    attributes[2].tag = kSecLabelItemAttr;
    attributes[3].tag = kSecModDateItemAttr;
    list.count = 4;
    list.attr = attributes;
    OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
    if (status != noErr)
    {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    return nil;
    }
    NSString *passwordString = nil;
    if (password != NULL)
    { char passwordBuffer[1024];
    if (length > 1023) {length = 1023;
    }
    strncpy(passwordBuffer, password, length);
    passwordBuffer[length] = '\0';
    passwordString = [NSString stringWithCString:passwordBuffer];
    }SecKeychainItemFreeContent(&list, password);CFRelease(item);return passwordString;
    }
    + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
    if (!username || !password || !serviceName) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
    return;
    }
    OSStatus status = noErr;
    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
    if (*error && [*error code] != noErr) {
    return;
    }
    *error = nil;
    if (item) {
    status = SecKeychainItemModifyAttributesAndData(item,
    NULL,
    strlen([password UTF8String]),
    [password UTF8String]);
    CFRelease(item);
    }
    else {
    status = SecKeychainAddGenericPassword(NULL,
    strlen([serviceName UTF8String]),
    [serviceName UTF8String],
    strlen([username UTF8String]),
    [username UTF8String],
    strlen([password UTF8String]),
    [password UTF8String],
    NULL);
    }
    if (status != noErr) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    }
    }
    + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
    return;
    }
    *error = nil;
    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
    if (*error && [*error code] != noErr) {
    return;
    }
    OSStatus status;
    if (item) {
    status = SecKeychainItemDelete(item);
    CFRelease(item);
    }
    if (status != noErr) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    }
    }
    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
    return nil;
    }
    *error = nil;
    SecKeychainItemRef item;
    OSStatus status = SecKeychainFindGenericPassword(NULL,
    strlen([serviceName UTF8String]),
    [serviceName UTF8String],
    strlen([username UTF8String]),
    [username UTF8String],
    NULL,
    NULL,
    &item);
    if (status != noErr) {
    if (status != errSecItemNotFound) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    }
    return nil;
    }
    return item;
    }
    #else
    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
    if (error != nil) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
    }
    return nil;
    }
    if (error != nil) {
    *error = nil;
    }
    // Set up a query dictionary with the base query attributes: item type (generic), username, and service
    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil];
    NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];
    // First do a query for attributes, in case we already have a Keychain item with no password data set.
    // One likely way such an incorrect item could have come about is due to the previous (incorrect)
    // version of this code (which set the password as a generic attribute instead of password data).
    NSMutableDictionary *attributeQuery = [query mutableCopy];
    [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes];
    CFTypeRef attrResult = NULL;
    OSStatus status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) attributeQuery, &attrResult);
    //NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult;
    if (status != noErr) {
    // No existing item found--simply return nil for the password
    if (error != nil && status != errSecItemNotFound) {
    //Only return an error if a real exception happened--not simply for "not found."
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    }
    return nil;
    }
    // We have an existing item, now query for the password data associated with it.
    NSMutableDictionary *passwordQuery = [query mutableCopy];
    [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData];
    CFTypeRef resData = NULL;
    status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData);
    NSData *resultData = (__bridge_transfer NSData *)resData;
    if (status != noErr) {
    if (status == errSecItemNotFound) {
    // We found attributes for the item previously, but no password now, so return a special error.
    // Users of this API will probably want to detect this error and prompt the user to
    // re-enter their credentials.  When you attempt to store the re-entered credentials
    // using storeUsername:andPassword:forServiceName:updateExisting:error
    // the old, incorrect entry will be deleted and a new one with a properly encrypted
    // password will be added.
    if (error != nil) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
    }
    }
    else {
    // Something else went wrong. Simply return the normal Keychain API error code.
    if (error != nil) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    }
    }
    return nil;
    }
    NSString *password = nil;
    if (resultData) {
    password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
    }
    else {
    // There is an existing item, but we weren't able to get password data for it for some reason,
    // Possibly as a result of an item being incorrectly entered by the previous code.
    // Set the -1999 error so the code above us can prompt the user again.
    if (error != nil) {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
    }
    }
    return password;
    }
    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error
    {
    if (!username || !password || !serviceName)
    {
    if (error != nil)
    {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
    }
    return NO;
    }
    // See if we already have a password entered for these credentials.
    NSError *getError = nil;
    NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];
    if ([getError code] == -1999)
    {
    // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
    // Delete the existing item before moving on entering a correct one.
    getError = nil;
    [self deleteItemForUsername: username andServiceName: serviceName error: &getError];
    if ([getError code] != noErr)
    {
    if (error != nil)
    {
    *error = getError;
    }
    return NO;
    }
    }
    else if ([getError code] != noErr)
    {
    if (error != nil)
    {
    *error = getError;
    }
    return NO;
    }
    if (error != nil)
    {
    *error = nil;
    }
    OSStatus status = noErr;
    if (existingPassword)
    {
    // We have an existing, properly entered item with a password.
    // Update the existing item.
    if (![existingPassword isEqualToString:password] && updateExisting)
    {
    //Only update if we're allowed to update existing.  If not, simply do nothing.
    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
    kSecAttrService,
    kSecAttrLabel,
    kSecAttrAccount,
    nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
    serviceName,
    serviceName,
    username,
    nil];
    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
    status = SecItemUpdate((__bridge_retained CFDictionaryRef) query, (__bridge_retained CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]);
    }
    }
    else
    {
    // No existing entry (or an existing, improperly entered, and therefore now
    // deleted, entry).  Create a new entry.
    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
    kSecAttrService,
    kSecAttrLabel,
    kSecAttrAccount,
    kSecValueData,
    nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
    serviceName,
    serviceName,
    username,
    [password dataUsingEncoding: NSUTF8StringEncoding],
    nil];
    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
    status = SecItemAdd((__bridge_retained CFDictionaryRef) query, NULL);
    }
    if (error != nil && status != noErr)
    {
    // Something went wrong with adding the new item. Return the Keychain error code.
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    return NO;
    }
    return YES;
    }
    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error
    {
    if (!username || !serviceName)
    {
    if (error != nil)
    {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
    }
    return NO;
    }
    if (error != nil)
    {
    *error = nil;
    }
    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];
    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
    OSStatus status = SecItemDelete((__bridge_retained CFDictionaryRef) query);
    if (error != nil && status != noErr)
    {
    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
    return NO;
    }
    return YES;
    }
    #endif
    @end
    收起阅读 »

    【环信征文】在android中5分钟实现环信昵称头像的显示

    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~ 一、将简版demo里的cache包(5个java文件)复制到自己项目里。 下载环信android简版Demo: 环信Android简版DEMO 昵称头像用到的工具类、mode...
    继续阅读 »
    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~


    23.jpg



    一、将简版demo里的cache包(5个java文件)复制到自己项目里。
    下载环信android简版Demo:
    环信Android简版DEMO

    昵称头像用到的工具类、model都在这个cache包里。 

    1.png


    类介绍:

    2.png



    二、显示昵称头像有两种解决方案:
    1. 从APP服务器获取;
    2. 从消息扩展属性中获取。


    接下来,我们就以官方demo为例,一步步教大家实现昵称头像的显示。
    无论哪种方案,先要设置3步:
    1.更改环信appkey;
    在AndroidManifest.xml中更改appkey,改为【xmxx#xmxx】,如下所示。 

    android:name="EASEMOB_APPKEY"
    android:value=“xmxx#xmxx" />

    2.增加第三方依赖库。根目录下的 build.gradle 下:
    compile 'com.j256.ormlite:ormlite-android:5.0'
    compile 'com.google.code.gson:gson:2.8.0'
    ormlite:操作sqlite数据库
    gson:json对象转换 

    3.设置用户信息提供者:
    在DemoHelper.java的getUserInfo函数里(第824行)增加如下代码: 


    环信头像昵称显示使用的是提供者模式(EaseUserProfileProvider),只要设置了用户信息提供者(setUserProfileProvider),EaseUI界面里显示用户昵称和头像时,就会调用这个getUserInfo函数。


    // 从本地缓存中获取用户昵称头像
    EaseUser user = UserCacheManager.getEaseUser(username);

    并且注释之前获取昵称头像的方法:
    // EaseUser user = null;
    // if(username.equals(EMClient.getInstance().getCurrentUser()))
    // return getUserProfileManager().getCurrentUserInfo();
    // user = getContactList().get(username);
    // if(user == null && getRobotList() != null){
    // user = getRobotList().get(username);
    // }

    然后,根据不同的方案,开发者可以选择不同步骤:

    三、从开发者自己的APP服务器获取的步骤:
    将UserCacheManager.java中第54行-62行代码,换成:通过okhttp(或者retrofit、volley)调用api接口,根据用户环信ID,从开发app服务器获取用户昵称头像。下面两张图是改之前和改之后的效果:
     
    改之前(用第三方云存储):

    1111.png



    改之后(用开发者自己服务器):

    2222.png


     

    四、从第三方后端云存储获取的步骤:

    1.配置后端云服务器;
    (注意:用开发者APP服务器存储昵称头像可以忽略这一步):但是必须直接实现调用API从开发者服务器中获取昵称头像。
    首先打开根目录下的 build.gradle 进行如下标准配置
    buildscript {    
    repositories {
    jcenter() //这里是 LeanCloud 的包仓库
    maven {
    url "http://mvn.leancloud.cn/nexus/ ... ot%3B
    }
    }

    dependencies {
    classpath 'com.android.tools.build:gradle:1.0.0'
    }
    }

    allprojects {
    repositories {
    jcenter() //这里是 LeanCloud 的包仓库
    maven { url "http://mvn.leancloud.cn/nexus/ ... ot%3B }
    }
    }
    继续在根目录下的build.gradle增加如下配置:
    android {    
    //为了解决部分第三方库重复打包了META-INF的问题
    packagingOptions{
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/NOTICE.txt'
    }
    lintOptions {
    abortOnError false
    }
    }
    dependencies {
    // LeanCloud 基础包
    compile ('cn.leancloud.android:avoscloud-sdk:v3.+'
    )
    }
    注意:以上代码,由于论坛里的编辑器会产生乱码,所以建议去【环信Android简版Demo】里复制。或者直接打开:
    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android/blob/master/examples/ChatDemoUI3.0/build.gradle​ 


     3.在DemoApplication.java第49行初始化后端云。(注意:用开发者APP服务器存储昵称头像可以忽略这一步)
    UserWebManager.config(this,"VBYeQuiVPD9CWS2aINWrwBv0-gzGzoHsz","1LItewi0x6hlkgYHxi8DERNN");
     
    4.在环信IM登录成功后,在后端云里新增用户。
    参考LoginActivity.java第173行代码。(注意:用开发者APP服务器存储昵称头像可以忽略这一步) 
    // 登录成功后,如果后端云没有缓存用户信息,则新增一个用户
    UserWebManager.createUser(userId, nickName, avatarUrl);

    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
    Random random=new Random();
    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路

    编译运行。这样就可以用方案一从APP服务端获取昵称头像了。

    五、从消息扩展属性中获取昵称头像。
    1.首先,要注释从APP服务器获取昵称头像的方法。
    删除类文件:UserWebInfo.java和UserWebManager.java。
    UserCacheManager.java中注释第57-68行,不从APP服务器中获取昵称头像: 
    // 如果本地缓存不存在或者过期,则从存储服务器获取
    if (notExistedOrExpired(userId)){
    UserWebManager.getUserInfoAync(userId, new UserWebManager.UserCallback() {
    @Override
    public void onCompleted(UserWebInfo info) {
    if(info == null) return;

    // 缓存到本地
    save(userId, info.getNickName(),info.getAvatarUrl());
    }
    });
    }
    2.登录(或注册)成功后,需要缓存当前用户的昵称头像。
    在登录(或注册)服务端回调(不是环信IM登录回调)里,增加如下代码: 
    // 登录成功,将用户的环信ID、昵称和头像缓存在本地
    UserCacheManager.save(userId, nickName, avatarUrl);

    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
    Random random=new Random();
    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路径)
    3.发送消息时携带昵称头像。
    ChatFragment.java里的 onSetMessageAttributes函数。第231行增加代码:
    // 设置消息的扩展属性,携带昵称头像
    UserCacheManager.setMsgExt(message);

    4.接收消息时携带昵称头像。
    DemoHelper.java里的 onMessageReceived函数。第856行增加代码:
    // 从消息的扩展属性里获取昵称头像
    UserCacheManager.save(message.ext());


    5.另外。音视频通话里,昵称头像也要进行处理。(不需要音视频通话功能的开发者可以省略后面所有步骤)
    发送音视频通话请求时携带昵称头像。
    CallActivity.java里第162行代码更改为: 
    // 通过扩展属性将昵称头像传给对方
    String ext = UserCacheManager.getMyInfoStr();
    if (msg.what == MSG_CALL_MAKE_VIDEO) {
    EMClient.getInstance().callManager().makeVideoCall(username,ext);
    } else {
    EMClient.getInstance().callManager().makeVoiceCall(username,ext);
    }

    6.接收音视频通话时保存昵称头像。
    CallReceiver.java第33行增加代码: 
    // 缓存用户昵称头像
    String ext = EMClient.getInstance().callManager().getCurrentCallSession().getExt();
    UserCacheManager.save(ext);

    7.音频通话里显示昵称头像。
    VoiceCallActivity.java第114行代码改为: 
    // 显示昵称头像
    UserCacheInfo user = UserCacheManager.get(username);
    if (user != null){
    nickTextView.setText(user.getNickName());
    //Glide.with(VoiceCallActivity.this).load(user.getAvatarUrl()).placeholder(R.drawable.em_default_avatar).into(avatarImage);
    }else {
    nickTextView.setText(username);
    }


    8.视频通话里显示昵称头像。
    VideoCallActivity.java第171行代码改为: 
    // 显示昵称头像
    UserCacheInfo user = UserCacheManager.get(username);
    if (user != null){
    nickTextView.setText(user.getNickName());
    }else {
    nickTextView.setText(username);
    }

    编译运行即可使用扩展消息的方式实现昵称头像显示了!
    可以通过这个web页面发送消息测试是否设置成功:
    http://duoroux.com/chat 

    测试账号:
    1.环信ID:biaoge  密码:biaoge
    2.环信ID:biaomei  密码:biaomei

    两种方案的完整代码,大家可以下载环信android简版Demo:
    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android 

    IOS版简版demo下载地址:
    https://github.com/mengmakies/ChatDemo-UI3.00-Simple 


    如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063 


    注意事项


    将此方案集成到自己项目时,要把SqliteHelper构造函数里的“DemoApplication.getInstance()”换成自己项目的应用程序类实例!




      收起阅读 »

    接私活发现甲方比自己还穷是什么样的体验

    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事: 故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买...
    继续阅读 »
    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事:


    故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买不起毒品却有和瘾君子一样的肾虚症状,那么他一定有资源。

    昨天我从我的diao丝同事那里要来一个打擦边球的公众号,有图有文没视频那种。忽然在一篇小黄图文下面看见这么一行字:“如有能力制作开发本平台APP的个人或公司,请联系QQ:***,具体事宜详谈!”

    我:(真没想到有机会接黄站的私活,赶紧加Q)需要Android外包?

    他:(没错,尽管资料填“女”,但我仍能看出他是抠脚大汉)我就要简单的社区 + 相册,相册是自己的作品集

    我:资料发我,我估个价,再找iOS同事给你做个iOS的

    他:APP目前的功能就是自己的作品集

    他:然后社区

    我:(根据公众号给的链接打开了网站的手机版,然后开电脑看了电脑版,是一个基于discuzz的擦边球社区)看了网站手机版感觉做成APP难度不大

    他:大概要多少钱?

    我:(发了一份报价单)Android估价2000左右,最高不超过3000,iOS的价格也差不多

    他:好贵呀

    我:你知道一个程序员每天多少钱工资吗?

    他:我每个月没钱吃饭啊

    他:(过了一会)相册和怎么做成那种看12章以后要会员 才能看?或这收费

    我:(连这个都不会弄,还能给终端提供API)我和你家技术谈谈吧

    他:(开启连珠炮模式)米技术啊

    他:就我和我姐妹弄的

    他:哪有技术

    他:全部自己学的

    他:网站都是自己装的

    我:我业余搞个外包容易吗?咋遇到的发包方个个比我还穷?

    他:那肿么办?

    他:袜子都买不起了

    我:(他缺的不是Android和iOS上的两个软件,而是一整套技术体系)我有个免费的方案,用HBuilder打包你的手机站,这样Android和iOS的APP就都有了,零成本

    他:作为粉丝的你不支持下我啊

    真没想到找私活的甲方比程序员还穷,让我免费做个APP真是讹上我了,瞬间觉得高中时班主任语文老师的顺口溜“上辈子杀猪,这辈子教书;上辈子杀人,这辈子教语文”有了下联:“上辈子叛党,这辈子搞互联网;上辈子卖国,这辈子搞安卓”

    第二天,那个公众号推送的小黄图文下面有这么一行字:“有木有洞APP开发架设的粉丝,如果有请自告奋勇联系小编,当然惊喜肯定给足” 收起阅读 »

    【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了

     ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫   在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙...
    继续阅读 »
     
    ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

      在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

    环信公开课12期讲了什么?
    1. 如何利用消息扩展属性显示昵称头像?
    2. 如何通过APP服务器处理昵称头像的显示?
    3. 昵称头像的本地缓存策略?
    4. 音视频通话如何显示昵称头像?


    关于环信大表哥:


       马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

    环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
     
    自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



    先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。

    QQ截图20170508115114.jpg



    环信公开课视频回放:视频观看地址
      
    环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
    ios简版demo地址
    Android简版demo地址 收起阅读 »

    【环信3.0SDK集成小米推送教程】实现离线消息推送和后台视频电话通知

    教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall】 前言   从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频...
    继续阅读 »


    教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall


    前言

      从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频通话请求就不能再走之前的聊天通道了,所以就要用到我们今天的主角-推送。苹果手机自带了apns,上传推送证书到环信后台就可以实现ios手机的消息推送,Android虽然也有gcm, 但是在大陆地区是不能正常使用的(海外APP不受影响),那么在国内Android的APP就需要用到第三方推送,环信调研了市场设备情况,选择集成了两家厂商推送,分别是小米推送和华为推送,最大程度保证了应用在后台被杀死的情况下也收到离线消息的通知。


    banner.jpg



    废话不多说,今天就通过集成最新的小米推送来实现下消息的离线推送通知,以及被呼叫方离线时方推送提醒对方启动 app 接听通话;其实都是通过集成推送完成!

    准备工作

    首先你的项目需要集成环信 sdk,并且已经实现了发送消息以及音视频通话功能(这个可以直接用我上边 github 上的项目);
    然后你需要有小米的开发者账户,需要创建一个应用,包名要和你自己的项目一样,然后需要用到的就是应用的appId、appKey、appSecret,这些在环信开发者后台上传小米证书,以及在项目中初始化小米推送需要用到;

    开始集成

    首先这边先把证书弄好了,证书的名字和秘钥以及包名一定要对应:

    QQ20170505-162024.png


     
    然后需要做的就是在代码中集成小米推送,需要做的有两个地方:

    在初始化 sdk 的时候调用 options 设置小米的 appId 和 appKey
    在 AndroidManifest配置文件配置相应的权限和广播接收器以及服务
        /**
    * 初始化环信sdk,并做一些注册监听的操作,这里把其他的处理都去掉了只写了小米推送
    */
    private void initHyphenate() {
    // 初始化sdk的一些配置
    EMOptions options = new EMOptions();
    // 设置小米推送 appID 和 appKey
    options.setMipushConfig("2882303761517573806", "5981757315806");
    // 初始化环信SDK,一定要先调用init()
    EMClient.getInstance().init(context, options);
    // 开启 debug 模式
    EMClient.getInstance().setDebugMode(true);
    }

    然后就是AndroidManifest配置
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.vmloft.develop.app.demo.call">

    <!-- 项目权限配置 -->
    <!--小米推送相关权限-->
    <permission
    android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"
    android:protectionLevel="signature"/>
    <uses-permission android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"/>
    <!--小米推送权限 end-->
    <!--程序入口-->
    <application
    android:name="com.vmloft.develop.app.demo.call.AppApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:largeHeap="true"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    ...
    <!--小米推送相关配置-->
    <service
    android:name="com.xiaomi.push.service.XMJobService"
    android:enabled="true"
    android:exported="false"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:process=":pushservice"/>

    <service
    android:name="com.xiaomi.push.service.XMPushService"
    android:enabled="true"
    android:process=":pushservice"/>

    <service
    android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
    android:enabled="true"
    android:exported="true"/>
    <service
    android:name="com.xiaomi.mipush.sdk.MessageHandleService"
    android:enabled="true"/>

    <!--推送消息广播接收器-->
    <receiver
    android:name=".push.MIPushReceiver"
    android:exported="true">
    <intent-filter>
    <action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE"/>
    </intent-filter>
    <intent-filter>
    <action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED"/>
    </intent-filter>
    <intent-filter>
    <action android:name="com.xiaomi.mipush.ERROR"/>
    </intent-filter>
    </receiver>
    <receiver
    android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
    android:exported="true">
    <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>

    <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
    </receiver>
    <receiver
    android:name="com.xiaomi.push.service.receivers.PingReceiver"
    android:exported="false"
    android:process=":pushservice">
    <intent-filter>
    <action android:name="com.xiaomi.push.PING_TIMER"/>
    </intent-filter>
    </receiver>
    <!--小米推送配置 end-->
    </application>
    </manifest>










    其中MIPushReceiver这个广播接收器可以不用自己实现,环信 sdk 已经集成小米广播接收器EMMipushReceiver实现,可以直接用(这里如果需要自己与自己的业务处理可以继承它去处理自己的逻辑;详细可以根据小米推送官方 sdk 文档进行了解下);

    当我们做完这些之后在收到离线消息后就可以收到推送通知了,只不过这个推送通知我们不能自定义,因为这些都是服务器推什么我们接受什么,这点比较坑!

    通话的离线通知

    上边已经实现了消息的离线通知,我们下边就要做当呼叫对方时,对方却不在线,我们怎么通知对方打开 app 进行接听呢?
    曾经集成过环信用户应该知道,在呼叫对方不在线后会马上结束通话,回调对方不在线,在新版3.2.2的 sdk 中新增设置音视频参数及呼叫时对方离线是否发推送的接口,在初始化的时候进行以下设置:
    // 设置通话过程中对方如果离线是否发送离线推送通知,默认 false
    EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(true);
    // 设置了这个之后就不会在通话状态监听中回调对方不在线,需要实现另外一个回调
    ...
    // 设置音频通话推送提供者,在 onRemoteOffline()回调中给对方发送消息就行了
    EMClient.getInstance().callManager().setPushProvider(EMCallManager.EMCallPushProvider {
    @Override public void onRemoteOffline(String username) {
    EMMessage message = EMMessage.createTxtSendMessage("有人呼叫你,开启 APP 接听吧", username);
    // 设置强制推送
    message.setAttribute("em_force_notification", "true");
    // 设置自定义推送提示
    JSONObject extObj = new JSONObject();
    try {
    extObj.put("em_push_title", "有人呼叫你,开启 APP 接听吧");
    extObj.put("extern", "定义推送扩展内容");
    } catch (JSONException e) {
    e.printStackTrace();
    }
    message.setAttribute("em_apns_ext", extObj);
    message.setMessageStatusCallback(new EMCallBack() {
    @Override public void onSuccess() {
    // 在这里可以删除消息
    }
    @Override public void onError(int i, String s) {
    // 在这里可以删除消息
    }
    @Override public void onProgress(int i, String s) {}
    });
    EMClient.getInstance().chatManager().sendMessage(message);
    }
    });

    实现了上边的这个推送提供者之后,当对方不在线就会回调 onRemoteOffline()方法,就可以发送一条消息给对方,然后上边我们已经集成了小米推送,就可以通过离线推送的方式通知对方有新消息,对方看到后点击通知栏就可以打开 app了,这个时候我们的语音或视频呼叫还在一直呼叫,然后就可以连通了!

    结语

    OK 到这里基本就已经完成了,大家可以运行自己的项目,或者我上边的 demo 测试下,我这边通过小米5测试 OK;其实集成推送部分并不难,只是有几点需要注意:

    环信开发者后台的推送证书设置时一定要注意应用包名和小米推送后台的应用包名以及自己项目的包名,三个地方一定要一致
    初始化设置一定要通过环信的 options 去设置小米推送的 appId 和 appKey,不需要用小米的注册方法自己注册;
    Androidmanifest 一定要加上环信的广播接收器,或者继承自环信封装的广播接收器

    注意以上几点基本推送就没有问题了,如果不行可以先通过小米开发者后台的推送工具测试推送是否通了,然后检查以上几点;


    PS:华为推送相关其实一样,不过因为华为不允许个人开发者注册账户,所以这里暂时不赘述



    参考资料

    小米推送 Android SDK文档
    环信推送相关文档收起阅读 »

    环信移动客服v5.17已发布,客服可以通过名字、昵称搜索会话,查找客户更方便

    客服模式 支持快速搜索客户会话 在进行中会话列表,支持通过客户的名字、昵称进行搜索,以便客服能够快速查找客户的会话。  管理员模式 支持设置上班时间才自动分配会话 新增“只有上班时间才自动分配会话”开关。开关打开时,上班时间开始自动调度会话...
    继续阅读 »
    客服模式

    支持快速搜索客户会话


    在进行中会话列表,支持通过客户的名字、昵称进行搜索,以便客服能够快速查找客户的会话。 


    001.png


    管理员模式

    支持设置上班时间才自动分配会话


    新增“只有上班时间才自动分配会话”开关。开关打开时,上班时间开始自动调度会话给客服,下班时间结束自动调度。该开关可以与“允许客服手动接入会话”开关配合使用,以便所有客服均只在上班时间接待会话,使工作量分配更加公平。

    可以进入“管理员模式 > 设置 > 系统开关”页面,打开该开关。 

    002.png


    公共常用语支持全部删除

    公共常用语支持全部删除,方便管理员使用模版批量添加常用语。 

    003.png


    在多个页面显示Iframe页签

    在客服模式的会话、历史会话页面,管理员模式的历史会话、质量检查页面,均支持显示Iframe页签,方便已集成CRM系统的客户通过Iframe查看、编辑客户资料。

    关于如何集成CRM系统,请查看CRM系统对接。 

    004.png


    【优化】筛选时支持对客服进行多选

    在客服模式的留言页面,管理员模式的留言、历史会话、当前会话页面,对留言、会话进行筛选时,支持同时选择多名客服。
    • 留言:点击“自定义留言筛选”,对留言进行筛选;
    • 历史会话、当前会话:点击“筛选排序”,对会话进行筛选。


    【优化】筛选时支持对渠道、关联进行多选

    管理员模式下,在统计查询的工作量、工作质量页面,对统计数据进行筛选时,支持同时选择多条渠道、多个关联。  收起阅读 »

    环信SDK3.1.0-3.3.1升级改动

    1. EaseUI--EaseNotifer.java if(EMClient.getInstance().chatManager().isSlientMessage(message)){ return; ...
    继续阅读 »
    1. EaseUI--EaseNotifer.java 
    if(EMClient.getInstance().chatManager().isSlientMessage(message)){
    return;
    } (326, 175, 126)
    改为if(EaseCommonUtils.isSilentMessage(messages.get(messages.size()-1))){
    // 修改的地方 messages.size()-1)有的参数时message , 请注意
    return;
    }
    2. EaseConversationAdapter 
    performFiltering() -- conversationId() 替换  getUserName();  
    3. LnConversationAdapter 
    (378, 146, 258)(149  本来就存在)  conversationId() 替换  getUserName();
    4.  EaseChatFragment 
    (746) 少去两个方法 (onMessageReadAckReceived, onMessageDeliveryAckReceived)
    添加两个方法(onMessageDelivered, onMessageRead)
    5. EaseChatFragment
    EMChatRoomChangeListener -- 全部方法被注释掉了!
    6. EaseGroupRemoveListener 
    EaseGroupListener  -- (1885)
    7. CallActivity
    (175) EMClient.getInstance().callManager().endCall(); 添加异常处理
    8.  CallActivity 
    (333) setReceipt() 改为 : setTo ()  
    9.  conversationListFragment
     (118, 121) -- getUserName() 改为 : conversationId()
    10.  DiagnoseActivity 
    (67)  VERSION 修改! 
    11. GroupDetailsActivity
    (1638) 修改了GroupChangeLister 
    12. GroupDetailsActivity 
    (739) 修改了GroupChangeLister   添加方法 updateGroup(); refreshMembers();
    13. MainActivity
    修改了EMMessageListener()实现的方法  (309)
    14.  MainActivity
    修改了EMContactListener()要实现的方法 (435) 
    15. ChatFragment 
    (750) position = conversation.getAllMessages().indexOf(message);
    16. PublicChatRooomsActivity
    (132) ChatRoomChangeListener修改  -- (206) 复写监听方法
    17. VideoCallActivity 
    (274, 235) 电话断了  枚举变量修改   
    18. VideoCallActivity 
    (431, 432, 433 )  -- getVideoTimedelay 改为  getVideoLatency
    getVideoFramerate 改为 getVideoFrameRate getVideoLostcnt 改为
    19. VoiceCallActivity 
    (203, )DISCONNNECTED (挂断电话) 改为  DISCONNECTED  (249)
    ERROR --
    20. EaseChatRow
    (339) 重写updateView方法, 添加多参数方法  
    21. EaseUI 
    (147) options.setNumberOfMessagesLoaded(1); 方法弃用  
    22.  EaseUI 
    (176, 181) onMessageDeliveryAckReceived() onMessageReadAckReceived()方法弃用  重新添加两个方法 
    onMessageDelivered 和 onMessageRead



    23. DemoHelper
    (524) 原有方法 : onInvitationReceived, onInvitationAccpted, onInvitationDeclined
    onUserRemoved, onGroupDestroy, onApplicationReceived, onApplicationAccept, onApplicationDeclined
    onAutoAcceptInvitationFromGroup,
    onInvitationAccpted, -- 方法名错误 少个Accpted 改为 Accepted
    onGroupDestroy 改为onGroupDestroyed
    24. DemoHelper
    (846) EMContactListener -- onContactAgreed 改为 onFriendRequestAccepted 
    onContactRefused 改为 onFriendRequestDeclined
    25. DemoHelper 
    (1119) onMessageReadAckReceived, onMessageDeliveryAckReceived 去除      
    添加 onMessageDelivered , onMessageRead
    本文章纯属原创,转载请说明出处!  谢谢!!!!!! 收起阅读 »

    头像昵称的简述和处理方案

    环信的头像昵称,是一个老大难的问题,但是环信负责的是im的通讯能力,它只负责消息的通畅,头像昵称啥的,不是它要处理的,所以,只能给出一些建议性的解决方案。   从实际角度出发,有几种场景需要使用头像: 1、聊天的详情页面。 2、会话列表页面。 3、好...
    继续阅读 »
    环信的头像昵称,是一个老大难的问题,但是环信负责的是im的通讯能力,它只负责消息的通畅,头像昵称啥的,不是它要处理的,所以,只能给出一些建议性的解决方案。
     
    从实际角度出发,有几种场景需要使用头像:
    1、聊天的详情页面。
    2、会话列表页面。
    3、好友页面。
     
    针对以上几个场景,
    在聊天页面,接受消息的时候,需要知道对方叫什么,所以只需要发送方发消息中,把自己的名字,昵称带过来就行。
    这里可以选择用message的属性ext来实现。
     
    message.ext = @{@"nick":@"李明"};


    9CF6F919-BA41-48C7-8D8C-85D83E1838D8.png


     
     
    当接收方(韩梅梅)收到消息后,只需要解析message中的ext字段,就可以知道发送方叫李明。
    之后再ui上显示就可以了。此处的处理,单聊和群聊是一样的。
     
    注意事项:
    发送方设置ext一定要在消息发送之前。
     
     
    2、会话列表中展示:
    上文说道,ext属性是message的,而会话的对象是conversation。此时,如果需要得到对方最后一条消息,可以使用
    EMConversation对应的属性lastReceivedMessage。这个属性的描述是
     
    /*!
     *  \~chinese
     *  收到的对方发送的最后一条消息
     *
     *  @result 消息实例
     *
     *  \~english
     *  Get last received message
     *
     *  @result Message instance
     */
    - (EMMessage *)lastReceivedMessage;
     
    这个时候,我们只需要去去 conversation.lastReceivedMessage.ext, 也就可以得到会话列表的头像了。
     
     
    注意:
    群聊里,不能通过最后一条对方发的消息来处理,环信的群属性中,没有头像这一条,所以也不能直接从环信这边取,但是它有群描述。这里提供两个思路:
    * 把群属性放到自己的服务器,群头像也放到自己的服务器上,然后根据群id去自己服务器获取。
    * 把头像放到群名描述里,之后用自己定义的格式隔开,取头像的时候,就直接从群描述的url来取。
     
    3、好友列表中头像和昵称的获取。
    好友列表,好友的信息环信并没有提供多少属性,只提供了环信id。所以这个地方我们就没办法从环信这边获取任何信息了。这个地方说下环信demo的解决方式:
     
    环信demo中,用了一个第三方的云服务叫Parse,Parse的作用,是可以把k-v的键值对存到云服务器上。环信demo就把好友列表中,好友的环信id作为key,其他属性作为value存到了Parse上。这样在展示的时候,就直接去parse上下载,得到对应的昵称和url。 
     
    注意:
    不过就我了解,国外的Parse服务已经停止了。目前demo里用的是环信自己搭建的一个Parse服务,环信官方也没有承诺该服务始终免费,记得当时也是说过是为了demo展示用才搭建的,所以建议还是自己找一些其他提供类似功能的云服务来使用比较好。
     
     
      收起阅读 »

    APNs证书创建和上传到环信后台

    在iOS中,当app进程不存在的情况下,如果需要向设备发送通知,可以苹果提供的APNs 下面大概讲一下如果创建APNs证书和上传到环信。(首先需要有一个付费的苹果开发账号,否则无法创建相关证书) 文章最后有常见问题 1、前期准备   创建根证书很重要,要...
    继续阅读 »
    在iOS中,当app进程不存在的情况下,如果需要向设备发送通知,可以苹果提供的APNs
    下面大概讲一下如果创建APNs证书和上传到环信。(首先需要有一个付费的苹果开发账号,否则无法创建相关证书)

    文章最后有常见问题


    1、前期准备
     
    创建根证书很重要,要确保创建根证书的电脑和最好导出P12的电脑是一台,否者可能无法创建成功。
     
    打开电脑的“钥匙串访问”并按照以下操作


    196FFE18-F615-441B-955B-C22526FC7550.png


     


    439D8CD0-B95E-4639-923E-E39EB862FAAB.png


    邮箱需要符合邮箱格式,名称随意,之后保存到本地。
     

    06DF2A8F-8599-415A-B132-ABC0FA326C6F.png



    2、创建支持推送的APP
     


    07CFC71F-8F5A-4AF6-9716-0698572F483C.png




    D6439761-4693-4C83-BEC4-2A804A089C6D.png




    5B03EE6F-82CF-4B95-8079-298192464261.png




    588C53C4-951C-4175-BA3E-FABD79C0C714.png




    273DB89F-0D81-4AD2-99B5-1C9E6A539EF5.png



    3、创建推送证书
    此处以开发推送证书为例


    1F0696C9-4C96-482E-84F8-14AC2CC6BD09.png




    18F988F1-C700-4161-90DA-E2E4332FDE36.png




    F911C057-8279-4CFC-B1E2-0C6D54C66992.png




    F923A032-1318-4686-A6F7-69FA5CE015B1.png




    71C69E1B-D0A7-4929-9641-873D6FEB7FCF.png




    69093353-7D5E-4864-A42B-06D5EBD0E24B.png




    6CB97BF4-DB2D-4F31-B611-EA4F848A9133.png




    C8F22BCF-82B8-4BE3-87C9-972A88A5B567.png




    37F49798-C4C4-475A-93AC-5E4FE32B37D6.png




    77B837F9-3F76-4431-8C45-C0967A4F46F0.png




    5980CC0C-00F0-40E0-8B39-8A4642CCF55D.png



    再用同样的方式创建生产证书,注意命名要有区别。
    此时,我们应该有三个文件:


    8D1E5C71-B73D-47D5-AA63-EE682F1B3630.png


     
    4、制作环信用的P12推送证书
     
    同样以开发证书为例,双击导入aps_development.cer,


    778565F3-543F-4AE6-9CDA-E22F1CFF0DF5.png




    EC7CEA55-9CF5-4CB1-ADF0-10AA10652E0A.png




    9AB6B10F-3773-4F05-977F-0DA4FB8F4C93.png




    BA715C6D-684B-4BCF-9BBD-457C0763088B.png





    8A6A3280-1C1F-426E-8B2E-DF54AB90E3F0.png


     
    以同样的方式再生成生产用推送证书。此时应该一共有5个文件。


    9ADEA2A5-B424-49C7-B2C6-7A9F4E3667F8.png


     
    5、上传到环信
     


    CBA586BA-AB11-4A4A-AA35-9F3AEC7E8321.png




    195BA6B9-EBAA-47A6-B2A9-CFEC9C6987D9.png




    592ED719-5220-4D2F-B5DC-8AA98BA6AA89.png




    5BF934C9-2DD6-4CFD-B832-5B87E592E4A1.png




    7FAC6438-1349-414B-AE3C-90AC13AC704B.png




    759E8827-4DA4-459B-9079-5726FE7F9E71.png




    8946D27B-9EF2-424C-B2CC-2B96A83C5A67.png



     
    如果需要填写bundle id,一定要确保填写正确。
     


    CEAFFB8B-ED09-48CD-8F61-FA2A7260BB5F.png


    注意:此处也需要选择正确,是开发模式还是生产模式。
    密码就是到导出证书时候的密码。
     
    注意事项:
    app工程里要打开推送开关


    872EEB37-A79A-4221-9CCF-CFE6ABAB090B.png


     
     
    ==============================
     
    常见问题:
    为什么我按照配置后,app后台了还是收不到推送!


    环信的长连接存在的情况下,在服务器就属于在线状态,环信不会通过苹果的APNs 给你发推送,而是直接通过环信的长连接,只有app后台被系统挂起或者是进程被杀死了,才会走APNs,先出怎么处理:
     
    在appdelegate文件里,也实现消息的监听,这样有消息了,也能收到回调。
    收到回调后,先判断当前app的状态是在前台还是后台,如果是前台就忽视这条消息,如果在后台,就自己从代码里实现一个本地通知,把需要展示的消息内容得到后,自己发localNotifications. 本地通知的实现方式很简单,网上百度就行。
     
    下面我说下这种方式的好处与坏处。
    好处:在正常使用场景中,app之间切换很正常,这样的好处就是不需要频繁的断开重连,速度会很快,同时也会比较省电,而且用户体验会更好。
     
    坏处:其他app里,app的icon上的角标和app内部的角标数量是一致的,但是像自己弹出的处理方式,会可能导致角标不一致,因为apns的角标是服务器发过来的,而localNotification的角标是由app自己设置的。
     
    顺便说一句,目前微信的实现方式也是后台的时候长连接保持,app角标也存在不一致的情况。
     
    APP之前没有用推送,现在需要用了,我按照上面设置后还是不行。
     
    需要删除本地的描述文件,重新去开发者中心下载,描述文件就是Provisioning Profile
     
    APP上线之前,如何测试生产的推送是否好用?
    这个情况苹果已经替我们想好了,在打包的时候,有一个选项是ad-hoc。这个选项就是打一个生产用的包,并且可以导出保存到本地,之后用itunes安装就可以了。这个地方一定要注意,这个使用使用的证书需要是生产的证书了哦~ 收起阅读 »

    【客户世界·洞察者】刘俊彦:观点(二) 跨渠道环境下的客户服务体验是客服行业面对全媒体客服新趋势的主要挑战

    导读: “客户世界·洞察者” 随着信息爆炸的互联网时代的到来,人们阅读习惯的改变,客户世界推出立体化内容呈现方式(图文+短视频)来迎合人们碎片化时间的阅读。那么越来越高标准的阅读需求靠什么来满足? 众所周知《客户世界》做为国内唯一定位于“客户管理”...
    继续阅读 »
    导读:
     “客户世界·洞察者”

    随着信息爆炸的互联网时代的到来,人们阅读习惯的改变,客户世界推出立体化内容呈现方式(图文+短视频)来迎合人们碎片化时间的阅读。那么越来越高标准的阅读需求靠什么来满足?
    众所周知《客户世界》做为国内唯一定位于“客户管理”专业研究及其关联产业发展的专业纸媒,在客户中心行业广受赞誉。其精准的内容定位锁定了企业客户管理各级岗位的专业人士。
    行业中不乏有思想、有洞察力的行业洞察者,那么他们的观点将是行业人士追捧的话题,这时候就需要一个媒体平台,以猎奇的视角、亲和的形势展示出来。那么客户世界•洞察者就扮演了这个角色。
    “客户世界·洞察者”每周一个观点,敬请期待……
    本期洞察者:刘俊彦 环信CEO

       刘俊彦毕业于伦敦大学国王学院,计算机硕士。先后任职IONA,RedHat。重度开源软件参与者,JBOSS ESB, SOA-P、Apache CXF、JBOSS Drools、jBPM 等开源项目committer。专注于高并发消息中间件,实时消息系统,异构分布式企业系统集成和应用服务器。
    视频观看地址:【客户世界·洞察者】刘俊彦:观点(二) 

       上一期洞察者讲述的关于工具层面的全媒体接入的各种核心技术问题,本期洞察者谈谈当客户从各种媒体各种渠道接入进来以后,我们应该做些什么来提交效率和客服体验。全媒体客服不仅只是多渠道的接入和各个接入渠道之间的数据打通,更重要的是用户跨媒体、跨渠道、跨部门的体验和跟踪,在海量的数据中发现问题。
       多渠道客服接入环境下的客户声音的收集,整理,分析和理解需要一个企业全流程业务部门的参与,包括客服部门、产品研发部门、销售部门和市场部门之间的通力合作。“客户声音”将帮助企业解决四大挑战,请看下图:

    QQ截图20170502110938.jpg


    为了应对以上四大挑战,厂商推出了客户声音产品。客户声音产品是一款基于人工智能和大数据挖掘的客户体验透析产品。通过对来自多个渠道的非结构化客服数据进行自然语言解析,主题聚类,情感度建模等技术分析手段来挖掘和分析热点话题,发现服务运营问题,寻找畅销或问题产品,洞察销售机会。通过透析客户对企业产品和服务的准确体验,帮助企业识别和改善客户旅程的各个阶段。

    一、如何帮助企业倾听客户声音呢?
    1. 整合多渠道数据源,透析客户对企业产品和服务的准确体验。
    2. 基于人工智能技术,帮助企业识别和改善客户旅程的各个阶段。


    二、“客户声音”产品帮助提高用户体验的一些实践案例:

     1,主题及关键词热度分布,实时了解用户最关心的产品和服务。

    640.jpg


    实例:上述图1显示了某电商主题关键词热度分布。四种颜色代表四个主题,主题和关键词的比例表示该主题或关键词的用户讨论热度,关键词字体越大,表示热度越高,关键词颜色表示情感度。

    2,主题及其关键词情感分析,及时追踪到负面情绪。

    641.jpg


    图2示例:上图是讨论热度最高的主题“注册登录”下头15个关键词的情感估值对比图,绿柱表示用户满意度高。如果用户对某关键词对应的业务充满负面情绪,系统用红柱突出表示出来。黄色表示中性。

    3,按关键词或情感度追踪问题并解决问题。 

    642.jpg


    图3示例:某电商按关键词或情感度追踪并解决物流快递问题。
     
    转自客户世界,原文地址阅读原文 收起阅读 »

    人工智能,是在砸你饭碗还是在帮你挣钱?

      “近两年,AI很火,就我个人而言,我看到了它目前在工业领域产生的四个价值,自动驾驶、图像识别、医疗影像、再就是客服领域。”环信CEO刘俊彦在国内电商门户亿邦动力思路网专访中说道。 (图为环信CEO刘俊彦)    据刘俊彦介绍,环信是一家经过...
    继续阅读 »
      “近两年,AI很火,就我个人而言,我看到了它目前在工业领域产生的四个价值,自动驾驶、图像识别、医疗影像、再就是客服领域。”环信CEO刘俊彦在国内电商门户亿邦动力思路网专访中说道。

    微信图片_20170502104525.jpg


    (图为环信CEO刘俊彦)
       据刘俊彦介绍,环信是一家经过C轮融资后定位为全媒体智能客服的年轻创业公司,目前有三条产品线,第一个是环信即时通讯云,为企业提供通讯服务。第二个是环信移动客服,是一款针对电商、金融、教育等行业的SaaS客服软件。第三个是环信人工智能机器人,这个产品的产生是因为他们看到了客服行业的一个新“风口”。

       客服行业是企业服务市场里面的核心领域之一,而企业级服务这个大市场,刘俊彦认为是“既无内忧又无外患”,为什么这么说?

       企业服务在美国是个很成熟的行业, Oracle、微软、SAP等企业加起来已经有4千多亿的市值。而中国,所有的服务企业加起来不超过50亿美金,所以,无内忧是指市场还不成熟,除用友,金蝶外没有什么大公司,这条赛道还是空的,你要做的就是稍微比别人跑的快点。未来发展前景还很广阔。

      国家规定个人数据要保留在中国,外资公司不允许在国内建立数据中心。由于牌照问题,也进不来。这样就没有外患。

       所以,这么好的市场环境,未来机会很大,根本没有天花板,自己就是天花板,突破自己往前跑就对了!

       “近两年,AI很火,就我个人而言,我看到了它目前在工业领域产生的四个价值,自动驾驶、图像识别、医疗影像、再就是客服领域了。这个行业有大量的数据,可是客服行业在当下阶段是个“缺人”的行业。基于这些,我们开发了人工智能这条产品线。目前落地的产品是环信智能客服机器人,对于企业而言,一些简单重复的问题就可以交给机器人来回答,可以大幅降低人工工作量提升效率。”刘俊彦向思路网说道。

       目前,人工智能在售前售后都可以发挥价值。售后是以服务,解决问题为导向。比如常见的有简单的退换货和产品售后使用问题,而这种问题智能机器人是可以完全解决的。现在,客服机器人已经可以解决80%的常见问题了。

       但是,还有一种相对难的售后问题,像物流问题。举个例子:假如消费者提出:为什么我的宝贝还没到货?机器人回答,对不起,您在等等。这肯定是不行的,一下子就降低了用户体验。

       人工客服在解决此问题时,都会问问订单号,然后去物流系统查询再反馈给顾客。其实这块人工智也做的很好了,类似这种物流场景问题,机器人都可以解决,完全不用人工参与,所以在双十一的时候环信的几个用户上了这个功能,发货后,消费者在询问的时候明显是解决了大量的工作量。

       在售前环节,有个功能叫“人机协作”,虽然客服机器人做的还不错,但是跟一个优秀的销售比较还是有差距。所以,机器人怎么工作?其实它一直处于“充电”状态,当消费者与销售产生沟通的时候,机器人一直在听,可不是单纯的听,它可以通过分析能给销售推荐下一句适合说的话。

       众所周知,客服行业人员流失率很大,那么新人刚上岗第一时间接触消费者会出现不知道如何沟通,而这个时候智能机器人的“人机协作”能力就可以发挥了。除此之外,机器人还可以做产品推荐。

       据了解,目前新东方、泰康在线、中信证券、国美在线、链家、神州专车等都为环信用户。

       企业服务市场赛道众多,为何环信单单会锁定在客服领域?这个问题,勾起刘俊彦回忆起他的创业初衷。

       创业前,我们没想到会聚焦在客服这个领域,本身我是技术出身,之前做通讯方面的产品,对IM比较熟悉。2013年,微信,陌陌火了,很多人都在模仿他们。然后有人就找到我,让我帮忙做一个师生交友的工具,之后很多人都找我帮忙,帮一个两个容易,多了也没精力。就这样,我想是否能出个产品,做个系统框架,当你需要时看看文档看个Demo自己就能搞定。这么一个契机,创立了环信,做了第一个产品环信即时通讯云。

       之后我们又发现了一个很有意思的事情,以前是帮助大家做社交,连接人与人,但是有一个场景是之前没想到的,就是连接人和商品,旺旺就是一个最典型的例子,连接的是消费者和商家。这对我们是个很好的启发,由此我们用IM做了个连接人和商业的移动端客服产品。所以,只要是APP里面需要做内置客服的商家,几乎都是环信的用户。

       但是,我们又意识到了做APP内置客服更多的是个增量市场,不够大,所以即使达到垄断局面却还是觉得是小水洼里面的霸主。所以最后扩展到全媒体客服,包括网页、微博微信、电话等渠道,不局限于APP。

       可是,当产品做到一定程度后,又出现了一个很大的问题。产品趋于成熟,竞争依旧很激烈。在与客户聊天的时候发现大家的产品慢慢都走向驱同。由此,我们盯上了人工智能这条路,意识到这才是拉开各个企业之间差距的关键点!我们不再谈全渠道接入,不再谈报表如何厉害。我们谈什么?我们谈的是人工智能如何在你公司落地,如何降低客服成本,如何帮助客服提升转化率,如何实现精准营销。

       就眼前而言,如果你没意识到人工智能的重要,那么很可能就错过了,其实,环信刚开始接触这块的时候,也很犹豫,首先建立这么个团队花费很高,需要长期投入,当时决定做人工智能的时候内部也有很多争论。要不要做?值不值得?最后还是统一了意见,组成了20多人的研发团队专攻这块,决定跑下去。欣慰的是,现在已经产生效益。

       人工智能或许在未来更加智能,那么是否有一天可以颠覆传统客服人员?

       针对这个问题,刘俊彦有不同的看法“我觉得不会,目前我还没看到哪个企业因为用客服机器人用的好而裁员呢,呵呵,其实,目前互联网企业普遍很重视客户的服务体验。机器人确实解决了一些人力,但是,是将原来的这些人从新分配专注在提升用户体验的其他岗位上。

       举个例子,神州专车是我们的客户,现在他们司机端使用的是环信客服机器人,之前他们是由人工客服为司机服务,经常要回答司机遇到的海量紧急问题,比如:乘客下车忘记付款、修车和报销规则等...接通环信客服机器人以后,节约了大量的人工客服工作。

       刘俊彦表示,2017年,环信的重心会放在突破大客户上,对于一家企业服务公司,最终拼的还是大客户。其次会服务一些重点行业,例如银行,教育、保险、电商类。最后,会重点推动人工智能在客服行业的落地。

       但是,从客服软件行业本身来看,未来面临几大挑战。

       首先是移动端的挑战,国内企业客服有四个最常见的通道,电话、官网网页、微信公众号、移动端客服。但是消费者已经表现出一种很明确的趋势,需求点在移动端。那么在手机上最好的客户服务体验是什么?值得客服行业从业者深思。

       其次,消费者在线需求渠道目前还处于多样化,以前只是电话沟通,通过质检便可以了解用户的满意度,现在,渠道多样化,需求多样化,所以想关心用户体验变得异常困难,就这样很多企业已经放弃了用户体验这个环节。所以,知道用户说什么,关心什么,吐槽什么,这是以后对于客服软件行业的一个挑战。而环信去年推出了环信客户声音,来解决和提高用户跨媒体、跨渠道环境下的客户服务体验

       再次,就是人工智能的挑战,也许有一天人工智能就颠覆了客服软件。那这样,环信不就消失了吗?我认为,我们这种做客服软件的公司最多还能活五年!所以,与其别人颠覆你,不如自己颠覆自己,主动出击迎接挑战!

       最后,人力成本对于客服行业来说也是一大挑战。我们有的客户最初是在北京市中心,慢慢的开始搬向周边,甚至开始向其他省市迁移。人力成本提高,从业者逐渐年轻化,不愿意从事客服工作,而客服行业本身又需要较强的心理因素,故此人员短缺。

       其实,对于客服软件来说,降低企业人工成本其实是次要的,重点是充当销售提升转化率。很简单的一个道理,你想挣钱,你不能只会省钱!用户真正需要的是通过产品达到赚钱的目的。

       例如环信有个客户是做儿童座椅的,以前通过发送短信只有百分之零点几的转化率,用环信的客服软件后达到了百分之二三十的转化率。在客单价较高的情况下,成交量提高了很多。

       所以,商家在选择服务商的时候,有些还是值得注意的。刘俊彦也给出了自己的建议。对于大品牌企业来说,真正应该在乎的是这个服务企业是否可以长期陪伴你走下去?是否可以一直保证走在技术前沿?

       其次,规模实力也很重要,一个规模完善发展稳定的服务企业是双方合作的基础。

       最后,一定要关注移动端业务。选择擅长移动端的服务商,中国的几亿用户几乎都在移动端,商家必须将移动端重视起来! 收起阅读 »

    环信入选《创业家》最具价值企业服务商推荐榜,听经纬熊飞聊怎样才是靠谱的企业级生意!

    为了预见中国企业级服务市场即将诞生的独角兽,为了给《创业家》黑马用户推荐最优质的企业级服务商。创业家I黑马梳理了《冲刺期最具服务价值的企业服务商推荐榜》,环信凭借优秀的产品能力和领先的市场占有率入选榜单...企业级服务市场究竟该如何看待,记者近日接触经纬中国投...
    继续阅读 »
    为了预见中国企业级服务市场即将诞生的独角兽,为了给《创业家》黑马用户推荐最优质的企业级服务商。创业家I黑马梳理了《冲刺期最具服务价值的企业服务商推荐榜》,环信凭借优秀的产品能力和领先的市场占有率入选榜单...企业级服务市场究竟该如何看待,记者近日接触经纬中国投资董事熊飞先生听他讲述看待行业的方式,这篇口述文章也不失为大家看待行业的一种可参考性方向。



    001.jpg


    经纬中国投资董事熊飞先生


    口述 | 熊飞
    采访 | 李阳林
    整理 | 张一、李书娜

       首先,我认为(企业服务)行业是非常健康的。对于很多2C的需求,比较容易被大众所理解,因为大部分人都是用户;在2B领域,HR系统所提高HR的效率,一年价值是好几百万,所以创造的价值是很实在的。这是这个行业作为一个系统性的风口起来的一个机会。

       在中国,推动企业级服务领域发展的原因主要有两个:一、人力成本的持续上涨;二、中国经济进入新常态,商品市场供大于求导致对于效率的要求提高。

       先说人力成本。人力成本应该说是企业级服务发展的动力,中国企业发展已进入第一个拐点,这个拐点像六七年前一个人,每月三千块,一个电脑五千块。现在反过来,一个人每月五千块,一台电脑二千、二千五。企业主很理性,原来不用(工具)觉得贵,现在不用就是傻,人越来越贵,IT越来越便宜。

       再看经济模式转型。5年前中国经济的增长方式比较粗放,最近两三年系统性供大于求。以前的企业发展都是拉贷款扩产能,人越多体量越大。现在的市场已经变成了各行各业供大于求、毛利下降,各种各样的产品卖不出去,反过来人力成本还在涨。所以企业主都希望是不是能有一个工具,能够让100人干150人的活。这个市场客观需求在爆发。
     
    判断是否健康的三个标准
        三个判断标准在上述大背景下判断一家企业级服务公司是否健康,主要看:一、客户规模的大小;二、客户续费率的高低;三、公司的月、年盈亏是否平衡。
       第一:先看供应端企业的大小。企业级服务行业的马太效应比2C要强,巨头们通常资源广阔。但只有这些还不行。在企业级领域行业核心的本质是门槛、有时间的积累。一方面包括产品相对比较复杂;另一方面它需要用户的锤炼,是在跟用户互动的过程中,根据用户反馈去优化产品,包括各种配置,销售也是一个时间积累好的售前,好的实施,好的客户成功。要懂这个领域,不是从零开始,而是需要有很强的行业背景。

       市场是后知后觉的,两年前还是企业服务元年,我们喊要中大型客户。很多人都不相信,都去做中小型客户。原来做中大型客户的公司五六千万收入,亏个两三千万,大家觉得SaaS穷途末路,觉得这个东西不挣钱,不能投;两年之后,再看,这个市场并不是那么回事。这是因为你的续约率在那儿。

       第二,看客户续费率,用户续费率低于80%,基本上就是一个不太靠谱的生意。举个例子,为什么会要求这么高?我当年在一家零售交易平台(现已成为全球最大的零售交易平台),它的用户续费率是百分之六十几,什么概率?看上去挺高的,三年之后就没啥了,65%×65%×65%,剩不了什么?第二年就剩30%多,第三年剩20%多,你今年好不容易获取很多客户,三年之后这些客户剩二十几个了,这不是一个SaaS所谓长需的生意。

       比如:某国内领先Saas公司是90%多的用户续费率,超过100%的金额续费率。其实做大客户服务就符合这么高的续费率,不是说只为了做大客户,而是说大客户的经济性很好,就好像实现梦想有各种方式,做大客户这个方式是实现续约率、实现价值的好生意。

    第三,看公司能否实现单月盈亏平衡、全年盈亏平衡。

       中国Saas公司开始系统性的进入单月盈亏平衡,全年盈亏平衡。何谓单月盈亏平衡或全年盈亏平衡呢?SaaS收费模式是不同于做其它服务。原来一个三百万的单,现在第一年只能收三五十万,因为客户不太相信,只想先试试,供应端可能连三五十万都收不到,只有客户用好了才会持续买,如果不续约,那客户就流失了。理想性的说,金额续约率百分之百的时候,那么在开始之前企业就已经收到钱了。

       续约的维护成本是新销售的五分之一到十分之一。在美国有一个统计,平均你拿新销售一块钱的时候,所有的营销成本加在一起,续约的成本大概两毛钱、一毛七八,因为需求方跟供应方建立联系,需求方用得不错,客户成功,定期跟供应方沟通,帮需求方解决一些问题,收取费用。在这样的过程中,难免会增购其它服务。所以经济性就显著。 
    中国企业级服务公司的未来
     
       现在是Sass B、C轮公司投资的黄金期,中晚期投资Saas黄金期。为什么是中后期?
     
       在美国,企业服务从时间上划分有三拨:七十年代第一拨,微软,现在是四千亿美金的公司;第二拨是90年代末到2000年,所谓的云计算,如今也算是几十亿到五百亿美金公司;第三拨刚刚开始,中国是三拨合为一拨,三年前没有人谈企业服务,现在所有人在谈企业服务。

       如果和美国市场对照,在中国当前这样的环境下,其实是可以出现美国的SAP、微软这样的公司,非常像十五年前2C领域。比如说携程的市值是Priceline、Expedia的总和,为什么?因为在美国爆发互联网旅游的时候,旅行社已经很强大了,所以美国的线上做不起来。但是在中国线下旅行社不强,线上也没有,所以呢,有市场发展空间,所以中国的线上旅行社就发展起来了。再比如,淘宝在中国就比eBay在美国要强大的多了,很大一部分原因是因为美国线下商业形态很成熟,发展空间没那么大。

       现在对照企业级服务也是如此。中国企业级服务市场没有SAP,没有相对成熟的企业级服务公司。所以说中国企业级服务公司发展空间很大,没有天花板。现在的北森、销售易、环信,很可能就是未来各个垂直领域的微软和SAP。当然他们最终能不能做到三五千亿或者两三千亿美金,就看它未来的凶悍生长的能力。

       未来三到五年之内,一定有估值超一百亿到二百亿人民币的公司出现,十年之内,一定有五百到一千亿人民币估值的公司出现。为什么敢这么说?中国现在最大的SaaS公司,今年确认的收入大概在三到四亿,每年还在上升。

    具服务价值的企业服务商推荐榜
     
       为了预见这个市场即将诞生的独角兽,为了给黑马用户推荐最优质的企业级服务商。我们梳理了《冲刺期最具服务价值的企业服务商推荐榜》,排名不分先后,不单一的从估值角度出发,选取C轮、D轮、新三板企业样本,成熟的上市公司不进入参选范畴,由投资人及行业人士推荐,上一轮融资规模。

       我们将请这些未来的独角兽,为黑马及广大用户带来最有效的企业服务领域内的创业知识。黑马学吧将会邀请榜单里的18位成员,成立企业级理事会,位大家带来“企业级创业十八招”,敬请期待。

       本次榜单成型,感谢蓝海通讯创始人何晓阳、六度人和创始人张星亮、黑马企业级服务分会秘书长万涛、环信唐大欢、北森市场副总裁高燕、销售易市场副总裁Joyce在专业上的支持

    002.jpg




    003.jpg


     本文系创业&黑马原创发布,策划内容创业营,未经授权,转载必究。推荐关注i黑马微信公号(ID:iheima)。
    环信成立于2013年4月,是一家国内领先的企业级软件服务提供商,于2016年荣膺“Gartner 2016 Cool Vendor”。产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。截至2016年底,环信即时通讯云共服务了130176家APP客户。环信移动客服共服务了58541家企业客户,现已覆盖包括保险、证券、银行、电商、教育、O2O等领域的众多标杆企业,包括泰康在线、中意人寿、中信证券、国美在线、优信二手车、新东方、新浪微博、链家、58到家、神州专车等典型用户。
    收起阅读 »

    【公开课11回放】环信美女小双mm直播讲解环信客服集成+智能机器人配置

    盼望着,盼望着,东风来了,小双的脚步近了。    在4月20号这天,通过线上报名参加环信公开课的同学,早早的就收到了公众号、短信的通知。算下时间刚好晚上6点,华灯初上,邀约在这个还没开播的直播间里,那一帘幽蓝色的背景为我们的主角再增添了几分神秘,只有这片刻宁...
    继续阅读 »
    盼望着,盼望着,东风来了,小双的脚步近了。

       在4月20号这天,通过线上报名参加环信公开课的同学,早早的就收到了公众号、短信的通知。算下时间刚好晚上6点,华灯初上,邀约在这个还没开播的直播间里,那一帘幽蓝色的背景为我们的主角再增添了几分神秘,只有这片刻宁静才能使程序员们忘记在键盘上敲打了一天的劳累。因为大家都有一个共同的期待,小双来!
    身高165,温柔体贴,善解人意,会做饭会洗衣,会遛狗会铲屎,英雄联盟一区钻一,会花会活不粘人!


    我看着都动心了-某著名互联网公司女产品经理如是说到!



    微信图片_20170421171312.jpg


    拍摄于4月20号18点40分,公开课工作人员正在做直播最后的调试


    众里寻她千百度 那人或在屏幕深处!



    微信图片_20170421171524.jpg


    小双mm直播现场


    环信公开课第11期看点 
    ☞ 教您5分钟快速集成环信移动客服
    ☞ 我们怎样才能将环信智能机器人用在刀刃上!
    ☞ 如何高效的生成一份报表让客服绩效一目了然!

    环信公开课第11期视频回放观看●5分钟集成环信移动客服+环信智能机器人全解析


    收起阅读 »

    环信移动客服v5.16已发布,支持根据渠道筛选留言、新增客服账户管理

    客服模式 支持根据渠道筛选留言 支持根据渠道对留言进行筛选,方便根据渠道对留言进行管理。 进入“留言”页面,点击“自定义留言筛选”按钮,选择渠道,并点击“筛选查询”,即可筛选出对应渠道的留言。 管理员模式 新增客服账户管理 ...
    继续阅读 »
    客服模式

    支持根据渠道筛选留言


    支持根据渠道对留言进行筛选,方便根据渠道对留言进行管理。

    进入“留言”页面,点击“自定义留言筛选”按钮,选择渠道,并点击“筛选查询”,即可筛选出对应渠道的留言。

    管理员模式

    新增客服账户管理

    新增客服账户管理功能,支持管理员启用或禁用其他管理员和客服账户。一个租户下,在同一时间,最大启用数即为该租户的“购买坐席数”。客服管理功能可以帮助您更好地管理客服团队,在团队成员发生变动时,迅速切换启用的客服账户。

    账户处于启用状态时,管理员/客服可以正常登录移动客服系统,并使用角色对应的功能;账户处于禁用状态时,管理员/客服不能登录移动客服系统。

    进入“管理员模式 > 成员管理 > 客服”页面,在“账户启用”一列,启用/禁用管理员或客服账户。 

    001.png


    进入“管理员模式 > 设置 > 企业信息”页面,查看您的租户的“购买坐席数”和“账户到期日”。 

    002.png


    PC客服工作台

    当前版本:V2.1.2017.04060

    新增转接弹窗提示

    当客服收到转接的会话(且“转接会话需要对方确认”开关打开时),PC客服工作台在显示屏右下角弹窗提示:您有新的转接会话。确保客服能够及时处理转接的会话。

    移动客服iOS SDK
    当前版本:V1.0.2

    第二通道支持发送图片、语音消息

    移动客服iOS SDK内置第二通道功能,当IM消息通道(第一通道)出现短暂的消息发送失败的情况时,自动调用第二通道将客户消息发送至移动客服系统,确保客户的所有消息均能准时送达。

    在之前的版本,移动客服iOS SDK的第二通道仅支持发送文本消息;从该版本开始,第二通道支持发送文本、图片、语音消息。

    关于如何集成移动客服iOS SDK,请参考移动客服 iOS SDK 集成。 
     
    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.16 

     
    环信移动客服登陆地址http://kefu.easemob.com/
    收起阅读 »

    iOS 移动客服

    iOS 谁自己写过移动客服的demo      不是官网上的demo 能分享份吗
    iOS 谁自己写过移动客服的demo      不是官网上的demo 能分享份吗

    【客户世界·洞察者视频】刘俊彦:全媒体客服的核心是移动端接入,而移动端接入的最佳体验是基于IM(附Gartner报告全文)

         移动互联时代,客户正转移至移动端,服务需要紧跟客户步伐。Gartner报告指出:“消费者对移动设备的偏好正在快速发展,对于一些行业而言,到2019年,移动设备的使用将占到所有互联网交互的85%,如果不能改善移动客户服务,企业将遭受损失。”因为技术门槛...
    继续阅读 »
      
      移动互联时代,客户正转移至移动端,服务需要紧跟客户步伐。Gartner报告指出:“消费者对移动设备的偏好正在快速发展,对于一些行业而言,到2019年,移动设备的使用将占到所有互联网交互的85%,如果不能改善移动客户服务,企业将遭受损失。”因为技术门槛高目前仅有部分大型企业能够在移动APP上提供端到端的、完整的客户服务支持能力,但是中小型企业的部署热情高涨。同时,在社交媒体上(如Facebook、微博、微信等)入驻的企业都已经开始在平台上提供客户服务能力,相比传统的网页客服和呼叫中心,社交媒体客服更是得到年轻用户的青睐。包括移动APP内置客服、社交媒体客服、网页客服/HTML5客服、传统呼叫中心等接入的全媒体客服已是大势所趋,而全媒体接入的核心在于移动端接入。
     


     
    环信CEO参加客户世界.洞察者节目录制
    当前国内的主要接入渠道包括移动APP内置客服、网页客服/HTML5客服、社交媒体客服(微信、微博)和呼叫中心。由上表可见除开移动APP内置客服以外,其余三个主流的接入门槛较低,技术标准化且成熟,核心难点在于移动端接入。

    1.1,全媒体客服主流接入渠道特性:


    QQ截图20170419151432.jpg


    1.2,支持移动APP内置客户服务的关键技术和最佳实践: 

    1.2.1,移动APP内置客服帮助企业在移动端保持了品牌和服务的一致性:

        在移动APP中内置客户服务,使消费者不需要跳出APP就可以及时得到客户服务支持,而不再需要去寻求第三方比如呼叫中心等传统客服方式。这很好的解决了很多APP运营者,对消费者跳出APP后,可能不再返回APP的忧虑,同时企业保持了品牌和服务的一致性。
     
    1.2.2,移动APP内置客服的最佳体验是基于IM(即时通讯)。

        随着IM(即时通讯)类APP如Whatsapp, 微信等在手机上的流行,IM已经被证明是在移动终端上最适合连接人与人的沟通方式。在客服领域,以环信为代表的一批移动APP内置客服技术提供商的成功,也证明了IM同样是移动终端上最适合连接人与服务的沟通方式。将IM方式用于消费者与客服人员沟通有几大优势:

       1,支持富媒体消息,表现能力强。比如消费者可以发送位置,图片,订单消息等类型消息。这种类型的富媒体消息,往往很难通过电话描述。

       2,IM沟通是典型的异步沟通方式。对客服坐席来说,使用IM,可以和最多几十个消费者同时沟通,相比电话这种传统的一对一同步沟通方式,效率有极大的提高。与此同时,对于消费者来说,使用IM沟通,更符合手机碎片化使用的特点。

       3,使用IM客服,只要用户不卸载APP,即使用户离开APP,甚至杀死APP,客服也随时可以将消息以推送方式通知到手机。用户绝不会错过任何有价值的消息。

    866d76282385b43a6ca66d5fdfecf4e6.jpg


    示例:国美在线APP通过环信提供的APP内置客服很好的服务了上亿用户。

    1.2.3移动APP内置IM(即时通讯)客服技术选型建议:

    002.jpg


    附录:Gartner研究——移动端客户服务 分析师:迈克尔·毛兹

       定义:移动端客户服务应用存储在智能手机或平板电脑中、也可通过这些设备进行访问。这些应用的的使用可通过上下文搜索、联系上下文信息、客户定位服务或多模式交互(客户可以进行自助服务,也可以通过语音聊天或同步浏览请求、或得到在线人工支持)。其他技术还包括基于语音的搜索、虚拟客户助理以及触摸式或视觉交互式语音响应(IVR)。

       定位和市场接受速度:下载到移动设备上的数亿个移动应用通常缺乏其他渠道中常见的客服支持。它们并非本机自带的移动应用,因此可能无法利用移动平台的所有功能。这种差距将阻碍企业为客户提供丰富且令人满意的移动体验的主动性。因此,尽管移动客户支持尚不成熟,我们仍然认为其将成为IT业界和业务线(LOB)中最重要的优先项目。

       用户建议:与客户体验或客户支持副总合作的IT部门,应根据其在移动设备上的关键活动所获得的支持以及客户对各种功能的需求,来调查客户的满意度水平。根据调查结果来提高人们对移动支持当前状态的认知,创建路线图以改善客户支持和评估CRM供应商的移动应用程序和技术。

       业务影响:优秀的移动客户服务将促使客户使用公司网站时,从笔记本电脑或台式机转向移动设备。消费者对移动设备的偏好正在快速发展,对于一些行业而言,到2019年,移动设备的使用将占到所有互联网交互的85%,如果不能改善移动客户服务,企业将遭受损失。
    好处评级:高

    市场渗透率:目标受众的5%至20%

    成熟度:新兴

    供应商举例:Creative Virtual;甲骨文;Pegasystems;Salesforce ;SAP;TouchCommerce;
    关于《客户世界·洞察者》

    随着信息爆炸的互联网时代的到来,人们阅读习惯的改变,客户世界推出立体化内容呈现方式(图文+短视频)来迎合人们碎片化时间的阅读。那么越来越高标准的阅读需求靠什么来满足?《客户世界》做为国内唯一定位于“客户管理”专业研究及其关联产业发展的专业纸媒,在客户中心行业广受赞誉。其精准的内容定位锁定了企业客户管理各级岗位的专业人士。“客户世界·洞察者”每周一个观点,敬请期待。
      收起阅读 »

    对进入ChatRoom的疑惑

    之前写了个安卓端接入环信的demo,在进入ChatRoom成功后,会送数据库拉取最近的聊天记录逐条推送过来,不过最近将环信接入项目后,起初成功进入ChatRoom后还有消息推送过来,但是这两天突然就没有了推送,只是成功进入了ChatRoom,所以对这块一直有疑...
    继续阅读 »
    之前写了个安卓端接入环信的demo,在进入ChatRoom成功后,会送数据库拉取最近的聊天记录逐条推送过来,不过最近将环信接入项目后,起初成功进入ChatRoom后还有消息推送过来,但是这两天突然就没有了推送,只是成功进入了ChatRoom,所以对这块一直有疑惑。还有顺便问一个问题,文档中说的options.setNumberOfMessagesLoaded方法,怎么一直都找不到? 收起阅读 »

    【环信征文】| 环信的简单接入

    我是直接用的 环信的EaseUI 导入就不多言了 添加了官网让添加的第三方库 然后运行没有报错 下面开始接入 首先是注册和通知//这里的通知只是本地通知 在- (BOOL)application:(UIApplication *)application did...
    继续阅读 »
    我是直接用的 环信的EaseUI 导入就不多言了 添加了官网让添加的第三方库
    然后运行没有报错
    下面开始接入
    首先是注册和通知//这里的通知只是本地通知
    在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中添加
    //注册环信
    EMOptions *options = [EMOptions optionsWithAppkey:@"app 的 key"];
    options.apnsCertName = @"上传的推送证书的名字";
    [[EMClient sharedClient] initializeSDKWithOptions:options];
    options.enableDeliveryAck = YES;
    //添加监听在线推送消息
    [[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];
    (注:要让 AppDelegate遵循EMChatManagerDelegate协议)
    注:环信的通知只有在完全杀死 app 的情况下才会推送 如果只是app 在后台 是不会发推送通知的 所以要监听在线推送消息来发本地推送
    下面来实现监听在线推送消息的方法
    //监听环信在线推送消息
    - (void)messagesDidReceive:(NSArray *)aMessages {
    for (EMMessage *message in aMessages) {
    EMMessageBody *msgBody = message.body;
    switch (msgBody.type) {
    case EMMessageBodyTypeText: {
    // 收到的文字消息
    EMTextMessageBody *textBody = (EMTextMessageBody *)msgBody;
    NSString *txt = textBody.text;
    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    //判断 app 是不是在后台
    if (state == UIApplicationStateBackground) {
    UILocalNotification *localNote = [[UILocalNotification alloc] init];
    //设置通知发出的时间
    localNote.fireDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
    //设置通知的内容
    localNote.alertBody = txt;
    //设置锁屏界面的文字
    localNote.alertAction = txt;
    //设置锁屏界面alertAction是否有效
    localNote.hasAction = YES;
    //设置应用程序图标右上角的数字
    localNote.applicationIconBadgeNumber = 1;
    // 调度通知
    [[UIApplication sharedApplication] scheduleLocalNotification:localNote];
    } else {
    // 如果是在前台,这里可以选择增加角标或者其他提示
    }
    }
    break;
    case EMMessageBodyTypeImage: {
    // 得到一个图片消息body
    EMImageMessageBody *body = ((EMImageMessageBody *)msgBody);
    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    if (state == UIApplicationStateBackground) {
    UILocalNotification *localNote = [[UILocalNotification alloc] init];
    //设置通知发出的时间
    localNote.fireDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
    //设置通知的内容
    localNote.alertBody = @"收到一张图片";
    //设置锁屏界面的文字
    localNote.alertAction = @"收到一张图片";
    //设置锁屏界面alertAction是否有效
    localNote.hasAction = YES;
    //设置应用程序图标右上角的数字
    localNote.applicationIconBadgeNumber = 1;
    //调度通知
    [[UIApplication sharedApplication] scheduleLocalNotification:localNote];
    }
    }
    break;
    case EMMessageBodyTypeLocation: {
    EMLocationMessageBody *body = (EMLocationMessageBody *)msgBody;
    NSLog(@"纬度-- %f",body.latitude);
    NSLog(@"经度-- %f",body.longitude);
    NSLog(@"地址-- %@",body.address);
    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    if (state == UIApplicationStateBackground) {
    UILocalNotification *localNote = [[UILocalNotification alloc] init];
    //设置通知发出的时间
    localNote.fireDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
    //设置通知的内容
    localNote.alertBody = @"收到一条消息";
    //设置锁屏界面的文字
    localNote.alertAction = @"收到一条消息";
    //设置锁屏界面alertAction是否有效
    localNote.hasAction = YES;
    //设置应用程序图标右上角的数字
    localNote.applicationIconBadgeNumber = 1;
    // 调度通知
    [[UIApplication sharedApplication] scheduleLocalNotification:localNote];
    }
    }
    break;
    case EMMessageBodyTypeVoice: {
    // 音频sdk会自动下载
    EMVoiceMessageBody *body = (EMVoiceMessageBody *)msgBody;
    NSLog(@"音频remote路径 -- %@" ,body.remotePath);
    NSLog(@"音频local路径 -- %@" ,body.localPath); // 需要使用sdk提供的下载方法后才会存在(音频会自动调用)
    NSLog(@"音频的secret -- %@" ,body.secretKey);
    NSLog(@"音频文件大小 -- %lld" ,body.fileLength);
    NSLog(@"音频文件的下载状态 -- %u" ,body.downloadStatus);
    NSLog(@"音频的时间长度 -- %u" ,body.duration);
    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    if (state == UIApplicationStateBackground) {
    UILocalNotification *localNote = [[UILocalNotification alloc] init];
    //设置通知发出的时间
    localNote.fireDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
    //设置通知的内容
    localNote.alertBody = @"收到一条消息";
    //设置锁屏界面的文字
    localNote.alertAction = @"收到一条消息";
    //设置锁屏界面alertAction是否有效
    localNote.hasAction = YES;
    //设置应用程序图标右上角的数字
    localNote.applicationIconBadgeNumber = 1;
    //调度通知
    [[UIApplication sharedApplication] scheduleLocalNotification:localNote];
    }
    }
    break;
    case EMMessageBodyTypeVideo: {
    //视频消息
    EMVideoMessageBody *body = (EMVideoMessageBody *)msgBody;
    }
    break;
    case EMMessageBodyTypeFile: {
    // 文件消息
    EMFileMessageBody *body = (EMFileMessageBody *)msgBody;
    }
    break;
    default:
    break;
    }

    }
    }

    这里因为我是没有用到视频和文件的功能,所以这里收到之后并没有做处理

    //当程序关闭后 通过点击推送弹出的通知
    // iOS 10 支持的方法
    - (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler {
    //在这个方法中实现点击消息进入 app 之后的方法
    //我这里是写了一个通知 让 app 进入消息列表界面
    [[NSNotificationCenter defaultCenter] postNotificationName:@"jumpToChatListView" object:nil];
    }

    下面开始聊天
    我这里做的是每次开始聊天都要登陆一下 ,也可以选择在用户登陆 app 的时候 或者在 appdelegate 里面登陆
    EMError *error = [[EMClient sharedClient] loginWithUsername:@"用户名" password:@"密码"];
    if (!error) {
    //我在这里做了一个只要和他说话就添加他为好友 这样在后面获取好友列表的时候就有好友啦

    MYFMRequestBean* bean = [MyHttpRequest addFriendWithUserId:对方 id currntUserId:自己的 id];
    [bean connect:nil success:^(id responseObject) {

    } failure:^(NSError *error) {
    }];

    //这里自定义了一个聊天页面 继承于EaseMessageViewController 定义了两个属性 一个是 talkImg 是对方的头像 一个是 talkName 对方的名字 这个从当前页面就能获取到
    ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:对方的环信 idconversationType:EMConversationTypeChat];
    chatController.talkImg = 对方的头像的 url 地址;
    chatController.talkName = 对方的名字;
    chatController.hidesBottomBarWhenPushed=YES;//跳转时隐藏 tabbar
    [self.navigationController pushViewController:chatController animated:YES];
    } else {
    [SVProgressHUD showErrorWithStatus:@"连接失败,请稍后重试"];

    }

     下面我说一下我自定义的这个ChatViewController
    .h 文件就是
    #import "EaseMessageViewController.h"
    @interface ChatViewController : EaseMessageViewController
    @property (nonatomic, copy)NSString *talkName;
    @property (nonatomic, copy)NSString *talkImg;
    @end

    .m 文件:
    #import "ChatViewController.h"
    //遵循EaseMessageViewController的 DataSource 协议
    @interface ChatViewController ()<EaseMessageViewControllerDataSource>
    @end

    @implementation ChatViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    self.dataSource = self;
    [self.navigationItem setTitle:self.talkName];
    }
    //这里实现EaseMessageViewController的 DataSource 方法 来改变对方和自己的昵称和头像
    - (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController
    modelForMessage:(EMMessage *)message
    {
    //用户可以根据自己的用户体系,根据message设置用户昵称和头像
    id<IMessageModel> model = nil;
    //EaseMessageModel是环信EaseUI提供的model
    model = [[EaseMessageModel alloc] initWithMessage:message];
    //分两种情况 一种是当为当前用户的时候
    if ([model.nickname isEqualToString:[EMClient sharedClient].currentUsername]) {
    //默认图
    // model.avatarImage = [UIImage imageNamed:@"baseInfo"];
    //网络图
    model.avatarURLPath = [UserManager userImg];
    model.nickname = [UserManager userName];
    } else {//当为对方的时候
    model.avatarURLPath = _talkImg;//网络图
    // model.avatarImage = [UIImage imageNamed:@"baseInfo"];//默认图
    model.nickname = _talkName;//用户昵称
    }
    return model;
    }

    下面说一下消息列表
    自定义了一个ChatListViewController 继承于环信的EaseConversationListViewController
    .h文件:
    #import "EaseConversationListViewController.h"
    @interface ChatListViewController : EaseConversationListViewController
    - (void)reloadData;//这里我加了一个刷新方法 如果单击消息进入 app 的时候是在当前页面 要刷新一下,默认是不会刷新的,即使在 viewWillAppear 里面写了也不刷新

    @end
    .m文件:
    #import "ChatListViewController.h"
    #import "HPChatListDataModel.h"
    #import "NSDate+Category.h"//环信时间分类
    #import "ChatViewController.h"
    #import "ChatListDataModel.h"//这是自定义的一个 model 类 存放好友的头像昵称和 id
    #import "EaseUsersListViewController.h"
    @interface ChatListViewController ()<EaseConversationListViewControllerDataSource,EaseConversationListViewControllerDelegate>
    @property (nonatomic, strong)NSMutableArray *imageAndNameArray;
    @end

    @implementation ChatListViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    [self sendFriendsList];
    self.dataSource = self;
    self.delegate = self;
    [self.navigationItem setTitle:@"聊天列表"];
    //打开下来刷新
    self.showRefreshHeader = YES;
    [self tableViewDidTriggerHeaderRefresh];
    }
    #pragma mark - 好友列表
    - (void)sendFriendsList {
    [SVProgressHUD showWithStatus:@"加载中..."];
    MYFMRequestBean* bean = [MyHttpRequest friendsListWithUserId:[UserManager userId]];
    self.imageAndNameArray = [NSMutableArray array];
    __weak typeof(self) weakSelf = self;
    [bean connect:nil success:^(id responseObject) {
    [SVProgressHUD dismiss];
    NSDictionary* dic = responseObject;
    if ([dic optIntKey:@"status"] == 1) {
    NSArray* arr = dic[@"data"];
    for (NSDictionary* dict in arr) {
    HPChatListDataModel *model = [[HPChatListDataModel alloc]initWithDictionary:dict];
    [weakSelf.imageAndNameArray addObject:model];
    }
    [self tableViewDidTriggerHeaderRefresh];
    } else {
    [SVProgressHUD showErrorWithStatus:[dic optStringKey:@"msg"]];
    }
    } failure:^(NSError *error) {
    [SVProgressHUD showErrorWithStatus:@"网络错误"];
    }];
    }

    - (id<IConversationModel>)conversationListViewController:(EaseConversationListViewController *)conversationListViewController
    modelForConversation:(EMConversation *)conversation{
    //用环信提供的model就可以了
    EaseConversationModel *model = [[EaseConversationModel alloc] initWithConversation:conversation];
    //然后根据用户名 往上面赋值
    //self.imageAndNameArray为自定义的数组,其中存储的是从自己服务器上请求下来的数据
    //数据包括,昵称,头像
    for (HPChatListDataModel *dataModel in self.imageAndNameArray) {
    if ([dataModel.mobile isEqualToString:model.conversation.conversationId]) {//根据用户名对应起来
    model.avatarURLPath = dataModel.pic;//头像的网络图片
    model.title = dataModel.name;//昵称
    }
    }
    return model;
    }
    //下拉刷新
    - (void)tableViewDidTriggerHeaderRefresh{
    //super必须要有 要不会有问题
    [super tableViewDidTriggerHeaderRefresh];
    }
    #pragma mark delegate
    - (void)conversationListViewController:(EaseConversationListViewController *)conversationListViewController
    didSelectConversationModel:(id<IConversationModel>)conversationModel{
    EaseConversationModel *model = (EaseConversationModel *)conversationModel;
    //自定义点击cell推出的viewcontroller
    ChatViewController *viewController = [[ChatViewController alloc]initWithConversationChatter:model.conversation.conversationId conversationType:(EMConversationTypeChat)];
    viewController.talkImg = model.avatarURLPath;
    viewController.talkName = model.title;
    [self.navigationController pushViewController:viewController animated:YES];
    }
    - (void)viewWillAppear:(BOOL)animated {
    [self tableViewDidTriggerHeaderRefresh];
    }
    - (void)reloadData {
    [self tableViewDidTriggerHeaderRefresh];
    }


    下面是获取未读消息数量 然后显示小红点
     
    NSArray *conversations = [[EMClient sharedClient].chatManager getAllConversations];
    NSInteger unreadCount = 0;
    for (EMConversation *converstaion in conversations) {
    unreadCount += converstaion.unreadMessagesCount;
    }

    用 forin 的方式来获取数量

    又加了单击聊天的头像然后显示个人信息的功能
    so
    让ChatViewController 遵循EaseMessageCellDelegate
    然后别忘了 self.delegate = self;
    然后实现代理方法
     
    - (void)messageViewController:(EaseMessageViewController *)viewController
    didSelectAvatarMessageModel:(id<IMessageModel>)messageModel {
    //判断 model 的 nickname 是不是本人的 name 这里因为上面修改昵称的时候把这个 nickname 从 id 改成 name 了 所以这里判断的时候要用 name 来判断 不能用 id 了
    if ([messageModel.nickname isEqualToString:@"本人名字"]) {
    //跳转到个人信息
    } else {
    //跳转到对方信息
    }
    }
    收起阅读 »

    我又做了一次面试官

    有一天HR拿着一份简历找我,说有一个10年IT互联网从业经验,其中最近4年Android开发经验的人来面试。当时我被吓到了,不敢一个人去见他,于是扯着刚毕业的应届生小刚给我壮胆。 那个人看上去快40了,嫌会议室档次太低,要去演播室面试;进了演播室之后,他直奔...
    继续阅读 »
    有一天HR拿着一份简历找我,说有一个10年IT互联网从业经验,其中最近4年Android开发经验的人来面试。当时我被吓到了,不敢一个人去见他,于是扯着刚毕业的应届生小刚给我壮胆。

    那个人看上去快40了,嫌会议室档次太低,要去演播室面试;进了演播室之后,他直奔中间给主持人和嘉宾坐的两个沙发去了,剩下的另一个沙发我俩谁也没好意思坐,一人搬个板凳坐在他面前--当时的场景像极了他在面试我俩。

    我看了看他的简历,1983年出生的,从业以年来的经历写的不是“某大型上市公司”的Leader就是“某大型国企”的专家,但都没写具体公司名,技能写的也像很多从不懂技术的HR写的招聘条件上复制粘贴下来的。

    我:(他进来之后应该是我领导,问他点作为Leader该懂的)能简单讲讲敏捷开发吗?
    他:敏捷开发……就是开发时候思路和动作都敏捷点,多加点班,快点把结果交出来。

    我:(感觉他可能是不太擅长管理的技术专家,问点高难度的)能讲讲需要涉及到Android辅助功能的开发,比如自动抢红包的实现思路吗?
    他:不知道什么是“辅助功能”
    我:像微信一样在桌面上生成与某人会话的快捷方式怎么做呀?
    他:没了解过

    我:(感觉他没做过方向性太强的,问点常见的)能讲讲Android事件分发传递机制吗?
    他:Android事件分发传递机制呀?不知道
    我:能讲讲Android动画分哪几大类吗?
    他:Android动画呀?不知道
    我:WebView用什么接口与JavaScript交流呀?
    他:不知道
    我:方法数达到65k以后该怎么办呀?
    他:方法少写点就行了吧
    我:能说说自定义控件需要用到的方法除了OnDraw()和OnLayout()之外另一个是啥吗?
    他:有OnDraw()、OnLayout(),另一个不知道

    我:(似乎明白点啥)你有作品吗?
    他:有,有(说着打开了手机上一个APP)
    (我一看是个简单的新闻客户端,他给我演示了一下Fragment翻页)
    我:知道Fragment的懒加载吗?
    他:不知道
    我:还有别的作品吗?
    他:还有,还有(说着打开了手机上的另一个APP)
    (我一看是个简单的随手记,他给我演示了一下存储文字)
    我:用SharedPerfences保存的?
    他:嗯
    我:知道SharedPerfences的原理吗?
    他:是一个轻量级的数据库
    我:你不是四年经验吗?还做过别的APP吗?
    他:我……我给公司做的APP都是涉密项目

    我:(看他简历上还写着“精通Java”)能说说Java的基本数据类型有哪些吗?比如int和long
    他:int…?long…?还有String吧
    我:你确定String也是基本数据类型?
    他:挺常用的,应该是吧

    我:(看他简历上还写着“精通软件工程,精通面向对象,精通设计模式”)能说说面向对象三大特征除了封装和多态之外还有啥吗?
    他:封装…?多态…?还有啥我还真不知道

    我:(已经确定他是嫌站着工作累去培训班学了几个月的厨师或者洗剪吹了,简历上写的“精通算法”也没必要问了)小刚你问他点问题吧,我有点事先回去了

    (后来的对话是小刚告诉我的)
    小刚:你知道Android四大组件是啥吗?
    他:有个Activity吧?
    小刚:能说说Activity生命周期吗?
    他:(终于有个知道的了)这个我知道,Activity刚打开的时候调用onCreate(),关闭的时候用finish(),从上一个Activity退回来调用onResume()(也没说对)
    小刚:那你期望薪资是多少呀?
    他:(瞬间来了精神)必须不低于你俩之和!

    如果这个比相声还精彩的面试情景被录下来的话对公司的收视率还是很有帮助的,可惜当天视频部门没开摄像机。 收起阅读 »

    环信Android/ios V3.3.1 SDK 已发布,支持token登录,红包集成更快捷!

    Android ​V3.3.1 2017-04-07   新功能: 新增:使用token登录接口新增:群组群成员进出群组回调 优化: Demo中红包集成方式更改为aar,默认支持支付宝渠道支付 修复 之前EMChatManager.getMessage对应...
    继续阅读 »
    Android ​V3.3.1 2017-04-07
     
    新功能:
    1. 新增:使用token登录接口
    2. 新增:群组群成员进出群组回调


    优化:
    1. Demo中红包集成方式更改为aar,默认支持支付宝渠道支付


    修复
    1. 之前EMChatManager.getMessage对应的消息会保存在缓存中,修改后不缓存getMessage产生的消息。之前的代码会导致loadMoreMessage部分消息不显示。
    2. 3.3.0版本Demo中群组@键,弹出列表没有包含群组管理员
    3. 3.3.0版本EMGroup.getMuteList会崩溃
    4. 3.3.0版本EMChatRoom hash code错误
    5. 修复音视频被叫时多个应用都会收到通知的错误

     
    iOS V3.3.1 2017-04-07新功能:
    1. 新增:使用token登录
    2. 新增:群组群成员进出群组回调


    优化:
    1. 红包改用cocoapods方式集成,支持支付宝和京东支付


    修复:
    1. insertMessage小概率下会崩溃
    2. [EMMessage setTo:]赋值错误
    3. 聊天室获取详情接口[IEMChatroomManager fetchChatroomInfo:includeMembersList:error:]第2个参数传入YES时不能获取成员
    4. 2.x和3.x互通情况下,群组和聊天室的memberlist中出现admin和owner
    5. 发送消息成功后,对应的EMConversation没有更新最后一条消息

     
    版本历史:AndroidSDK 更新日志  ios SDK更新日志
    下载地址:SDK下载
    收起阅读 »

    环信公开课第11期●5分钟集成环信移动客服+环信智能机器人全解析

    APP、网页等多渠道如何快速接入智能云客服? 客服机器人号称能解决80%的问题,究竟是确有其事还是言过其实? 如何高效的生成一份报表让客服绩效一目了然! 环信公开课第11期(2017.4.20 19:00)●5分钟集成环信移动客服+环信智能机器人全解析 ...
    继续阅读 »

    a9a1178518f9eb962418377ca0050143.gif


    APP、网页等多渠道如何快速接入智能云客服?

    客服机器人号称能解决80%的问题,究竟是确有其事还是言过其实?

    如何高效的生成一份报表让客服绩效一目了然!

    环信公开课第11期(2017.4.20 19:00)●5分钟集成环信移动客服+环信智能机器人全解析
     
    环信公开课 讲师简介


    0bc0c69e06763b574510b760d2f137edT.gif


    小双mm
    疑难投诉处理专家,环信首席程序猿鼓励师
     
    身高165,温柔体贴,善解人意,会做饭会洗衣,会遛狗会铲屎,英雄联盟一区钻一,会花会活不粘人!
     
    环信公开课 活动看点

    5分钟集成环信移动客服+环信智能机器人全解析(2017.4.20 19:00)
     


    ☞ 教您5分钟快速集成环信移动客服
     
    ☞ 我们怎样才能将环信智能机器人用在刀刃上!

    ☞ 如何高效的生成一份报表让客服绩效一目了然!

    ☞ 在线问答


    环信公开课 活动说明


    主讲嘉宾:环信颜值担当小双mm

    参会时间:2017.4.20(周四)19:00

    活动形式:线上公开课

    注意事项:联网手机|电脑均可观看


    环信公开课 参会两步走
     
    Step1:在下方填写准确的报名信息
    http://mk.meeket.com/flyer/978654/157834.html?source=16


    Step2:添加“环信公开课小助手”,小助手拉您进公开课专用微信群,等待开讲。


    方法①:长按下方二维码快速添加

    方法②:直接添加微信号:huanxin-hh


    15a3ca3d987b4573f1f678f7e0bec08aT.gif




    QQ图片20170412154043.jpg

    环信成立于2013年4月,是一家国内领先的企业级软件服务提供商,于2016年荣膺“Gartner 2016 Cool Vendor”。产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。
    收起阅读 »

    环信移动客服v5.15发布——统计数据增加客服的在线时长

    客服模式 统计数据增加客服的在线时长 客服可以查看自己的在线时长数据,包括空闲、忙碌、离开、隐身、在线、离线的时长和占比。 进入客服模式的统计数据页面,选择时间段,查看自己的在线状态分布。  留言支持按创建时间排序 留言支...
    继续阅读 »
    客服模式

    统计数据增加客服的在线时长

    客服可以查看自己的在线时长数据,包括空闲、忙碌、离开、隐身、在线、离线的时长和占比。

    进入客服模式的统计数据页面,选择时间段,查看自己的在线状态分布。 

    001.png


    留言支持按创建时间排序

    留言支持按创建时间排序,默认为倒序排列,即最新的留言排在前面。可以点击“创建时间”右侧的排序按钮,切换正序/倒序排列方式。 

    002.png


    管理员模式

    自定义报表


    移动客服系统支持自定义报表功能。系统提供90天内的统计数据,管理员可以根据不同的时间段、指标项目和指标维度自由搭配出不同的报表,满足多样化的报表需求。

    自定义报表功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。旗舰版客户可以直接使用自定义报表功能。 

    003.png


    添加自定义报表

    在“管理员模式 > 统计查询 > 自定义报表”页面,点击“添加自定义报表”按钮,填写报表名称,选择时间段、报表类型、指标项目、指标维度,并保存。

    每份自定义报表最多可以展示90天内、10个指标项目、2个指标维度的数据。自定义报表会根据您设置的指标实时更新,显示最新的数据。

    指标项目包括两类:

    • 按会话创建时间计算:排队时长平均值、排队时长最大值、排队总次数、独立访客总数;

    • 按会话接起时间计算:质检评分平均值、满意度评分平均值、会话总数、消息总数、会话时长平均值、会话时长最大值、首次响应时长平均值、首次响应时长最大值、响应时长平均值、响应时长最大值。



    指标维度包括:客服、访客标签、渠道类型、关联、会话类型、会话有效类型、时间粒度。 

    004.png

    查看自定义报表

    在“管理员模式 > 统计查询 > 自定义报表”页面,自定义报表以缩略图的形式展示。点击任意报表的内容区域,可以展开该报表,查询更详细的报表内容。

    在展开的报表中,您还可以重新设置报表的时间范围,以及对已有的指标项目和指标维度进行筛选。 

    005.png

    支持调整最大接待人数上限

    目前客服的最大接待人数上限为100,可以设置为0~100之间的数值。当客服的进行中会话数小于最大接待人数时,系统会自动为该客服分配会话。

    如果该最大接待人数上限不能满足您的业务需求,移动客服系统支持将最大接待人数上限调整为200。

    调整最大接待人数上限为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    支持设置历史会话数据的查看权限

    支持为自定义角色设置查看全部或所在技能组的历史会话数据的权限,更好地进行技能组的管理。

    进入“管理员模式 > 设置 > 权限管理”页面,点击自定义角色,编辑该角色的权限。在管理员模式下,勾选“历史会话(全部)”或“历史会话(技能组)”,并保存。

    说明:

    • 仅勾选“历史会话(技能组)”时,该角色可以查看其所在的所有技能组的历史会话;若该角色不属于任何技能组,则可以查看“未分组”的历史会话。

    • 若同时选择“历史会话(全部)”和“历史会话(技能组)”,使用该角色的坐席在管理员模式可以查看全部历史会话。




    006.png


    Android客服工作台

    当前版本:V2.8

    新增消息撤回功能

    新增消息撤回功能。客服使用Android客服工作台与APP、网页渠道的客户聊天时,可以撤回2分钟内的聊天消息。聊天消息被撤回后,将在APP、网页访客端消失。

    注:暂时只有最新版web插件和最新版Android SDK支持消息撤回。

    消息撤回功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    新增锁屏接收消息功能

    手机锁屏时,依然可以通过Android客服工作台收到客户的消息。

    关于更多Android客服工作台的更新日志,请查看Android 客服工作台 更新日志。

    移动客服iOS SDK

    当前版本:V1.0.1

    支持实时语音、实时视频

    移动客服iOS SDK支持实时语音、实时视频(实时音视频)。当客户使用iOS APP联系客服时,可以向客服发起视频聊天。

    实时音视频功能需要调用iOS SDK的接口进行集成,集成方式可参考“商城”demo。

    注:需要在网页端客服工作台开通“实时视频”这项增值服务后,才能向客服发起视频聊天。

    支持发送位置消息

    移动客服iOS SDK支持发送地理位置消息。

    关于移动客服iOS SDK的集成说明,请查看移动客服 iOS SDK 集成。 
     
    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.15

     
    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

    一封来自环信小伙伴“小爱爱”的表扬信,同时还邮寄了大量情趣礼品

          表扬信热情洋溢感人至深,大量使用了兢兢业业、不辞劳苦等成语,淋漓尽致体现出了环信以客户成功为己任,对客户那种发自肺腑最深沉的爱,表现了环信员工...       深圳市小爱爱科技有限公司成立于2014年8月,是一家专业研究智能情趣用品的高...
    继续阅读 »

    ea790d9dly1fehg4r6ncrj21411hcwq8.jpg


       
     

    ea790d9dly1fehg4snja7j21hc141wkp.jpg


    表扬信热情洋溢感人至深,大量使用了兢兢业业、不辞劳苦等成语,淋漓尽致体现出了环信以客户成功为己任,对客户那种发自肺腑最深沉的爱,表现了环信员工... 
     
       深圳市小爱爱科技有限公司成立于2014年8月,是一家专业研究智能情趣用品的高科技公司,创始人有多年健康产品行业及电商经验,联合创始人和团队均来华为、腾讯等公司。团队目前50多人,核心团队主要分为产品研发、设计以及生产等。同时继承了多年研发、生产及销售男性女性生理健康、美容用品的行业经验,结合并融入当今先进的工业设计,人体工程学及通讯技术,目前在智能健康设备领域取得了不错的业绩。 收起阅读 »

    使用环信3.xSDK 在 TV 端集成音视频通话功能

    使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上   项目git源码https://github.com/lzan13/VMChatDemoCall   VMTVCall 使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装...
    继续阅读 »
    使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上
     
    项目git源码https://github.com/lzan13/VMChatDemoCall
     
    VMTVCall

    使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上,让爸妈在家和自己进行高清通话

    使用版本
    需要注意的是,这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下实现功能
    • 项目首次启动自动注册登录
    • 拨号盘实现
    • 历史通话记录 TODO
    • 视频通话功能(因为电视不需要语音通话以及最小化)
    • 视频通话的录制
    • 通话截图


    其他相关项目

    这也实现了一个移动端的音视频小项目,使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能

    移动端项目【移动端实现音视频通话项目

    项目截图

    首界面 

    001.jpg


    通话界面 

    002.jpg


      收起阅读 »

    使用环信3.xSDK 集成音视频通话功能

        使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能   ...
    继续阅读 »
        使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能
     
    项目源码git地址https://github.com/lzan13/VMLibraryManager
     
    使用版本


    PS:这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下 PS:必须使用环信SDK3.3.0以后的版本

    实现功能

    • 通话界面最小化及恢复

    • 通话悬浮窗的实现,可拖动

    • 视频通话界面切换

    • 视频通话的录制

    • 视频通话的截图

    • 横竖屏的自动切换



    已知问题

    • 未接通时切换到悬浮窗,当接通时无法显示画面

    • 主叫方接通时无法显示远程图像



    项目截图

    001.png



    002.png




    003.png




    004.png




    005.png


    关联项目

    实现有一个 TV 端的应用,可以实现和移动端进行实时通话,给大家在 TV 端使用环信 SDK 进行集成音视频通话加以参考
    TV 端视频通话项目
     
      收起阅读 »

    李理:Theano tutorial和卷积神经网络的Theano实现 Part2

    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第9篇。 作者:李理 ...
    继续阅读 »
    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第9篇。

    作者:李理 
    目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
     
    相关文章: 
    李理:从Image Caption Generation理解深度学习(part I)
    李理:从Image Caption Generation理解深度学习(part II)
    李理:从Image Caption Generation理解深度学习(part III)
    李理:自动梯度求解 反向传播算法的另外一种视角
    李理:自动梯度求解——cs231n的notes
    李理:自动梯度求解——使用自动求导实现多层神经网络
    李理:详解卷积神经网络
    李理:Theano tutorial和卷积神经网络的Theano实现 Part1
     
    上文
    7. 使用Theano实现CNN

    接下来我们继续上文,阅读代码network3.py,了解怎么用Theano实现CNN。

    完整的代码参考这里

    7.1 FullyConnectedLayer类

    首先我们看怎么用Theano实现全连接的层。
    class FullyConnectedLayer(object):

    def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
    self.n_in = n_in
    self.n_out = n_out
    self.activation_fn = activation_fn
    self.p_dropout = p_dropout
    # Initialize weights and biases
    self.w = theano.shared(
    np.asarray(
    np.random.normal(
    loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
    dtype=theano.config.floatX),
    name='w', borrow=True)
    self.b = theano.shared(
    np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
    dtype=theano.config.floatX),
    name='b', borrow=True)
    self.params = [self.w, self.b]

    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
    self.inpt = inpt.reshape((mini_batch_size, self.n_in))
    self.output = self.activation_fn(
    (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
    self.y_out = T.argmax(self.output, axis=1)
    self.inpt_dropout = dropout_layer(
    inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
    self.output_dropout = self.activation_fn(
    T.dot(self.inpt_dropout, self.w) + self.b)

    def accuracy(self, y):
    "Return the accuracy for the mini-batch."
    return T.mean(T.eq(y, self.y_out))
    7.1.1 init
    FullyConnectedLayer类的构造函数主要是定义共享变量w和b,并且随机初始化。参数的初始化非常重要,会影响模型的收敛速度甚至是否能收敛。这里把w和b初始化成均值0,标准差为sqrt(1.0/n_out)的随机值。有兴趣的读者可以参考这里

    此外,这里使用了np.asarray函数。我们用np.random.normal生成了(n_in, n_out)的ndarray,但是这个ndarray的dtype是float64,但是我们为了让它(可能)在GPU上运算,需要用theano.config.floatX,所以用了np.asarray函数。这个函数和np.array不同的一点是它会尽量重用传入的空间而不是深度拷贝。

    另外也会把激活函数activation_fn和dropout保存到self里。activation_fn是一个函数,可能使用静态语言习惯的读者不太习惯,其实可以理解为c语言的函数指针或者函数式变成语言的lambda之类的东西。此外,init函数也把参数保存到self.params里边,这样的好处是之后把很多Layer拼成一个大的Network时所有的参数很容易通过遍历每一层的params就行。

    7.1.2 set_input

    set_inpt函数用来设置这一层的输入并且计算输出。这里使用了变量名为inpt而不是input的原因是input是Python的一个内置函数,容易混淆。注意我们通过两种方式设置输入:self.inpt和self.inpt_dropout。这样做的原因是我们训练的时候需要dropout。我们使用了一层dropout_layer,它会随机的把dropout比例的神经元的输出设置成0。而测试的时候我们就不需要这个dropout_layer了,但是要记得把输出乘以(1-dropout),因为我们训练的时候随机的丢弃了dropout个神经元,测试的时候没有丢弃,那么输出就会把训练的时候大,所以要乘以(1-dropout),模拟丢弃的效果。【当然还有一种dropout的方式是训练是把输出除以(1-dropout),这样预测的时候就不用在乘以(1-dropout)了, 感兴趣的读者可以参考这里
    def set_inpt(self, inpt, inpt_dropout, mini_batch_size): 
    self.inpt = inpt.reshape((mini_batch_size, self.n_in))
    self.output = self.activation_fn( (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
    self.y_out = T.argmax(self.output, axis=1)
    self.inpt_dropout = dropout_layer(inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
    self.output_dropout = self.activation_fn( T.dot(self.inpt_dropout, self.w) + self.b)
    下面我们逐行解读。
    1.reshape inpt 
       首先把input reshape成(batch_size, n_in),为什么要reshape呢?因为我们在CNN里通常在最后一个卷积pooling层后加一个全连接层,而CNN的输出是4维的tensor(batch_size, num_filter, width, height),我们需要把它reshape成(batch_size, num_filter * width * height)。当然我们定义网络的时候就会指定n_in=num_filter width height了。否则就不对了。

    2.定义output 
       然后我们定义self.output。这是一个仿射变换,然后要乘以(1-p_dropout),原因前面解释过了。这是预测的时候用的输入和输出。【有点读者可能会疑惑(包括我自己第一次阅读时),调用这个函数时会同时传入inpt和inpt_dropout吗?我们在Theano里只是”定义“符号变量从而定义这个计算图,所以不是真的计算。我们训练的时候定义用的是cost损失函数,它用的是inpt_dropout和output_dropout,而test的Theano函数是accuracy,用的是inpt和output以及y_out。

    3.定义y_out 
       这个计算最终的输出,也就是当这一层作为最后一层的时候输出的分类结果。ConvPoolLayer是没有实现y_out的计算的,因为我们不会把卷积作为网络的输出层,但是全连接层是有可能作为输出的,所以通过argmax来选择最大的那一个作为输出。SoftmaxLayer是经常作为输出的,所以也实现了y_out。

    4.inpt_dropout 先reshape,然后加一个dropout的op,这个op就是随机的把一些神经元的输出设置成0
    def dropout_layer(layer, p_dropout): 
    srng = shared_randomstreams.RandomStreams(np.random.RandomState(0).randint(999999))
    mask = srng.binomial(n=1, p=1-p_dropout, size=layer.shape)
    return layer*T.cast(mask, theano.config.floatX)
    5.定义output_dropout 
       直接计算
     
    ConvPoolLayer和SoftmaxLayer的代码是类似的,这里就不赘述了。下面会有network3.py的完整代码,感兴趣的读者可以自行阅读。

    但是也有一些细节值得注意。对于ConvPoolLayer和SoftmaxLayer,我们需要根据对应的公式计算输出。不过非常幸运,Theano提供了内置的op,如卷积,max-pooling,softmax函数等等。

    当我们实现softmax层时,我们没有讨论怎么初始化weights和biases。之前我们讨论过sigmoid层怎么初始化参数,但是那些方法不见得就适合softmax层。这里直接初始化成0了。这看起来很随意,不过在实践中发现没有太大问题。
     
    7.2 ConvPoolLayer类
    7.2.1 init
        def __init__(self, filter_shape, image_shape, poolsize=(2, 2),
    activation_fn=sigmoid):
    self.filter_shape = filter_shape
    self.image_shape = image_shape
    self.poolsize = poolsize
    self.activation_fn=activation_fn
    # initialize weights and biases
    n_out = (filter_shape[0]*np.prod(filter_shape[2:])/np.prod(poolsize))
    self.w = theano.shared(
    np.asarray(
    np.random.normal(loc=0, scale=np.sqrt(1.0/n_out), size=filter_shape),
    dtype=theano.config.floatX),
    borrow=True)
    self.b = theano.shared(
    np.asarray(
    np.random.normal(loc=0, scale=1.0, size=(filter_shape[0],)),
    dtype=theano.config.floatX),
    borrow=True)
    self.params = [self.w, self.b]
    首先是参数。
    1.filter_shape (num_filter, input_feature_map, filter_width, filter_height) 
       这个参数是filter的参数,第一个是这一层的filter的个数,第二个是输入特征映射的个数,第三个是filter的width,第四个是filter的height
     
    2.image_shape(mini_batch, input_feature_map, width, height) 
       输入图像的参数,第一个是mini_batch大小,第二个是输入特征映射个数,必须要和filter_shape的第二个参数一样!第三个是输入图像的width,第四个是height

    3.poolsize 
       pooling的width和height,默认2*2

    4.activation_fn 
       激活函数,默认是sigmoid

    代码除了保存这些参数之外就是定义共享变量w和b,然后保存到self.params里。

    7.2.2 set_inpt
     def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
    self.inpt = inpt.reshape(self.image_shape)
    conv_out = conv.conv2d(
    input=self.inpt, filters=self.w, filter_shape=self.filter_shape,
    image_shape=self.image_shape)
    pooled_out = downsample.max_pool_2d(
    input=conv_out, ds=self.poolsize, ignore_border=True)
    self.output = self.activation_fn(
    pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))
    self.output_dropout = self.output # no dropout in the convolutional layers
    我们逐行解读
    1.reshape输入

    2.卷积 
      使用theano提供的conv2d op计算卷积

    3.max-pooling 
      使用theano提供的max_pool_2d定义pooled_out

    4.应用激活函数 
       值得注意的是dimshuffle函数,pooled_out是(batch_size, num_filter, out_width, out_height),b是num_filter的向量。我们需要通过broadcasting让所有的pooled_out都加上一个bias,所以我们需要用dimshuffle函数把b变成(1,num_filter, 1, 1)的tensor。dimshuffle的参数’x’表示增加一个维度,数字0表示原来这个tensor的第0维。 dimshuffle(‘x’, 0, ‘x’, ‘x’))的意思就是在原来这个vector的前面插入一个维度,后面插入两个维度,所以变成了(1,num_filter, 1, 1)的tensor。

    5.output_dropout 
      卷积层没有dropout,所以output和output_dropout是同一个符号变量

    7.3 Network类
    7.3.1 init
     def __init__(self, layers, mini_batch_size):
    self.layers = layers
    self.mini_batch_size = mini_batch_size
    self.params = [param for layer in self.layers for param in layer.params]
    self.x = T.matrix("x")
    self.y = T.ivector("y")
    init_layer = self.layers[0]
    init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
    for j in xrange(1, len(self.layers)):
    prev_layer, layer = self.layers[j-1], self.layers[j]
    layer.set_inpt(
    prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
    self.output = self.layers[-1].output
    self.output_dropout = self.layers[-1].output_dropout
    参数layers就是网络的所有Layers。

    比如下面的代码定义了一个三层的网络,一个卷积pooling层,一个全连接层和一个softmax输出层,输入大小是mini_batch_size 1 28 28的MNIST图片,卷积层的输出是mini_batch_size 20 24 24,pooling之后是mini_batch_size 20 12 12。然后接一个全连接层,全连接层的输入就是pooling的输出20 12*12,输出是100。最后是一个softmax,输入是100,输出10。
    net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=20*12*12, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    首先是保存layers和mini_batch_size

    self.params=[param for layer in …]这行代码把所有层的参数放到一个list里。Network.SGD方法会使用self.params来更新所以的参数。self.x=T.matrix(“x”)和self.y=T.ivector(“y”)定义Theano符号变量x和y。这代表整个网络的输入和输出。

    首先我们调用init_layer的set_inpt
     init_layer = self.layers[0]
    init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
    这里调用第一层的set_inpt函数。传入的inpt和inpt_dropout都是self.x,因为不论是训练还是测试,第一层的都是x。

    然后从第二层开始:
     for j in xrange(1, len(self.layers)):
    prev_layer, layer = self.layers[j-1], self.layers[j]
    layer.set_inpt(
    prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
    拿到上一层prev_layer和当前层layer,然后把调用layer.set_inpt函数,把上一层的output和output_dropout作为当前层的inpt和inpt_dropout。

    最后定义整个网络的output和output_dropout`
     self.output = self.layers[-1].output
    self.output_dropout = self.layers[-1].output_dropout
    7.3.2 SGD函数
       def SGD(self, training_data, epochs, mini_batch_size, eta,
    validation_data, test_data, lmbda=0.0):
    """Train the network using mini-batch stochastic gradient descent."""
    training_x, training_y = training_data
    validation_x, validation_y = validation_data
    test_x, test_y = test_data

    # compute number of minibatches for training, validation and testing
    num_training_batches = size(training_data)/mini_batch_size
    num_validation_batches = size(validation_data)/mini_batch_size
    num_test_batches = size(test_data)/mini_batch_size

    # define the (regularized) cost function, symbolic gradients, and updates
    l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
    cost = self.layers[-1].cost(self)+\
    0.5*lmbda*l2_norm_squared/num_training_batches
    grads = T.grad(cost, self.params)
    updates = [(param, param-eta*grad)
    for param, grad in zip(self.params, grads)]

    # define functions to train a mini-batch, and to compute the
    # accuracy in validation and test mini-batches.
    i = T.lscalar() # mini-batch index
    train_mb = theano.function(
    , cost, updates=updates,
    givens={
    self.x:
    training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    validate_mb_accuracy = theano.function(
    , self.layers[-1].accuracy(self.y),
    givens={
    self.x:
    validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    test_mb_accuracy = theano.function(
    , self.layers[-1].accuracy(self.y),
    givens={
    self.x:
    test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    self.test_mb_predictions = theano.function(
    , self.layers[-1].y_out,
    givens={
    self.x:
    test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    # Do the actual training
    best_validation_accuracy = 0.0
    for epoch in xrange(epochs):
    for minibatch_index in xrange(num_training_batches):
    iteration = num_training_batches*epoch+minibatch_index
    if iteration % 1000 == 0:
    print("Training mini-batch number {0}".format(iteration))
    cost_ij = train_mb(minibatch_index)
    if (iteration+1) % num_training_batches == 0:
    validation_accuracy = np.mean(
    [validate_mb_accuracy(j) for j in xrange(num_validation_batches)])
    print("Epoch {0}: validation accuracy {1:.2%}".format(
    epoch, validation_accuracy))
    if validation_accuracy >= best_validation_accuracy:
    print("This is the best validation accuracy to date.")
    best_validation_accuracy = validation_accuracy
    best_iteration = iteration
    if test_data:
    test_accuracy = np.mean(
    [test_mb_accuracy(j) for j in xrange(num_test_batches)])
    print('The corresponding test accuracy is {0:.2%}'.format(
    test_accuracy))
    print("Finished training network.")
    print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(
    best_validation_accuracy, best_iteration))
    print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))
    有了之前theano的基础和实现过LogisticRegression,阅读SGD应该比较轻松了。 
    虽然看起来代码比较多,但是其实逻辑很清楚和简单,我们下面简单的解读一下。

    1. 定义损失函数cost​
      l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
    cost = self.layers[-1].cost(self)+\
    0.5*lmbda*l2_norm_squared/num_training_batches
    出来最后一层的cost,我们还需要加上L2的normalization,其实就是把所有的w平方和然后开方。注意 self.layers[-1].cost(self),传入的参数是Network对象【函数cost的第一个参数self是对象指针,不要调用者传入的,这里把Network对象自己(self)作为参数传给了cost函数的net参数】。

    下面是SoftmaxLayer的cost函数:
      def cost(self, net):
    "Return the log-likelihood cost."
    return -T.mean(T.log(self.output_dropout)[T.arange(net.y.shape[0]), net.y])
    其实net只用到了net.y,我们也可以把cost定义如下:
     def cost(self, y):
    "Return the log-likelihood cost."
    return -T.mean(T.log(self.output_dropout)[T.arange(y.shape[0]), y])
    然后调用的时候用
     cost = self.layers[-1].cost(self.y)+\
    0.5*lmbda*l2_norm_squared/num_training_batches
    我个人觉得这样更清楚。

    2. 定义梯度和updates​
       grads = T.grad(cost, self.params)
    updates = [(param, param-eta*grad)
    for param, grad in zip(self.params, grads)]
    3. 定义训练函数​
     i = T.lscalar() # mini-batch index
    train_mb = theano.function(
    , cost, updates=updates,
    givens={
    self.x:
    training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    train_mb函数的输入是i,输出是cost,batch的x和y通过givens制定,这和之前的Theano tutorial里的LogisticRegression一样的。cost函数用到的是最后一层的output_dropout,从而每一层都是走计算图的inpt_dropout->output_dropout路径。

    4. 定义validation和测试函数​
         validate_mb_accuracy = theano.function(
    , self.layers[-1].accuracy(self.y),
    givens={
    self.x:
    validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    test_mb_accuracy = theano.function(
    , self.layers[-1].accuracy(self.y),
    givens={
    self.x:
    test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
    self.y:
    test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    输出是最后一层的accuracy self.layers[-1].accuracy(self.y)。accuracy使用的是最后一层的output,从而每一层都是用计算图的inpt->output路径。

    5. 预测函数​
      self.test_mb_predictions = theano.function(
    , self.layers[-1].y_out,
    givens={
    self.x:
    test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
    })
    输出是最后一层的y_out,也就是softmax的argmax(output)

    7.4 用法​
    training_data, validation_data, test_data = network3.load_data_shared()
    mini_batch_size = 10
    net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=20*12*12, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data)
    至此,我们介绍了Theano的基础知识以及怎么用Theano实现CNN。下一讲将会介绍怎么自己用Python(numpy)实现CNN并且介绍实现的一些细节和性能优化,大部分内容来自CS231N的slides和作业assignment2,敬请关注。 收起阅读 »