注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

客服云网页下班留言配置

                                                                              网页下班留言配置 网页渠道:如果下班时间的路由规则是指定的技能组,可以从网页插件页面配置 下班时间让访...
继续阅读 »
                                                                              网页下班留言配置
网页渠道:如果下班时间的路由规则是指定的技能组,可以从网页插件页面配置 下班时间让访客直接进入留言界面,这样下班时间访客无法发起会话,只能提交留言(注意:网页端访客暂时无法收到客服和管理员对留言的评论,需要通过电话或邮箱联系访客。 )
如需修改,客服系统 切换到管理员模式--渠道管理--网站 --- 功能设置页签,找到 下班留言配置:可修改为 展示默认消息或进入聊天窗,最后保存。访客端需重新加载页面


26-1网页下班留言配置.png


  收起阅读 »

客服云 访客发送的消息客服系统看不到排查

                              访客发送的消息客服系统看不到 1. 先到 管理员模式--搜索  点击消息按钮,输入访客发送的消息内容 查询看下,默认是搜索的近7天的消息,如果时间不在7天内,可以重新点击筛选排序,放大时间范围筛选 。...
继续阅读 »
                              访客发送的消息客服系统看不到
1. 先到 管理员模式--搜索  点击消息按钮,输入访客发送的消息内容 查询看下,默认是搜索的近7天的消息,如果时间不在7天内,可以重新点击筛选排序,放大时间范围筛选 。若可以查询到,点击会话后查看是哪一个客服接入的

22-1访客发送的消息客服系统看不到.png


如果直接无法看到是哪一个客服接入的,复制会话ID 到历史会话 ID一栏,查询出具体会话,点击进入会话详情页,看右侧记录一栏的数据,可以看出是哪一个客服接入的没有回复造成的

22-2访客发送的消息客服系统看不到.png



注:如果是该客服没有上线造成的没有回复消息。需要客服前一天退出登录前手动关闭自己进行中所有会话,这样才能保证新会话能自动调度给上线的客服
也可以通过配置 不活跃会话超时结束 或 访客超时未回复结束会话实现关闭会话(管理员模式--设置--系统开关 页面
2.排查访客是否被加入黑名单
管理员模式---客户中心--客户信息 黑名单列表可以查看。
黑名单中的客户可以再次发送消息,但系统不会为其创建会话;客户被移除黑名单后,再次发送消息时可以成功创建会话。 
如果用户是在黑名单中,可以先移除黑名单后再测试。

22-3访客发送的消息客服系统看不到.png



3. 看 是否达到了待接入上限
   待接入上限:每个租户如果坐席数量 小于等于5,允许的最大待接入会话数为1000,如果某个租户下坐席数超过5个,则该租户的最大待接入会话数为坐席数x200。
  待接入会话数超过上限后,不允许访客创建新的会话。
   管理员模式--待接入 模块可以查看排队的会话量
   

待接入.png


 
如果仍无法解决,可以工作时间登录客服系统,切到管理员模式 --右上角 技术支持--联系客服 发起会话咨询。 收起阅读 »

客服云查看会话满意度评价

                                                                          查看会话的满意度评价 两种情况:查看具体某一个会话的评分或者是查看系统对所有会话评分的统计结果 1》查看具体...
继续阅读 »
                                                                          查看会话的满意度评价
两种情况:查看具体某一个会话的评分或者是查看系统对所有会话评分的统计结果
1》查看具体某一个会话的评分
结束的会话才能到历史会话看评分。先把会话关闭掉,之后可以到管理员模式---历史会话模块,列表直接有评分一列的数据。如果列表不显示关闭的会话,可以点击右上角筛选排序,重新指定条件筛选出会话(可选择按会话结束时间筛选)

查看评价19-1.png


或者点击一个会话,进入会话详情页,看右侧指标一栏(如下图)

查看评价19-2.png



2》查看系统对会话的评价总统计结果
管理员模式--会话统计--工作质量 页面,页面顶部的满意度评分是系统的会话评价统计,页面底部是对每一个客服服务的评分统计(见下图)

查看评价19-3.png




查看评价19-4.png


  收起阅读 »

客服云 客服管理操作

1》添加客服 管理员模式--成员管理--客服 右上角 添加客服按钮 添加时可以设置 客服账号邮箱密码、昵称、最大接待人数、权限等信息。如果需要重置坐席密码也可在此页面操作。 2》修改客服状态 管理员可以修改客服或其他管理员的在线状态,支持在...
继续阅读 »
1》添加客服
管理员模式--成员管理--客服 右上角 添加客服按钮
添加时可以设置 客服账号邮箱密码、昵称、最大接待人数、权限等信息。如果需要重置坐席密码也可在此页面操作。

客服操作17-1.png


2》修改客服状态
管理员可以修改客服或其他管理员的在线状态,支持在空闲、忙碌、隐身、离开状态之间切换。在客服列表中点击“在线状态”一列的下拉按钮,修改客服的在线状态。
注:若客服离线,则不可强行修改其状态为在线。暂不支持自定义角色修改管理员或客服的在线状态。

客服操作17-2.png


 
3》设置客服最大接待人数
最大接待人数可以由管理员统一操作也可以由每一个客服自行调整自己的数量(二者只能选其一)
 ----1---统一由管理员调整
管理员模式--设置--系统开关 客服自定义最大接待人数 需要关闭。之后管理员在 成员管理--客服 页面点击编辑按钮,弹窗后可以修改客服的最大接待人数 ,最后保存

设置客服最大接待人数18-1.png



----2---每一个客服自行修改自己的最大接待人数
管理员模式--设置--系统开关 客服自定义最大接待人数 需要开启。修改后客服模式 最大接待人数为可调状态

设置客服最大接待人数18-2.png


 
客服更多配置操作参见以下文档:
http://docs.easemob.com/cs/200admin/20teammgm#%E5%AE%A2%E6%9C%8D%E7%AE%A1%E7%90%86 收起阅读 »

客服云配置系统自动结束会话

                                                                   配置系统自动结束会话 注意:修改配置后针对新创建的会话生效,旧会话还是需要手动关闭 1》机器人名下的会话自动结束配置: ...
继续阅读 »
                                                                   配置系统自动结束会话
注意:修改配置后针对新创建的会话生效,旧会话还是需要手动关闭
1》机器人名下的会话自动结束配置:
        管理员模式---智能机器人 会话超时设置页签 
可以设置超时时间和超时提示语
可以设置超时时间(1~30分钟)和1~5条超时回复。当访客超过设定时间仍没有回复消息时,机器人将随机从超时回复中选择1条回复访客,1分钟后访客仍没有回复,会话将被自动结束。 

系统自动结束会话15-1.png


 
2》人工客服名下的会话自动结束配置
        i. 会话在待接入排队的情况 
            管理员模式--设置--系统开关 待接入超时结束会话

系统自动结束会话15-2.png


 
       ii.会话由人工客服接入的情况(在人工客服的进行中会话列表)
           管理员模式--设置--系统开关 访客超时未回复自动结束会话                不活跃会话超时自动结束 两个配置项

系统自动结束会话15-3.png




系统自动结束会话15-4.png



注意:如果同时打开了“访客超时未回复自动结束会话”和“不活跃会话超时自动结束”开关,当满足任意开关的超时条件时,会话将会被自动结束。
配置文档可参考以下链接
http://docs.easemob.com/cs/200admin/50settings#%E8%AE%BF%E5%AE%A2%E8%B6%85%E6%97%B6%E6%9C%AA%E5%9B%9E%E5%A4%8D%E8%87%AA%E5%8A%A8%E7%BB%93%E6%9D%9F%E4%BC%9A%E8%AF%9D
注意:修改配置后针对新创建的会话生效,旧会话还是需要手动关闭
 
 
  收起阅读 »

客服云问候语配置

                                                             问候语的配置 (注意:问候语只有上班时间才会 自动发送,下班时间不会触发,上班时间是走的时间计划页面的配置) 客服系统可以同时配置三种...
继续阅读 »
                                                             问候语的配置
(注意:问候语只有上班时间才会 自动发送,下班时间不会触发,上班时间是走的时间计划页面的配置)
客服系统可以同时配置三种问候语:企业问候语 技能组问候语 客服问候语三种,触发条件不同

1.  企业问候语(管理员模式--设置--系统开关 企业问候语):当访客发送首条消息后,收到系统自动发送的企业问候语。

问候语配置14-1.png



2.技能组问候语(管理员模式--成员管理--在线技能组 基础设置 技能组问候语):会话调度给某个技能组后会触发该技能组的问候语

问候语配置14-2.png


3.客服问候语(客服模式--客服信息 客服问候语 每一个客服配置自己的问候语):会话成功分配或转接给当前客服/管理员时,访客会收到系统自动发送的客服问候语

问候语配置14-3.png


如果同时配置三个,可能都会触发。根据需求配置即可,一般情况只需要配置企业问候语。其他的不使用可以把对应开关关闭 收起阅读 »

客服云机器人转人工配置

                                       机器人转人工配置  1》旧版机器人:      管理员模式--智能机器人 自动回复--转人工设置。      可以规定转人工时间(全时间段允许 全时间段禁止 自定义时间<...
继续阅读 »
                                       机器人转人工配置 
1》旧版机器人
     管理员模式--智能机器人 自动回复--转人工设置。
     可以规定转人工时间(全时间段允许 全时间段禁止 自定义时间<仅上班 或者仅下班允许转接>)
     配置限制转接提示语
指定转人工技能组(如果是不指定,会话是转接到未分组。如果要实现机器人转接不同的技能组,此处选择为不指定,之后配合 会话分配规则页面的 关联指定 来实现机器人转接到不同技能组)


机器人转人工配置11-1.png




机器人转人工配置11-2.png




2》企业版机器人转人工配置
    1)管理员模式--智能机器人 点击机器人信息页面的蓝色字体 机器人管理 进入到机器人平台


机器人转人工配置11-3.png



    2)

机器人转人工配置11-4.png


上图是配置转人工时间段和指定技能组的
机器人设置模块--转人工设置 
默认情况可以把转人工指令和转人工意图开启即可


机器人转人工配置11-5.png


转人工指令可以自定义修改,默认的指令有:
中文:转人工、转坐席、转人工客服、转人工坐席、转人工服务;英文:chat with agent
   企业版机器人的转人工更多配置可以参见以下文档:
http://docs-ai.easemob.com/cs/900help#%E8%AE%BE%E7%BD%AE%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%BD%AC%E4%BA%BA%E5%B7%A5 收起阅读 »

客服云在线技能组配置

                                            技能组操作 设置技能组下班提示语            管理员模式--成员管理--在线技能组 切换到基础设置页签            有技能组下班提示语 开关(需要...
继续阅读 »
                                            技能组操作
  1. 设置技能组下班提示语 

          管理员模式--成员管理--在线技能组 切换到基础设置页签 
          有技能组下班提示语 开关(需要 打开)配置下班提示语,最后保存(见下图)
          如果下班时间路由规则直接指定技能组,下班时间访客发起会话时,系统自动发送下班提示语给访客

技能组操作10-1.png


      2.配置技能组问候语
        同上界面,基础设置页签 有问候语开关 需要打开,配置问候语,最后保存(注:问候语只有上班时间才会发送下班时间不会发送。这里的上班时间是时间计划页面的配置)

       更多技能组配置可以参见以下文档:
http://docs.easemob.com/cs/200admin/20teammgm#%E6%8A%80%E8%83%BD%E7%BB%84%E7%AE%A1%E7%90%86
  收起阅读 »

客服云 客服发送知识库附件

                                                           客服发送知识库中附件给访客 发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右...
继续阅读 »
                                                           客服发送知识库中附件给访客
发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右上角的“发送”按钮。


发送知识库附件6-1.png


设置知识库附件发送个访客的方式:
管理员模式--知识库  点击发送方式设置按钮


发送知识库附件6-2.png




发送知识库附件6-3.png


  收起阅读 »

客服云待接入会话不自动分配

                                                               待接入不自动分配问题          查看待接入列表的数据(管理员模式---待接入 可以看到租户所有的待接入数据)  ...
继续阅读 »
                                                               待接入不自动分配问题
         查看待接入列表的数据(管理员模式---待接入 可以看到租户所有的待接入数据) 

待接入不自动分配4-1.png


        看截图框选 技能组和 坐席名两列的数据
 i..如果坐席名列有数据,例如截图中第一条,说明有指定客服,需要保证指定的坐席上线后系统才能自动调度给该指定的客服
ii..如果坐席名一列没有数据,看技能组一列显示的技能组名称
之后对照到 (管理员模式--成员管理--在线技能组 ) 查看该技能组内客服状态分布
需要保证技能组内有 空闲状态的客服同时该客服的进行中会话数量不能超过最大接待人数

待接入不自动分配4-2.png



例如上图,是因为技能组内没有空闲的客服存在
     解决:
让该客服调整状态为空闲
如果仍不能自动调度,查看该空闲客服的当前进行中会话是否达到最大接待量(最大接待量不能是0)

待接入不自动分配4-3.png


客服的进行中会话数量可以到(管理员模式--当前会话 右上角 指定客服筛选看数量,对比进行中和最大接待量)

待接入不自动分配4-4.png


如果是因为超过最大接待量导致的,需要该客服手动关闭部分进行中会话,使其数量小于最大接待量
注意:另外后续不想客服手动关闭,可以配置系统自动结束会话
可以到  管理员模式----设置--系统开关 页面  
   访客超时未回复自动结束会话
   不活跃会话超时自动结束。配置后针对的是新会话生效,旧会话还是需要手动结束

配置文档:
http://docs.easemob.com/cs/200admin/50settings#%E8%AE%BF%E5%AE%A2%E8%B6%85%E6%97%B6%E6%9C%AA%E5%9B%9E%E5%A4%8D%E8%87%AA%E5%8A%A8%E7%BB%93%E6%9D%9F%E4%BC%9A%E8%AF%9D



如果按照上述条件均排查都符合正常,可以看下不会自动调度的时间是非工作时间,有个系统开关控制 
(管理员模式--设置--系统开关 只有技能组的上班时间才自动分配会话)如果是开启需要关闭可解决不自动调度的问题

待接入不自动分配4-5.png


 如果是是工作时间的会话没有自动调度,确认下如果客服坐席使用的手机APP工作台登录,需要把  管理员模式---设置--系统开关页面  分配会话时检测坐席在线状态  这个配置开关要关闭

检测.png


如果仍无法解决,可以在工作时间发起会话联系在线客服咨询解决。
    需要登录客服系统,切换到管理员模式 之后点击右上角技术支持,发起会话即可。

    登录客服云地址:https://kefu.easemob.com 收起阅读 »

客服云代码指定客服

                                          代码指定客服(APP,网页渠道都可以实现) 1》注意:代码指定针对新会话生效,需要到(管理员模式--当前会话) 手动关闭旧会话,访客端发起新会话测试 指定客服说明: 指定...
继续阅读 »
                                          代码指定客服(APP,网页渠道都可以实现)
1》注意:代码指定针对新会话生效,需要到(管理员模式--当前会话) 手动关闭旧会话,访客端发起新会话测试

  • 指定客服说明:

  • 指定客服之后,若指定客服在线,不受接待量和状态的限制(无论什么状态,无论接待量是多少),都直接分配给该客服

  • 指定客服之后,若指定客服不在线,有个灰度功能可以控制客服不在线时,会话是否分配给客服

  • 开关关闭时,客服不在线,则会话在待接入中排队(开关默认为关闭状态)

  • 开关开启时,客服不在线,则会话直接分配给该坐席

  • 开关为灰度功能,客户不能自己控制,需要联系环信开通,开通功能联系商务即可


2》相关代码 安卓文档:
http://docs.easemob.com/cs/300visitoraccess/androidsdk#%E6%8C%87%E5%AE%9A%E5%AE%A2%E6%9C%8D
ios:
http://docs.easemob.com/cs/300visitoraccess/iossdk#%E6%8C%87%E5%AE%9A%E6%9F%90%E4%B8%AA%E5%AE%A2%E6%9C%8D
网页(h5链接)
http://docs.easemob.com/cs/300visitoraccess/web-widget#%E6%8C%87%E5%AE%9A%E5%AE%A2%E6%9C%8D1
网页(引入js)
http://docs.easemob.com/cs/300visitoraccess/web-widget#%E6%8C%87%E5%AE%9A%E5%AE%A2%E6%9C%8D 

     参数中的客服账号可以到(管理员模式--成员管理--客服 列表)邮箱一列数据 为指定的客服账号


代码指定客服3-1.png


  
   验证方法: 如果某一个会话没有生效,先手动关闭掉,之后到到管理员模式---历史会话  找到该会话,点击后 查看右侧记录页面的数据:确认下 是否有该坐席被指定发起会话的记录,如果有说明成功,没有的话,请检查下代码和是否是新会话测试的
   

代码指定客服3-2.png

收起阅读 »

客服云 历史会话的筛选和导出

                                                                  历史会话的筛选和导出    tips:     结束的会话才能到历史会话模块进行导出。(历史会话记录默认保存6个月,如需延长...
继续阅读 »
                                                                  历史会话的筛选和导出
   tips:
  1.     结束的会话才能到历史会话模块进行导出。(历史会话记录默认保存6个月,如需延长,请联系环信商务经理。 )
  2.     客服模式和管理员模式都可以进行导出,数据权限不同
  3.     历史会话模块默认显示的是本周数据,从周日开始
  4.     在客服模式的“历史会话”页面,客服和管理员仅可查看自己参与并结束的会话;
  5.     在管理员模式的“历史会话”页面,管理员可以查看所有客服的历史会话。

 第一:筛选
      进入历史会话模块,点击右上角筛选排序,可以重新指定时间段,根据会话标签,访客昵称,环信ID等条件筛选


历史会话2-1.png



 ​第二:导出
     直接点击界面右下角导出按钮,生成的文件需要到导出管理进行下载


历史会话2-2.png




历史会话2-3.png



     更多历史会话操作 文档地址:
http://docs.easemob.com/cs/100agentmode/30history#%E5%AF%BC%E5%87%BA%E5%8E%86%E5%8F%B2%E4%BC%9A%E8%AF%9D
收起阅读 »

客服云 代码传访客属性

代码传访客属性(例如访客资料页的:昵称,名字,公司,描述等字段) 代码传客户信息 针对自定义字段不生效 客服系统以下开关需要打开           管理员模式--设置--系统开关 允许访客端修改客户信息  开关保持开启状态,见下图 ...
继续阅读 »
代码传访客属性(例如访客资料页的:昵称,名字,公司,描述等字段)
代码传客户信息 针对自定义字段不生效
  1. 客服系统以下开关需要打开

          管理员模式--设置--系统开关 允许访客端修改客户信息  开关保持开启状态,见下图


开关1-1.png




      2.APP和网页集成都需要对应的代码传值   相关代码文档:
Android 
http://docs.easemob.com/cs/300visitoraccess/androidsdkapi#%E5%8F%91%E9%80%81%E5%B8%A6%E8%AE%BF%E5%AE%A2%E5%B1%9E%E6%80%A7%E7%9A%84%E6%B6%88%E6%81%AF
ios
http://docs.easemob.com/cs/300visitoraccess/iossdkapi#%E5%8F%91%E9%80%81%E5%B8%A6%E8%AE%BF%E5%AE%A2%E5%B1%9E%E6%80%A7%E7%9A%84%E6%B6%88%E6%81%AF
网页(引入js)
http://docs.easemob.com/cs/300visitoraccess/web-widget#%E6%98%BE%E7%A4%BA%E8%AE%BF%E5%AE%A2%E4%BF%A1%E6%81%AF

   代码传值针对新创建的会话生效,需要到管理员模式--当前会话 找到旧会话,手动关闭。(手动关闭会话见下图)


手动关闭会话1-2.png


 
  之后访客端发送新消息,要求新会话第一条消息扩展必须有访客属性
  收起阅读 »

如何用 30 天入门年薪 30 万的技术领域?

2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。 值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。 自此之...
继续阅读 »
2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。
值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。
自此之后,深度学习开始逐渐进入大家的视野。

01
什么是深度学习?


深度学习其实是机器学习(Machine Learning)的一个分支学科,而机器学习主要是研究数据之言的关系的,比如它可以用来分析性别、年龄、学历、职业等因素之间的数学关系。
显而易见,这些因果关系并不是一个简单的线性关系就能解决的。但是深度学习通过多非线性模型表示数据之间的关系,从而确定数据之间的相互关系是什么。

02
深度学习已经融入到了人们的日常生活中


现在汽车的自动驾驶、短信邮件的自动回复、扫地机器人,以及在围棋中击败最优秀人类选手的软件,都应用到了深度学习。甚至骚扰电话,也有很多是AI机器人打过来的,可能电话那头和你聊得正酣、声音甜美的客服妹子只是一串代码。
因此,阿里达摩院的工程师们,受不了骚扰骚扰电话的骚扰,发起了“二哈”AI,让机器人与骚扰电话自动聊天。
大家感受一下这对话画风


1.gif


 
总之,AI机器人的骚扰电话,正在用AI进行解决。

03

可能是编程领域薪资最高的岗位


而与此同时学习深度学习的人也开始变得多了起来,但由于深度学习涉及面比较广,很多学者会对此望而却步。然而如果学好深度学习,不仅仅可以给你带来高薪资,也有可能会给你带来很多的荣誉!因为在此领域还是非常缺乏人才的!
大家可以看一份数据


2.png


 
平均月薪已达到3w,年薪36w。


4.png


 
就应届生深度学习岗的薪资,都已经达到了很多其他互联网职业3年工作经验都难以企及的水平。
现在各个大厂,也都有深度学习相关的项目,而且待遇方面也是极其的诱人。
百度推出“少帅计划”,针对30岁以下的深度学习科学家,开出100万以上年薪
阿里巴巴对外宣布将通过校园招聘组建一支规模达数百人的 NASA“青年军”
华为更是开出200万年薪招聘应届毕业生,其专业均为最前沿的人工智能领域
作为一个有追求的程序员,相信你在脑海中闪过无数次这样的疑问:如何掌握深度学习?

04

如何掌握深度学习?


目前市场对于深度学习的需求,早已出现供不应求的现象,但太多工程师想入门或者转行却不知从何开始,很多初学者都会有这样的困惑:
一边要熟练掌握线性代数、矩阵计算,一边还要搞概率论
一边要去研究各种库与框架,一边学习如何在用编程语言实现算法
一边学习如何制作数据集、特征提取,一边微调参数,选择合适的算法
这样一轮下来,深度学习还没有开始就已经走上了放弃之路。
放眼网上现有的一些深度学习课程,经常会发现有些知识点覆盖不全,或者学习门槛较高,研究性的问题不多,或者说只关注面试,而忽略了底层的逻辑以及真实案例的实操。
因此,我们通上百名深度学习用户的调研,与老师花费上千小时的时间,提炼了102节精华课程。
我们希望通过“从零开始深度学习”这门课程,让你0基础入门深度学习,建立起完整的学习路径,同时通过“智能问答模型”的实战案例,将所学知识学以致用。
#购课即送王海良老师在京东原价69的《智能问答与深度学习》实体书一本#

讲师介绍
王海良:Chatopera联合创始人&CEO,微软人工智能最有价值专家,先后工作于IBM软件开发实验室和创新中心。
李卓桓:25年编程经验,曾任优酷网首席科学家、叽歪网创始人,水木清华BBS站长,紫霞BBS站长。
林旭鸣:北京邮电大学模式识别实验室研究生,任职阿里巴巴的阿里小蜜团队。
陈可心:香港大学硕士,任职经历包括:微软中国、今日头条研发中心,联想香港人工智能中心以及联合国亚太分部。
李思珍:现任职今日头条,主要工作实现人机交互系统的意图识别和关键词优化。

#课程介绍#
相信很多小伙伴,在入门深度学习的路上,会被一些数据基础搞得犯怵,搭建环境的时候也许会遇到很多坑。
因此,我们把课程分为五个模块:数学基础、Python编程语言基础、深度学习初步、深度学习深化、智能问答模型实践,带你一探深度学习的究竟,希望你通过此次学习,不仅掌握原理更能动手实操。
除了上述102节视频课程,我们还为这次课程搭配了书籍《深度学习与智能问答》,一边跟着视频实操,一边阅读书籍巩固,学习效果double~


5.jpg


 
如何保证你的学习效果?
由基础的数学学习开始,逐渐建立完整的深度学习知识体系
五大模块,由知识学习到真实案例的实践
购课送原价69元深度学习实体书,边学边读,加深记忆
建立一个互助、监督的高效学习社群,随时交流问题

购课须知
本课程包含哪些内容?
包含102节视频课(20+小时)+实体书(京东原价69)

上课形式是怎样的?
课程授课形式为:视频+群答疑

#如何报名#
限时特惠:99元(原价199元)
拼团特惠69元~
图书(含邮寄) + 100+节视频课程,每课仅需6毛钱

扫码立即抢购!
平均一天2元,就是一瓶水钱
坚持30天,换你一次进入高薪技术领域的机会
如果对课程有疑问
欢迎扫码回复“1”进课程咨询群


6.jpg


 
 
  收起阅读 »

环信助力2019 中国开源年会(COSCon'19)顺利启航!

  COSCon '19正式启动啦!时间: 2019-11-02 09:00 ~ 11-03 17:00 地址: 上海普陀区上海普陀区中山北路3663号华东师范大学(中北校区) 业界最具影响力的开源年度盛会2019中国开源年会 ( COSCon'19 )...
继续阅读 »


微信图片_20190909152905.jpg


 
COSCon '19正式启动啦!时间: 2019-11-02 09:00 ~ 11-03 17:00
地址: 上海普陀区上海普陀区中山北路3663号华东师范大学(中北校区)
业界最具影响力的开源年度盛会2019中国开源年会 ( COSCon'19 )将于 11月2-3日在华东师范大学 (上海普陀区中山北路校区)由开源社举办。
我们预期会有超过1600人现场参与这次盛会,还会有超过1万名在线的观众,热切围观。感谢许多社区伙伴、企业伙伴和志愿者,携手促使这样规模的 COSCon  诞生。
本次大会的主题是“开源无疆、携手出航”(Let’s Cross the Boundaries Together!),这也代表我们对于中国开源,走向世界,走向辉煌的殷切期望。
本次大会将持续两天,我们策划的主题包括:开源软件、开源硬件、社区运营与治理、开源教育等方向。也特别希望听到各种自由/开源相关的成功故事和酸甜苦辣的经历。我们诚挚地邀请您的参与!
 
大会亮点


活动亮点.jpg


 
活动报名链接:https://www.bagevent.com/event/5744455 
  收起阅读 »

【报名】环信&华为首届《5G音视频开发创业沙龙》大咖+干货+大奖,等你来撩!

2019年是5G商用元年,作为第五代通信技术,未来将结合云计算、人工智能、物联网等技术逐渐改变亿万用户的生活消费方式,带来万亿级的产业空间。Gartner预测到2020年,7%的全球通信服务提供商将拥有商业上可行的无线5G服务,2023年5G智能手机将占总销量...
继续阅读 »
2019年是5G商用元年,作为第五代通信技术,未来将结合云计算、人工智能、物联网等技术逐渐改变亿万用户的生活消费方式,带来万亿级的产业空间。Gartner预测到2020年,7%的全球通信服务提供商将拥有商业上可行的无线5G服务,2023年5G智能手机将占总销量的51%,谁先拥抱5G谁就能在万亿级的新产业空间里拔得头筹。
当下,短视频、互动直播等音视频应用火爆来袭,抖音、快手等已成为当红流量入口,当移动互联红利消失殆尽,各行业获客成本不断攀升的态势下,5G催生的音视频应用风口已成为兵家必争之地,让各条赛道又迎来弯道超车的好机会。
 同时,音视频应用正在加快与人工智能、5G信息显示等领域的融合,不断催生新业态和商业模式,但对创业者和开发者来说还是挑战诸多:1,5G时代如何选择一家靠谱的云基础设施资源服务厂商?音视频领域有哪些技术要点和哪些典型应用?2,5G时代音视频社交领域有哪些新玩法?3,5G创业如何实现从0—1的快速冷启动?4,产品研发上线后该如何科学运营,拉新、获客、留存,洞悉市场和用户行为实现快速增长……
 
9月21日,坐标北京,中关村创业大街,来这里听一场就够了!
 
环信联合华为举办首届《5G音视频开发创业沙龙》,给创业者们带来5G音视频方面的最新最佳技术实践,分享在5G音视频领域创业,开发、运营、安全、部署等一揽子解决方案,深入解读5G音视频火热背后的技术奥秘和新增长机会。


5G音视频长图.jpg


 


微信二维码.jpg


 
扫码进入 环信&华为@5G沙龙交流群
 
报名链接:http://hdxu.cn/sJ8fm 
  收起阅读 »

new

修改聊天界面titlebar 标题 看LoginActivity intent跳转传参  setTitleName() 这个参数就是title // 进入主页面 Intent intent = new IntentBuilder(LoginActivit...
继续阅读 »
  1. 修改聊天界面titlebar 标题

看LoginActivity intent跳转传参  setTitleName() 这个参数就是title
// 进入主页面
Intent intent = new IntentBuilder(LoginActivity.this) .setTargetClass(ChatActivity.class) .setVisitorInfo(DemoMessageHelper.createVisitorInfo()) .setServiceIMNumber(Preferences.getInstance().getCustomerAccount()) .setScheduleQueue(DemoMessageHelper.createQueueIdentity(queueName)) .setTitleName(titleName) // .setScheduleAgent(DemoMessageHelper.createAgentIdentity("ceshiok1@qq.com")) .setShowUserNick(true) .setBundle(bundle) .build();
startActivity(intent);        2. 设置显示客服头像和昵称
      (1)首先客服系统 切换到管理员模式--设置--系统开关  访客端显示客服昵称开关打开。
      (2)参考DemoHelpe类中,setEaseUIProvider方法,消息判断为接收方receive中的代码,客服发送的消息,从消息扩展可以获取到坐席信息。
   参考文档:http://docs.easemob.com/cs/300visitoraccess/extended-message-format#%E6%98%BE%E7%A4%BA%E5%AE%A2%E6%9C%8D%E5%A4%B4%E5%83%8F%E5%92%8C%E6%98%B5%E7%A7%B0
         3.设置显示访客头像:
           (1) 找到DemoHelper 类中,setEaseUIProvider 方法,消息判断为发送方send的代码,可以设置访客头像
           (2)需要修改kefueaseui 库中ChatRow类的代码,如果远程依赖无法修改,建议先使用本地依赖的方式
本地依赖下载地址http://www.easemob.com/download/cs   打开链接,页面底部找访客端SDK和Demo,Demo中找到kefueaseui库,手动导入,把之前远程依赖的注释掉,重新添加本地依赖
          (3)ChatRow 类中找到setUpBaseView  之后看对adapter的判断


chatrow.png


把框选位置条件删掉即可
     注意:kefueaseui 库,消息布局没有访客昵称控件,如果需要显示访客昵称数据,需要单独先给布局添加昵称控件。
           
  收起阅读 »

iOS SDK 日志文件的导出

一、console后台获取,如图二、xcode获取环信SDK提供2.x和3.x两个版本。初始化SDK成功,会写入日志文件到本地。 日志文件路径如下: 2.x 已停止维护            &nb...
继续阅读 »
一、console后台获取,如图


二、xcode获取
环信SDK提供2.x和3.x两个版本。初始化SDK成功,会写入日志文件到本地。

日志文件路径如下:
  • 2.x 已停止维护             沙箱/Library/EaseMobLog
  • 3.5.4之前(不含3.5.4) 沙箱Documents/HyphenateSDK/easemoblog 
  • 3.5.4之后(含3.5.4)    沙箱Library/Application Support/HyphenateSDK/easemobLog 
下面以3.5.4之后的版本为例,演示获取方法

模拟器: 
  • 打印NSHomeDirectory()      

模拟器1.png

  • 复制路径,打开Finder前往     

模拟器2.png

  • 访问到沙箱目录

模拟器3_路径.png


真机:
  • 打开Xcode连接设备,运行成功后,前往Xcode --> Window --> Devices and Simulators 

真机1.png

  • 进入Devices界面

真机2.png

  • 选择Download Container之后会下载到本地一个.xcappdata文件。选中这个文件鼠标右键显示包内容。

真机3.png

  • 访问到沙箱目录   

真机4_路径.png


注:easemobDB是存放历史消息的本地数据库
收起阅读 »

【源码下载】一款使用环信实现的开源灵魂社交APP(含服务器)

#前言 近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。   上代码 服务器:VMServer 客户端:VMMatch    #VM...
继续阅读 »
#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。


猿匹配_logo_副本.png



  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图


1.png



2.png



3.png



4.png



5.png



6.png


  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖
npm install

6.全局安装pm2
npm install pm2 -g
 
7.运行 vmshell.sh
 

环信冬冬_副本.jpg


扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
  收起阅读 »

(客服云)iOS访客端接收图文消息不展示封面图片怎么办?

将HDBubbleView.h+Article.m中的- (instancetype)initWithDictionary:(NSDictionary *)dic方法改成下面这样就可以了 - (instancetype)initWithDictionary:(...
继续阅读 »
HDBubbleView.h+Article.m中的- (instancetype)initWithDictionary:(NSDictionary *)dic方法改成下面这样就可以了
- (instancetype)initWithDictionary:(NSDictionary *)dic {
self = [super init];
if (dic != nil) {
_type = HDCellTypeSub;
_title = [dic objectForKey:@"title"];
double createTime = [[NSDate date] timeIntervalSince1970] * 1000;
if ([dic objectForKey:@"createdTime"]) {
createTime = [[dic objectForKey:@"createdTime"] doubleValue];
}
_createTime = [self timeFormatter:createTime/1000];
_digest = [dic objectForKey:@"digest"];

// //封面展示缩略图
// NSString *thumbUrl = [dic objectForKey:@"thumbUrl"];
// if (thumbUrl) {
// if (thumbUrl && [thumbUrl hasPrefix:@"http"]) {
// _imageUrl = thumbUrl;
// }else {
// _imageUrl = [NSString stringWithFormat:@"%@%@",[HDClient.sharedClient kefuRestServer], thumbUrl];
// }
// }

// 封面展示原图
NSString *picUrl = [dic objectForKey:@"picurl"];
if (picUrl) {
if (picUrl && [picUrl hasPrefix:@"http"]) {
_imageUrl = picUrl;
}else {
_imageUrl = [NSString stringWithFormat:@"%@%@",[HDClient.sharedClient kefuRestServer], picUrl];
}
}

NSString *detailUrl = [dic objectForKey:@"url"];
if (detailUrl) {
if (detailUrl && [detailUrl hasPrefix:@"http"]) {
_url = detailUrl;
}else {
_url = [NSString stringWithFormat:@"%@%@",[HDClient.sharedClient kefuRestServer], detailUrl];
}
}
}
return self;
}
收起阅读 »

(客服云)企业版机器人如何设置转人工

注意:只购买了企业版机器人,不使用客服系统的同学,可忽略第一、二步。   一、管理员模式--智能机器人--机器人设置,选择【转人工设置】,这里可以设置“转人工时间段”、“限制转人工提示语”和“转人工指定技能组”。 不指定技能组,转人工时,会按照 设置--会话分...
继续阅读 »
注意:只购买了企业版机器人,不使用客服系统的同学,可忽略第一、二步。
 
一、管理员模式--智能机器人--机器人设置,选择【转人工设置】,这里可以设置“转人工时间段”、“限制转人工提示语”和“转人工指定技能组”。
不指定技能组,转人工时,会按照 设置--会话分配规则 页面设置的“路由规则”分配给对应的技能组。

企业版机器人转人工设置1.png


 
二、还是这个页面,选择【基础设置】,然后点击【机器人管理】,跳转到机器人管理页面。
    有些同学点击后没反应,那有可能是被浏览器拦截了,手动释放即可。

企业版机器人转人工设置2.png


 
三、来到机器人管理页面,找到机器人设置--转人工设置,先【添加转人工参数】,添加时“参数名”随便写,“参数值”必须是技能组的名称,否则无法转接到此技能组。不添加转人工参数,则无法指定技能组。
然后打开【转人工指令】的开关,配置“转人工指令”以及“参数”
开关开启,但是不指定参数,转人工时,会按照客服系统中 设置--会话分配规则 页面设置的“路由规则”分配给对应的技能组。
注意:在这里开启了“转人工指令”,第1步的“转人工指定技能组”则失效,以这里为准。
         不开启,则以第1步的“转人工指定技能组”的配置为准。

企业版机器人转人工设置3.png
收起阅读 »

《拍拍二手》微信小程序之环信接入

拍拍二手闲置平台,可以将自己的闲置物品进行转让或者捐赠。想和卖家达成共识就需要涉及IM聊天。拍拍二手闲置平台目前接入的是环信IM聊天。下面我将从三个阶段带大家玩转环信IM会话。 前期 初识IM聊天 带着问题去调研 必须接入环信吗?除了环信是否可以接入其...
继续阅读 »
拍拍二手闲置平台,可以将自己的闲置物品进行转让或者捐赠。想和卖家达成共识就需要涉及IM聊天。拍拍二手闲置平台目前接入的是环信IM聊天。下面我将从三个阶段带大家玩转环信IM会话。
前期
初识IM聊天
带着问题去调研

必须接入环信吗?除了环信是否可以接入其他即时通信?
环信目前有哪些功能呢?支持微信小程序吗?
如何接入小程序呢?
调研分析

必须接入环信吗?除了环信是否可以接入其他即时通信?
现状: 微信小程序API 提供了WebSocket 方法。
扩展: 如果服务端支持scoket通信,ios\android\H5 也全都支持Im聊天了
备注:专业第三方Im有融云、环信、云之讯等,底层实现均是基于scoket 通信。明白scoket通信后也可以自己写即时通信。
环信目前有哪些功能呢?支持微信小程序吗?
错误想法: 环信就是做im聊天的,咱们上去按照接入文档,开发就能搞定!!!
这种想法是很致命的。在所有的第三方组件接入中,如果我们不能跳出来看待问题,只是为了完成任务而完成任务。那么我们永远是最底层的低级码农。
环信目前是同行业里面做的算不错的。那么他的官网、接入规范都应该有的。微信小程序也是支持的。在后面小编会带领大家一切怎么去阅读一个官网
如何接入小程序?
接入小程序是否需要申请一个账号呢?我直接运行他们的demo可以吗? 怎么去测试呢? 此时我们可以有很多的猜想。我认为在开始接入之前我们应该很好的进行一些思考,答案显而易见。
环信接入思考篇
快即时慢

在工作中,大家会经常遇到第三方组件的接入。当接收到任务后,为了尽快完成任务。上来就google,找攻略,找技巧。往往认为这样做速度是最快的。结果适得其反,做了很多无用的功。我们意识中的快,结果却变成了慢
慢即时快image逆向思维: 任何一个第三方的组件,特别是一个大点的平台,他们为了推出自己的产品,一定会有各种各样的功能支持,接入文档说明。我们放慢速度,将这些资源用上半天的时间进行简单的梳理。后期的开发进度会有很大的提升。
上图是我在接入环信Im后进行的反思。因为在接入环信之前,其他团队成员用了很长的时间联调。假如他们在接入环信聊天之前,了解环信拥有自己的后台,可以直接给用户端发送测试消息;可以直接创建用户、创建聊天室、创建群组。他们还会花费那么久的时间去联调吗?完全不用依赖服务端。不用依赖ios,依赖android。自己使用环信后台,轻轻松松完成各种测试。
环信接入
环信官网注册自己的即时通讯云,并登陆后台


1.jpg



创建自己的应用,并记录关键信息


2.jpg



以下是关键信息哦!!!


3.jpg



备注:
应用标识 应用接入时会使用
IM 用户 可以创建、删除用户、发送消息
群组 可以创建、删除群组信息、发送消息
聊天室 可以创建、删除聊天室、发送消息
tip 通过这个后台管理系统,就可以玩转环信的接入测试了。
从环信下载小程序demo,替换 appkey 进行联调测试


4.jpg



测试走起
用户测试 
在环信后台创建用户,在小程序端登录 (用户demo1 密码:123456)

5.jpg



一对一会话测试
① 在环信后台创建用户demo2
② 点击操作,查看用户好友将demo1和demo2 添加为好友。
③ 在小程序端用demo1给demo2发送测试消息。
④ 退出demo1用户,登录demo2查看是否会接收到demo1发送的会话


6.jpg



由于环信工程师们相信码农的实力,在群组测试和聊天室测试这块为大家留下了想象空间。demo 中群组测试和聊天室测试为明确写出。让我继续带大家飞

群组测试
① 创建群组记录群组id,并给群组添加成员(demo2)


7.jpg



② 环信后台给群组发送测试消息


8.jpg



③ 控制台能收到群组测试消息,怎么展示呢? 请阅读源码解析篇
聊天室测试
① 创建聊天室记录聊天室id,将demo1 设置为超级管理员,demo2设置为管理员
② 聊天室这里没有聊天室消息的发送。请阅读源码解析篇
通过以上4个简单的测试,android、ios、h5、小程序的聊天测试均可以参照以上4点进行顺利的测试。初期就此结束。下面带代价进行源码的解析
中期
看源码前期思考


10.jpg


 
核心源码阅读


11.jpg


 
以上是环信sdk 基础代码结构。 通过简单阅读会发现:环信的scoket 通信也使用了微信小程序暴露的scoket 通信 (猜想 android、ios 其他端也有对应的scoket通信)
环信的api包装在connection.js 组件中,如果某些api没有,咱们可以扩展connection 中的方法
环信核心代码阅读完成后,发现没有涉及到缓存。看来缓存的处理是在对应的业务逻辑中。
设想:
消息应该在哪里缓存
哪里进行会话链接的监听注册
环信demo 代码阅读
会话、群组
通过前面提到的方式,大家可以在小程序控制台抓取到用户收到的会话和群组消息
会话
app.js
环信scoket 注册监听代码在app.js 中
核心代码如下:
{
//调用API从本地缓存中获取数据
var that = this
var logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)

WebIM.conn.listen({
onOpened: function (message) {//连接成功回调
// 如果isAutoLogin设置为false,那么必须手动设置上线,否则无法收消息
// 手动上线指的是调用conn.setPresence(); 如果conn初始化时已将isAutoLogin设置为true
// 则无需调用conn.setPresence();
WebIM.conn.setPresence()
},
onPresence: function (message) { //处理“广播”或“发布-订阅”消息,如联系人订阅请求、处理群组、聊天室被踢解散等消息
switch(message.type){
case "unsubscribe":
pages[0].moveFriend(message);
break;
case "subscribe":
if (message.status === '[resp:true]') {
return
} else {
pages[0].handleFriendMsg(message)
}
break;
case "joinChatRoomSuccess":
console.log('Message: ', message);
wx.showToast({
title: "JoinChatRoomSuccess",
});
break;
case "memberJoinChatRoomSuccess":
console.log('memberMessage: ', message);
wx.showToast({
title: "memberJoinChatRoomSuccess",
});
break;
case "memberLeaveChatRoomSuccess":
console.log("LeaveChatRoom");
wx.showToast({
title: "leaveChatRoomSuccess",
});
break;
}
},
onRoster: function (message) { //处理好友申请
var pages = getCurrentPages()
if (pages[0]) {
pages[0].onShow()
}
},

onVideoMessage: function(message){ //视频处理
console.log('onVideoMessage: ', message);
var page = that.getRoomPage()
if (message) {
if (page) {
page.receiveVideo(message, 'video')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'video',
data: message.url
},
style: '',
time: time,
mid: 'video' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},

onAudioMessage: function (message) { // 音频处理
console.log('onAudioMessage', message)
var page = that.getRoomPage()
console.log(page)
if (message) {
if (page) {
page.receiveMsg(message, 'audio')
} else {
var chatMsg = that.globalData.chatMsg || []
var value = WebIM.parseEmoji(message.data.replace(/\n/mg, ''))
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'audio',
data: value
},
style: '',
time: time,
mid: 'audio' + message.id
}
console.log("Audio msgData: ", msgData);
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},

onLocationMessage: function (message) { // 收到位置信息
console.log("Location message: ", message);
},

onTextMessage: function (message) {//收到文本消息
var page = that.getRoomPage()
console.log(page)
if (message) {
if (page) {
page.receiveMsg(message, 'txt')
} else {
var chatMsg = that.globalData.chatMsg || []
var value = WebIM.parseEmoji(message.data.replace(/\n/mg, ''))
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'txt',
data: value
},
style: '',
time: time,
mid: 'txt' + message.id
}
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onEmojiMessage: function (message) { //收到表情信息
//console.log('onEmojiMessage',message)
var page = that.getRoomPage()
//console.log(pages)
if (message) {
if (page) {
page.receiveMsg(message, 'emoji')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'emoji',
data: message.data
},
style: '',
time: time,
mid: 'emoji' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || [] //tip 从本地缓存中获取用户的消息 发消息+来源 适用于单人会话 msgData.yourname + message.to+当前登录人 群组/聊天室

chatMsg.push(msgData)
//console.log(chatMsg)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onPictureMessage: function (message) {//收到图片信息
//console.log('Picture',message);
var page = that.getRoomPage()
if (message) {
if (page) {
//console.log("wdawdawdawdqwd")
page.receiveImage(message, 'img')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'img',
data: message.url
},
style: '',
time: time,
mid: 'img' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
// 各种异常
onError: function (error) {
// 16: server-side close the websocket connection
if (error.type == WebIM.statusCode.WEBIM_CONNCTION_DISCONNECTED) {
if (WebIM.conn.autoReconnectNumTotal < WebIM.conn.autoReconnectNumMax) {
return;
}

wx.showToast({
title: 'server-side close the websocket connection',
duration: 1000
});
wx.redirectTo({
url: '../login/login'
});
return;
}

// 8: offline by multi login
if (error.type == WebIM.statusCode.WEBIM_CONNCTION_SERVER_ERROR) {
wx.showToast({
title: 'offline by multi login',
duration: 1000
})
wx.redirectTo({
url: '../login/login'
})
return;
}
},
})


}
实际开发过程中,在微信中,退出小程序,重新进入时,webscoket 通信并没有重新创建链接。存在用户收到不到消息的情况。可以将以上代码封装,例如addHXLIstener(...)。当用户重新打开后,再次注册环信监听即可。
拍拍二手闲置交易平台,主要集成的是文本聊天功能。
环信登录 例如 initLoginHX();
 var uin=wx.getStorageSync('hxuin');
var pwd=wx.getStorageSync('hxpwd');
console.log('initHX:' + uin+"||"+pwd);
var options = {
apiUrl: '服务器url',
user: '用户名',// 用户名要是字符
pwd: '密码',
grant_type: 'password',
appKey: 'appkey',
success: function (res) {
console.log("环信创建连接成功")
},
error: function (res) {
console.log("环信创建连接失败")
}
};
WebIM.conn.open(options);
chat 会话
环信的会话列表存储在本地,并没有调用服务器端数据
 var that = this
var member = wx.getStorageSync('member')
var myName = wx.getStorageSync('myUsername')
var array = []
for (var i = 0; i < member.length; i++) {
if (wx.getStorageSync(member[i].name + myName) != '') {
array.push(wx.getStorageSync(member[i].name + myName)[wx.getStorageSync(member[i].name + myName).length - 1])
}
}
//console.log(array,'1')
this.setData({
arr: array
})
通过以上代码得出结论: 环信的会话是通过遍历用户id+对方id 构成的数据。
那群组和聊天室的怎么处理呢?
环信小程序demo中只提供了聊天室列表的获取接口我们可以轻松实现聊天室列表,并没有提供群组列表的获取方式。我们需要在conection中扩展调用群组列表的接口,来实现群组列表。参照聊天室列表获取即可实现。聊天室列表实现方式如下:
connection.prototype.getChatRooms = function (options) {

var conn = this,
token = options.accessToken || this.context.accessToken;

if (token) {
var apiUrl = this.apiUrl;
var appName = this.context.appName;
var orgName = this.context.orgName;

if (!appName || !orgName) {
conn.onError({
type: _code.WEBIM_CONNCTION_AUTH_ERROR
});
return;
}

var suc = function (data, xhr) {
typeof options.success === 'function' && options.success(data);
};

var error = function (res, xhr, msg) {
if (res.error && res.error_description) {
conn.onError({
type: _code.WEBIM_CONNCTION_LOAD_CHATROOM_ERROR,
msg: res.error_description,
data: res,
xhr: xhr
});
}
};

var pageInfo = {
pagenum: parseInt(options.pagenum) || 1,
pagesize: parseInt(options.pagesize) || 20
};
// 想要实现群组列表,修改对应接口即可
var opts = {
url: apiUrl + '/' + orgName + '/' + appName + '/chatrooms',
dataType: 'json',
type: 'GET',
header: {'Authorization': 'Bearer ' + token},
data: pageInfo,
success: suc || _utils.emptyfn,
fail: error || _utils.emptyfn
};
wx.request(opts);
} else {
conn.onError({
type: _code.WEBIM_CONNCTION_TOKEN_NOT_ASSIGN_ERROR
});
}
chatroom
从本地缓存中获取聊天记录,并展示
// 环信demo 发送消息

sendMessage: function () {

if (!this.data.userMessage.trim()) return;


var that = this
// //console.log(that.data.userMessage)
// //console.log(that.data.sendInfo)
var myName = wx.getStorageSync('myUsername')
var id = WebIM.conn.getUniqueId();
var msg = new WebIM.message('txt', id);
msg.set({
msg: that.data.sendInfo,
to: that.data.yourname,
roomType: false,
success: function (id, serverMsgId) {
console.log('send text message success')
}
});
// //console.log(msg)
console.log("Sending textmessage")
msg.body.chatType = 'singleChat'; // 群组聊天 groupRoom
WebIM.conn.send(msg.body);
// 消息发送完成

if (msg) {
var value = WebIM.parseEmoji(msg.value.replace(/\n/mg, '')) // 环信表情处理
var time = WebIM.time()
var msgData = {
info: {
to: msg.body.to
},
username: that.data.myName,
yourname: msg.body.to,
msg: {
type: msg.type,
data: value
},
style: 'self',
time: time,
mid: msg.id
}
that.data.chatMsg.push(msgData)
// console.log(that.data.chatMsg)

// 存储聊天记录
// 注: 单独单聊天 key 对方环信uin+自己的uin
// 注: 群组聊天 key 群组id\聊天室id+对方环信uin+自己的uin
wx.setStorage({
key: that.data.yourname + myName,
data: that.data.chatMsg,
success: function () {
//console.log('success', that.data)
that.setData({
chatMsg: that.data.chatMsg,
emojiList: [],
inputMessage: ''
})
setTimeout(function () {
that.setData({
toView: that.data.chatMsg[that.data.chatMsg.length - 1].mid
})
}, 100)
}
})
that.setData({
userMessage: ''
})
}
},

// 环信demo 收到消息
receiveMsg: function (msg, type) {
var that = this
var myName = wx.getStorageSync('myUsername')
if (msg.from == that.data.yourname || msg.to == that.data.yourname) {
if (type == 'txt') {
var value = WebIM.parseEmoji(msg.data.replace(/\n/mg, ''))
} else if (type == 'emoji') {
var value = msg.data
} else if(type == 'audio'){
// 如果是音频则请求服务器转码
console.log('Audio Audio msg: ', msg);
var token = msg.accessToken;
console.log('get token: ', token)
var options = {
url: msg.url,
header: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'audio/mp3',
'Authorization': 'Bearer ' + token
},
success: function(res){
console.log('downloadFile success Play', res);
// wx.playVoice({
// filePath: res.tempFilePath
// })
msg.url = res.tempFilePath
var msgData = {
info: {
from: msg.from,
to: msg.to
},
username: '',
yourname: msg.from,
msg: {
type: type,
data: value,
url: msg.url
},
style: '',
time: time,
mid: msg.type + msg.id
}

if (msg.from == that.data.yourname) {
msgData.style = ''
msgData.username = msg.from
} else {
msgData.style = 'self'
msgData.username = msg.to
}

var msgArr = that.data.chatMsg;
msgArr.pop();
msgArr.push(msgData);

that.setData({
chatMsg: that.data.chatMsg,
})
console.log("New audio");
},
fail: function(e){
console.log('downloadFile failed', e);
}
};
console.log('Download');
wx.downloadFile(options);
}
//console.log(msg)
//console.log(value)
var time = WebIM.time()
var msgData = {
info: {
from: msg.from,
to: msg.to
},
username: '',
yourname: msg.from,
msg: {
type: type,
data: value,
url: msg.url
},
style: '',
time: time,
mid: msg.type + msg.id
}
console.log('Audio Audio msgData: ', msgData);
if (msg.from == that.data.yourname) {
msgData.style = ''
msgData.username = msg.from
} else {
msgData.style = 'self'
msgData.username = msg.to
}
//console.log(msgData, that.data.chatMsg, that.data)
that.data.chatMsg.push(msgData)

// 存储聊天记录
// 注: 单独单聊天 key 对方环信uin+自己的uin
// 注: 群组聊天 key 群组id\聊天室id+对方环信uin+自己的uin

wx.setStorage({
key: that.data.yourname + myName,
data: that.data.chatMsg,
success: function () {
if(type == 'audio')
return;
//console.log('success', that.data)
that.setData({
chatMsg: that.data.chatMsg,
})
setTimeout(function () {
that.setData({
toView: that.data.chatMsg[that.data.chatMsg.length - 1].mid
})
}, 100)
}
})
}
},
环信聊天页面,聊天数据全部存储在缓存当中,跟进聊天类型的不同,主要需要调整缓存的key。详情如下:
单对单聊天 对方uin+自己的uin
群组聊天(针对某个商品,不需要好友关系,只需要临时聊天) 群组id+对方uin+自己的uin
聊天室(同群组聊天)
问题大杂烩
群组聊天缓存如何存储?
答: 缓存key 设置为 群组id+对方uin+自己的uin
聊天时,如何在聊天中携带扩展信息
答: 消息内容中,ext 支持用户自定义参数传递
var option = {
msg: data.userMessage.trim(), // 消息内容
to: data.groupId, // 接收消息对象(聊天室id)
roomType: true,
chatType: 'groupRoom',
from: data.myuin,
ext: {
//todo 需要补充的字符哦
},
success: function () {
console.log('send room text success');
},
fail: function () {
console.log('failed');
}
};
```
会话列表如何实现?
答: 通过接口获取环信的群组列表,通过自己的服务器端补全对应的会话信息。
回顾
整个环信接入,整体围绕 假设-->猜想-->实践完成的。仔细阅读官网,会为大家节约很多时间
作者:贾慧斌
文章来源:简书

  收起阅读 »

重磅开源!业内首个 React Native 转微信小程序引擎

前言 Alita 是一套由京东 ARES 多端技术团队打造的 React Native 代码转换引擎工具。它对 React 语法有全新的处理方式,支持在运行时处理 React 语法,实现了 React Native 和微信小程序之间的主要组件对齐,可以用...
继续阅读 »
前言

Alita 是一套由京东 ARES 多端技术团队打造的 React Native 代码转换引擎工具。它对 React 语法有全新的处理方式,支持在运行时处理 React 语法,实现了 React Native 和微信小程序之间的主要组件对齐,可以用简洁、高效的方式把 React Native 代码转换成微信小程序代码。
Alita 不是一个新框架,不会有额外的学习成本,她只是一套转换引擎工具,可以把 React Native 扩展到微信小程序端,大大降低多终端上的业务开发成本。以后移动端开发者只需要掌握 React Native 技术栈,就可以轻松实现 Android、iOS、Windows、Web(已有开源项目支持)、微信小程序等多端渲染。
 
Alita 项目

开源地址: https://github.com/areslabs/alita
React Native 


11.gif


 
微信小程序


1.gif



Alita 具备哪些能力
Alita 的设计目标是要尽可能无损的转换 RN 应用,即使是已经存在的 RN 应用,我们也希望只做少量的修改就可以在微信小程序平台运行,所以这就要求 Alita 必须对 React 语法有足够的支持,包括 JSX 语法,React 生命周期等

JSX 语法

Alita 支持大部分 JSX 语法,这意味着什么呢?意味着你可以使用 React 自由的代码方式以及强大的组件化支持,意味着你可以延用自己的编程习惯,不需要对已有的 RN 代码进行过多修改。这主要得益于 Alita 是在运行时处理 JSX 语法,而不是现在社区上常见的编译时处理。

因此 Alita 没有诸如以下社区其他方案的限制:

JSX 只允许出现的组件的 render 方法中
不能通过 props 传递 JSX 片段或者返回 JSX 的函数
不支持在属性上传递函数

Alita 转换以下代码毫无压力:


2.jpg


生命周期

Alita 支持所有的 React 生命周期。微信小程序本身给组件提供了生命周期,但是这些生命周期在写法和调用上与 React 存在着一些的差异,另外 React 生命周期更加丰富。Alita 在支持 React 生命周期的时候,把它们分为了两类:

第一类: componentDidMount,componentDidUpdate,componentWillUnmount 这 3 个生命周期在微信小程序上有相应的触发时机,比如ready, detached,只需要在微信小程序相关回调触发的时候,调用 React 组件对应的方法即可。
另外一类,在微信小程序端没有直接对应的生命周期,对于这一类生命周期,主要是借助于 Alita 内部嵌入的 mini-react,触发相应的回调。通过这两种方式,Alita 实现了 React 生命周期的对齐。

此外,Alita 抹平了 RN 和微信小程序之间的事件及样式差异,能够无损得将 RN事件和样式传递到微信小程序中。

RN 基本组件和 API

RN 提供了很多基本的组件和 API,这些组件加上 React 开发方式,共同构成了 RN 应用。Alita 除了要对 React 语法进行处理,还必须在预先在微信小程序平台对齐出一套与 RN 等效的组件和 API。比如在 RN 端,请求网络的方式是通过 fetch 方式,但是微信小程序本身并不存在 fetch 方法,就这要求 Alita 必须基于微信小程序的网络 API,在微信小程序上实现一个 fetch 方法。 同样的以 RN 组件 FlatList 为例,当 Alita 把 RN 应用转化为微信小程序代码之后,FlatList 在微信小程序平台并不存在,需要预先在微信小程序平台实现小程序版本的 FlatList 。这个预先处理的过程,我们称之为对齐,对齐的过程包括组件,组件属性,API 等。


3.jpg


Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理,并且易于测试,是当前 React 技术栈流行的数据层管理方案。得益于 Alita 运行阶段处理 React 逻辑的设计,Alita 支持将使用 Redux 的 RN 应用转换成微信小程序。

动画

动画是每一个 app 不可或缺的能力,RN 和微信小程序的动画实现差异很大,RN 的动画能力要强于微信小程序,想要完全把 RN 的动画转化至微信小程序的是不可能的。为此我们封装了一套动画组件库,这一套动画组件库涵盖了所有微信小程序的动画能力,所有使用此动画库开发的动画,都可以无损转化到小程序端。
React Native


5.gif


微信小程序


6.gif


Alita 原理简介

那么 Alita 是如何将 RN 转换运行在微信小程序上的呢?我们不打算在这篇文章深入剖析,简单从编译阶段和运行阶段来说明。

编译阶段:我们通过静态分析 RN 源码,将其转换为微信小程序可以识别的代码,首先我们会将 JSX 语法转换为微信小程序的 wxml 模块语法,RN 组件在这个阶段会被转换为微信小程序自定义组件,一般会产生微信小程序需要的 4 个文件 wxml, js,json 和 wxss。 此外,我们会保留一份 babel 转译之后的 RN 源码,这份代码里面所有的 JSX 都已经由 React.createElement 替换,运行阶段,会使用这个能被微信小程序的 JavaScript 运行环境识别的源码。

运行阶段:Alita 内部嵌入了一个 mini-react,这个 mini-react 在运行阶段会运行上文所说的转译后的 RN 源码,与 React 一样,递归(React Fiber 之后,不再是递给的方式)的处理组件树,调用组件的 render 方法,调用组件生命周期,计算 context 等。另外 React 在运行的过程中有一个重要的 reconciliation 算法(即 virtual-dom),mini-react 同样提供了简化版本的 reconciliation 来决定组件的销毁与复用。mini-react 执行完之后,最终会输出一个描述视图的数据结构,这份数据结构提供了微信小程序渲染所需要的所有数据。微信小程序通过桥接模块与 mini-react 通信,获取到这一份数据,通过 setData 的方式设置到微信小程序模版上,从而渲染出视图。


7.jpg


Alita 组件库

在项目开发中,仅仅使用 RN 基本组件和 API,是很难满足需要的。我们在使用 Alita 的过程中,积累了很多常用的三端组件,包括ScrollTabView,ViewPager,SegmentedControl等等,我们正在剥离和梳理这些组件,很快会发布兼容三端的 Alita 组件库。此组件库也是我们日后的工作重点之一,我们将会不断优化和扩展新组件。

除了 Alita 组件库,我们还提供了扩展方式,开发者可以很方便的把本团队的基本 UI 组件库扩展到微信小程序端,然后通过 Alita 把使用了这些组件的 RN 应用运行在微信小程序平台。


8.jpg


结语

我们将不断拓展 Alita 的能力,支持更多端能力,如:百度小程序、头条小程序等,继续完善开发者体验,提高开发者效率,帮助更多开发者。

我们也在考察 Flutter 这一新的跨端方案和微信小程序融合转化的可行性。

我们十分重视开源社区的反馈和建议,会不断从中汲取养分,让 Alita 变得更加强大。

意见反馈

如果有任何的意见或者建议,欢迎在 Github 创建 issue,感谢你的支持和贡献。
  收起阅读 »

GitHub 开源跨平台神器 Electron 实践

认识 Electron Electron是由GitHub开发,用HTML、CSS 和 JavaScript来构建跨平台桌面应用程序的一个开源库。Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac、Windows...
继续阅读 »
认识 Electron

Electron是由GitHub开发,用HTML、CSS 和 JavaScript来构建跨平台桌面应用程序的一个开源库。Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac、Windows和Linux系统下的应用。Electron于2013年作为构建GitHub上可编程的文本编辑器Atom的框架而被开发出来。

这不意味着Electron是绑定了GUI库的JavaScript。相反,Electron使用Web页面作为它的GUI,所以你能把它看作成一个被JavaScript控制的,精简版的Chromium浏览器。

Electron的版本更新很频繁,基本保持在1周发布一个小版本,每季度发布一个大版本。除了稳定版外还有Beta版和Nightly(最新功能试用版),Chromium更新时,Electron也会跟着更新。

为什么选择Electron

如今的桌面应用软件基本都需要跨平台运行,类似于MFC、Duilib等技术都无法满足需求。当今的跨平台桌面应用软件开发以使用QT,Electron较多。

QT跨平台开发

Qt是一个跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。作为使用C++语言开发的框架,他的优缺点十分明显。

优点:

运行效率高;

架构健壮,性能强大。

缺点:

开发周期长;

需要开发者具有C++编程能力;

QT是一款收费软件,如果不想缴费购买License,又想用QT开发商业(闭源)程序,必须遵守LGPL协议,开源使用了LGPL库的源代码。

Electron桌面软件开发

Electron最早用于开发GitHub上的可编程文本编辑器Atom,它是一个借助Node.js和Chromium, 利用HTML/CSS/JavaScript语言创建桌面应用的框架。与之类似的还有NW.js, 但是NW.js社区发展基本处于停滞状态,更新也较慢。

优点:

使用JavaScript语言作为开发语言,方便前端开发者轻松开发桌面应用,原C++/Java语言开发者,也可以很快入手开发;

方便调试,提供了浏览器的开发者工具,轻松断点调试;

丰富的Web前端UI资源,可以快速制作绚丽的界面;

快速构建,迭代开发。最复杂的底层浏览器部分Electron已经帮你搞定,你只需要负责上层界面及业务逻辑的开发。Electron还提供了热更新功能,只需加载更新模块,会自动帮你检查更新并后台下载;

崩溃日志报告。轻松收集崩溃日志,定位错误代码;

C++插件扩展;

代码开源。Electron是GitHub上的开源项目,开发者有疑问可以在GitHub社区(https://github.com/electron/electron)上直接提issue,高级开发者可以修改Electron底层代码,订制自己的Elcetron。

缺点:

打包文件太大。Electron毕竟是一个浏览器,最小的应用安装包也要几十兆大小;

无法代码加密。和Web开发类似,使用者可以在开发者工具看到应用的客户端代码,商业软件需要代码加密的可以选择重要功能在服务端实现,桌面应用请求,或使用Node文件实现;

运行耗资源。浏览器通病,Electron应用也是多进程系统,启动几个Electron应用还好,如果太多会造成机器卡顿;

不支持XP系统,Node.js并不支持XP系统。

综上,如果你想快速的开发出炫酷的桌面应用,而又对系统限制不大,建议你选择Electron,如果你是一个前端开发人员,又想制作桌面应用,建议你选择Electron。

创建一个简单的应用

环境安装

Electron应用本质上是一个Node.js应用程序,需要安装Node.js,到官网(http://Node.js.cn/download/)安装即可。安装完后,在命令行窗口中分别输入node -v和npm -v来查看Node和NPM的版本。

初始化应用
与Node.js模块相同,应用的入口为package.json 文件,该文件可以在一个文件夹下使用npm init命令,按照提示填充各项信息生成。 一个最基本的Electron 应用一般来说会有如下的目录结构:


1.png


 main.js是主进程,完成窗口的创建,url或html文件的加载。GitHub上提供了一个简单的Electron应用https://github.com/electron/electron-quick-start.git,可供学习参考。

使用C++插件扩展功能
对于复杂的业务逻辑,可以开发成C++插件Node,C++插件主要完成一些复杂的逻辑功能,供Electron调用。Electron对于C++生成的Node插件引用功能来自于Node.js,可以使用require() 函数加载到工程中,像普通的模块一样使用。JavaScript 与C++ 库之间接口使用V8引擎,如下图所示:


2.jpg


 插件开发环境

C++插件的开发需要安装node-gyp、Python 2.76,Windows下开发还要安装Visual Studio。
每个插件都有一个工程文件binging.gyp,配置了源文件、include路径及链接库,目标文件,使用的编译器等,格式如下:


3.png


 
C++与JavaScript通过V8交互执行的整体过程如下图所示:


4.jpg


 
C++可以使用Napi接口,模块的加载使用宏NODEAPIMODULE(hello, Init),导出的JavaScript接口在Init中定义,示例如下:


5.png


 
编译C++插件使用命令如下:


6.png


 
生成的C++插件为node文件,如hello.node 在JavaScript中调用C++插件直接使用require函数,代码hello.js如下:


7.png


 JavaScript就可以调用C++的接口了,执行命令node hello.js,输出"world"。

C++中调用JavaScript传递的回调函数需要使用libuv库,libuv实现了Node.js的事件循环、工作线程、以及平台所有的的异步操作的C库。 具体参考示例代码https://github.com/nodejs/node-addon-examples

Electron打包

Electron应用打包可以使用electron-builder和electron-packager,推荐使用electron-builder,打包命令为npm run builder,可以使用参数配置生成的安装包的操作系统。

环信IM桌面端

环信的IM桌面端SDK提供了JavaScript接口,并且使用Electron框架开发的示例Demo,可以让任何一个前端人员在极短时间内搭建出一款同时在Mac、Windows上运行的即时通讯软件,拥有单聊、群聊和聊天室功能,支持文字、表情、图片、音视频等消息格式,开发时间短、界面美观,可以为开发者提供方便快捷的桌面端即时通讯解决方案。

下载地址:http://www.easemob.com/download/im
集成说明:http://docs-im.easemob.com/im/pc/intro/integration

作者:李小明,现就职于环信,高级软件开发工程师,负责IM桌面端软件的研发,以C++、Node.js为开发语言,从事多年桌面软件开发经验,对行业前沿技术永远不懈追求。 收起阅读 »

收藏了~阿里巴巴程序员常用的 15 款开发者工具

从人工到自动化,从重复到创新,技术演进的历程中,伴随着开发者工具类产品的发展。 阿里巴巴将自身在各类业务场景下的技术积淀,通过开源、云上实现或工具等形式对外开放,本文将精选了一些阿里巴巴的开发者工具,希望能帮助开发者们提高开发效率、更优雅的写代码。 由于开...
继续阅读 »
从人工到自动化,从重复到创新,技术演进的历程中,伴随着开发者工具类产品的发展。

阿里巴巴将自身在各类业务场景下的技术积淀,通过开源、云上实现或工具等形式对外开放,本文将精选了一些阿里巴巴的开发者工具,希望能帮助开发者们提高开发效率、更优雅的写代码。

由于开发者涉及的技术领域众多,笔者仅从自己熟悉的领域,以后端开发者的视角盘点平时可能用得到的工具。每个工具按照以下几点进行介绍:

工具名称和简介

使用场景

使用教程

获取方式

一、Java 线上诊断工具 Arthas

Arthas 阿里巴巴 2018 年 9 月开源的一款 Java 线上诊断工具。

工具的使用场景:

这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?

我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

是否有一个全局视角来查看系统的运行状况?

有什么办法可以监控到 JVM 的实时运行状态?

Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

使用教程:

基础教程:

https://alibaba.github.io/arthas/arthas-tutorials?language=cn&amp;id=arthas-basics

进阶教程:

https://alibaba.github.io/arthas/arthas-tutorials?language=cn&amp;id=arthas-advanced

获取方式:(免费)

开源地址:

https://github.com/alibaba/arthas

二、IDE 插件 Cloud Toolkit

Cloud Toolkit是一款 IDE 插件,可以帮助开发者更高效地开发、测试、诊断并部署应用。通过 Cloud Toolkit,开发者能够方便地将本地应用一键部署到任意机器(本地或云端),并内置 Arthas 诊断、高效执行终端命令和 SQL 等,提供 IntelliJ IDEA 版,Eclipse 版,PyCharm 版和 Maven 版。

工具的使用场景:

每次修改完代码后,是否正在经历反复地打包?

在 Maven 、Git 以及其他运维脚本和工具的之间频繁切换?

采用 SCP 工具上传?使用 XShell 或 SecureCRT 登陆服务器?替换部署包?重启?

文件上传到服务器指定目录,在各种 FTP、SCP 工具之间频繁切换 ?

使用教程:

IntelliJ IDEA 版:

https://help.aliyun.com/document_detail/98762.html

Eclipse 版:

https://help.aliyun.com/document_detail/29970.html

PyCharm 版:

https://help.aliyun.com/document_detail/112740.html

Maven 版:

https://help.aliyun.com/document_detail/108682.html

获取方式:(免费) 工具地址:

https://www.aliyun.com/product/cloudtoolkit

三、混沌实验注入工具 ChaosBlade

ChaosBlade是一款遵循混沌工程实验原理,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具,可实现底层故障的注入,提供了延迟、异常、返回特定值、修改参数值、重复调用和 try-catch 块异常等异常场景。

工具的使用场景:

微服务的容错能力不易衡量?

容器编排配置是否合理无法验证?

PaaS 层健壮性的测试工作无从入手?

使用教程:

https://github.com/chaosblade-io/chaosblade/wiki/ 新手指南

获取方式:(免费)

开源地址:

https://github.com/chaosblade-io/chaosblade/wiki/ 新手指南

四、Java 代码规约扫描插件

该插件用于检测 Java 代码中存在的不规范的位置,并给予提示。规约插件是采用 Kotlin 语言开发。

使用教程:

IDEA 插件使用文档:

https://github.com/alibaba/p3c/wiki/IDEA 插件使用文档

Eclipse 插件使用文档:

https://github.com/alibaba/p3c/wiki/Eclipse 插件使用文档

获取方式:(免费)

开源地址:

https://github.com/alibaba/p3c

五、应用实时监控工具 ARMS

ARMS是一款 APM 类的监控工具,提供前端、应用、自定义监控 3 类监控选项,可快速构建实时的应用性能和业务监控能力。

工具的使用场景:

晚上 10 点收到 37 条报警信息,你却无从下手?

当我们发现问题的时候,客户 / 业务方已经发起投诉?

每个月花几十万买服务器,却无法保障用户体验?

使用教程:

前端监控接入:

https://help.aliyun.com/documentdetail/106086.html

应用监控接入:

https://help.aliyun.com/documentdetail/63796.html

自定义监控:

https://help.aliyun.com/document_detail/47474.html

获取方式:(收费)

工具地址:

https://www.aliyun.com/product/arms

六、静态开源站点搭建工具 Docsite

Docsite一款集官网、文档、博客和社区为一体的静态开源站点的解决方案,具有简单易上手、上手不撒手的特质,同时支持 react 和静态渲染、PC 端和移动端、支持中英文国际化、SEO、markdown 文档、全局站点搜索、站点风格自定义、页面自定义等功能。

使用教程:

https://docsite.js.org/zh-cn/docs/installation.html

获取方式:(免费)

项目地址:

https://github.com/txd-team/docsite

七、Android 平台上的秒级编译方案 Freeline

Freeline 可以充分利用缓存文件,在几秒钟内迅速地对代码的改动进行编译并部署到设备上,有效地减少了日常开发中的大量重新编译与安装的耗时。Freeline 最快捷的使用方法就是直接安装 Android Studio 插件。

使用教程:

https://github.com/alibaba/freeline/blob/master/README-zh.md

获取方式:(免费)

项目地址:

https://github.com/alibaba/freeline

八、性能测试工具 PTS

PTS可以模拟大量用户访问业务的场景,任务随时发起,免去搭建和维护成本,支持 JMeter 脚本转化为 PTS 压测,同样支持原生 JMeter 引擎进行压测。

使用教程:

https://help.aliyun.com/document_detail/70290.html

获取方式:(收费)

工具地址:

https://www.aliyun.com/product/pts

九、云效开发者工具 KT

KT 可以简化在 Kubernetes 下进行联调测试的复杂度,提高基于 Kubernetes 的研发效率。

使用教程:

https://yq.aliyun.com/articles/690519

获取方式:(免费)

工具地址:

https://yq.aliyun.com/download/3393

十、架构可视化工具 AHAS

AHAS为 K8s 等容器环境提供了架构可视化的功能,同时,具有故障注入式高可用能力评测和一键流控降级等功能,可以快速低成本的提升应用可用性。

工具的使用场景:

服务化改造过程中,想精确的了解资源实例的构成和交互情况,实现架构的可视化?

想引入真实的故障场景和演练模型?

低门槛获得流控、降级功能?

使用教程:

https://help.aliyun.com/document_detail/90323.html

获取方式:(免费)

工具地址:

https://www.aliyun.com/product/ahas

十一、数据处理工具 EasyExcel

EasyExcel 是一个用来对 Java 进行解析、生成 Excel 的框架,它重写了 poi 对 07 版 Excel 的解析,原本一个 3M 的 Excel 用 POI sax 需要 100M 左右内存,EasyExcel 可降低到 KB 级别,并且再大的 excel 也不会出现内存溢出的情况。03 版依赖 POI 的 sax 模式。在上层做了模型转换的封装,让使用者更加简单方便。

使用教程:

https://github.com/alibaba/easyexcel/blob/master/quickstart.md

获取方式:(开源)

https://github.com/alibaba/easyexcel

十二、iOS 类工具 HandyJSON

HandyJSON 是一个用于 Swift 语言中的 JSON 序列化 / 反序列化库。

与其他流行的 Swift JSON 库相比,HandyJSON 的特点是,它支持纯 Swift 类,使用也简单。它反序列化时 (把 JSON 转换为 Model) 不要求 Model 从 NSObject 继承 (因为它不是基于 KVC 机制),也不要求你为 Model 定义一个 Mapping 函数。只要你定义好 Model 类,声明它服从 HandyJSON 协议,HandyJSON 就能自行以各个属性的属性名为 Key,从 JSON 串中解析值。

使用教程:

https://github.com/alibaba/HandyJSON/blob/master/README_cn.md

获取方式:(开源)

https://github.com/alibaba/HandyJSON

十三、云上资源和应用部署工具 EDAS Serverless

EDAS Serverless一款基于 Kubernetes,面向应用和微服务的 Serverless 平台。用户无需管理和维护集群与服务器,即可通过镜像、WAR 包和 JAR 包,快速创建原生支持 Kubernetes 的容器应用,同时支持 Spring Cloud 和 Dubbo 等主流微服务框架。

使用教程:

https://help.aliyun.com/document_detail/102048.html

获取方式:(公测期间免费)

https://help.aliyun.com/document_detail/97792.html

十四、数据库连接池 Druid

Druid 是 Java 语言下的数据库连接池,它能够提供强大的监控和扩展功能。

使用教程:

https://github.com/alibaba/druid/wiki/ 常见问题

获取方式:(开源)

http://central.maven.org/maven2/com/alibaba/druid/

十五、Java 工具集 Dragonwell

Alibaba Dragonwell 是阿里巴巴内部 OpenJDK 定制版 AJDK 的开源版本, AJDK 为在线电商,金融,物流做了结合业务场景的优化,运行在超大规模的,100,000+ 服务器的阿里巴巴数据中心。 Alibaba Dragonwell 与 Java SE 标准兼容,目前仅支持 Linux/x86_64 平台。

使用教程:

https://github.com/alibaba/dragonwell8/wiki/ 阿里巴巴 Dragonwell8 用户指南

获取方式:(开源)

https://github.com/alibaba/dragonwell8


上一篇: Java首度承认PK失败,愿永久服软Python!
  收起阅读 »

史上最完整的官方Oracle OCP中文文教材,快来下载吧!!

内含文件: 1、Oracle Database 11g:SQL 基础 学生指南第1 册  2、Oracle Database 11g:SQL 基础 学生指南第2 册  3、Oracle Database 11g:数据库管理- 课堂练习I 学生指南第1 册...
继续阅读 »
内含文件:
1、Oracle Database 11g:SQL 基础 学生指南第1 册 
2、Oracle Database 11g:SQL 基础 学生指南第2 册 
3、Oracle Database 11g:数据库管理- 课堂练习I 学生指南第1 册
4、Oracle Database 11g:数据库管理- 课堂练习II 学生指南第1 册 
5、Oracle Database 11g:数据库管理- 课堂练习I 学生指南第2 册 
6、Oracle Database 11g:数据库管理- 课堂练习II 学生指南第2 册 
7、Oracle Da等等 收起阅读 »

手!慢!无!价值1980的数据分析教程,终终终于免费啦!!!

对比互联网各个岗位的裁员程度可以发现,数据分析相关岗位正在不断的扩招,已经成为了这波逆流中的黑马,什么原因导致的数据分析人才如此紧缺? 因为数据分析是大势所趋,未来的发展空间会大有可为。随着5G网络即将商用,企业每天将会产生海量的数据,BAT日均数据更是达到了...
继续阅读 »
对比互联网各个岗位的裁员程度可以发现,数据分析相关岗位正在不断的扩招,已经成为了这波逆流中的黑马,什么原因导致的数据分析人才如此紧缺?
因为数据分析是大势所趋,未来的发展空间会大有可为。随着5G网络即将商用,企业每天将会产生海量的数据,BAT日均数据更是达到了PB的级别,数据分析相关岗位才会存在着巨大的需求缺口。
长此以往,企业要用尽可能少的人才,来满足尽可能多岗位的诉求,可以这么说,数据分析将会是每个程序员个人能力最重要的补充,也是BAT这类大公司急招人才的必备技能。
但是一提数据分析,很多人就觉得无从下手,知识点零散总是抓不住重点,学习起来相当吃力。这有一份廖雪峰大神历时3个月打磨出来的《数据分析必备技能》的视频学习资料,由浅入深系统化的讲解,内容详尽。基本囊括了平时学习工作中经常用到的分析方式,这份不可或缺的宝贵资料原价值1980元,现在,关注公众号cainiao_xueyuan就可以免费领取(仅限300名)。

学完这套资料可以给你将会得到哪些收获?

1. 总时长>48个小时的干货内容,每天2小时,20天掌握数据分析必备技能;
2. 对照自己掌握知识点进行查缺补漏,帮助你扫除知识盲区、重构知识体系。
具体详细的资料内容:
1 数学理论基础           
01.数据挖掘之数学基础02.数学基础之微积分
03.机器学习之线性回归
04.机器学习之逻辑回归
05.朴素贝叶斯
06.机器学习之决策树
07.机器学习之集成学习
2 必备Python基础            
01.Python语言介绍、发展、特色02.概念介绍:Python解释器
03.Python函数及高级特性
04.交互环境介绍:启动和退出交互环境
05.Python基础语法及模块
3 高效scrapy爬虫框架           
01.scrapy简介02.scrapy选择器
03.创建scrapy爬虫
04.下载器与爬虫中间件
05.突破反爬虫机制与策略
06.使用管道 pipelines        
4 Excel数据处理            
01.认识数据表的字段和记录02.使用Excel制作数据表
03.指定常用数据类型
04.Excel导入网站数据、文本数据
05.Excel数据清洗、筛选
06.Excel数据抽样和计算

5 使用SQL实现数据操作
01SQL基础语法
02.SQL表连接
03.SQL普通函数
04.SQL窗口函数
05.SQL优化 

长按扫码 添加微信,领取干货视频


微信图片_20190507095431.jpg



Ps:学习资料由"开课吧"友情提供。 收起阅读 »

打击电信诈骗保障客户安全,环信客服云6大安全机制让骗子无所遁形!

近日,环信收到海淀网警和用户的反馈,部分不法分子通过注册环信客服云获取专业客服工具在各大论坛和二手交易平台:如百度贴吧、58同城“转转”等进行电信诈骗,不法分子冒充平台的专业客服人员诱导客户先行付款进行诈骗,造成了很恶劣的社会影响。 ...
继续阅读 »
近日,环信收到海淀网警和用户的反馈,部分不法分子通过注册环信客服云获取专业客服工具在各大论坛和二手交易平台:如百度贴吧、58同城“转转”等进行电信诈骗,不法分子冒充平台的专业客服人员诱导客户先行付款进行诈骗,造成了很恶劣的社会影响。


2.jpg




环信第一时间获悉即敏锐的采取行动,一期关停450个诈骗账号信息,同时推出6大安全机制让骗子无所遁形。

环信客服云防诈骗六大安全机制:

1.注册环节:单手机号15天内只能注册一个账号,以防反复账号行骗。

2.使用环节:关停H5聊天窗口,需申请审核后才能打开。

3.聊天环节:敏感词预警,在用户聊天记录过程中系统监控到提前设置的关键词,会触发预警给到运营人员,进行账户关停。

4.账号限制:实时操作异常账号关闭,一经发现,立即关停账号

5.黑名单机制:加入黑名单的注册手机号,永久无法再注册环信

6.报警机制:第一时间反馈所有诈骗信息至公安网警等相关部门

同时,环信也开通了反诈骗投诉信箱:Antifraud@easemob.com 和 400专线:400-622-1776,我们的工作人员会第一时间解决所有用户碰到的所有潜在欺诈问题。

维护网络安全繁荣是环信义不容辞的社会责任,环信会一如既往和网警网安等部门密切合作,打击电信诈骗,保护客户安全,环信客服云一直在路上! 收起阅读 »

同一个网站,手机端跟电脑端显示不同是怎么实现的?

同一个网站,手机端跟电脑端不同是怎么实现的? 常见的方式有三种: 1,自适应网站 同一套代码,自动实现手机端和电脑端的布局自动调整。例如:openGPS.cn 网站现在大部分页面已经支持自适应展示,手机端电脑端都可以访问本站内容,正常阅读。自适应站点,往...
继续阅读 »
同一个网站,手机端跟电脑端不同是怎么实现的?

常见的方式有三种:

1,自适应网站

同一套代码,自动实现手机端和电脑端的布局自动调整。例如:openGPS.cn 网站现在大部分页面已经支持自适应展示,手机端电脑端都可以访问本站内容,正常阅读。自适应站点,往往是对CSS布局的重点考虑,本站使用的是BootStrap这个前端样式组件实现的自适应布局。

2,网站二级目录

这种是早期网站比较喜欢的做法,因为其实这是一个网站。早期网站往往是使用虚拟主机(也叫空间)发布,一个空间只能放一个网站,所以这种做法在早期特别流行。这种结构本质还是一个网站,但是针对手机电脑客户端单独做了往往对应的一套目录,例如:

电脑站点地址一般是:www.domain.com/xxxxxxx

手机站点地址往往是:www.domain.com/m/xxxxxxx

3,手机站点使用二级域名,电脑手机各一套2套站点代码


这种做法,工作量跟二级目录基本相似,严格来说代码量稍微多点。由于是2套代码,所以发布时候也得配备2套域名,不过要求两套站点连接同一个数据库来实现数据统一。例如:

电脑端网站域名是:www.domain.com

手机端网站域名是:m.domain.com




原文地址: https://www.opengps.cn/Blog/View.aspx?id=302 文章的更新编辑依此链接为准。欢迎关注源站原创文章!
  收起阅读 »

在微信小程序里实现聊天室

第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。 花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。   准备工作 下载环信 小程序demo+sdkgit clone https://github.com/easemob/w...
继续阅读 »
第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
  1. 下载环信 小程序demo+sdk
    git clone https://github.com/easemob/webim-weixin-xcx
  2. 创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明

    ml.png


集成
  1. 登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改

    login.png

  2. 在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用

    tz.png

  3. 修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室

    getroom.png

    然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉
  4. 然后在roomlist.wxml 修改对应的 变量绑定名称

    listui.png


    list.png

  5. demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面

    joinrom.png

    Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧
  6. 到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了

    send.png


    chat.png

    就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx

收起阅读 »

(客服云)iOS访客端集成常见问题(非报错)

1、UI上很多地方显示英文,比如聊天页面的工具栏 把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。   2、进入聊天页面没有加载聊天记录 这种情况一般出现在只使用了 HDM...
继续阅读 »
1、UI上很多地方显示英文,比如聊天页面的工具栏

显示英文1.png


把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。

显示英文2.png


 
2、进入聊天页面没有加载聊天记录
这种情况一般出现在只使用了 HDMessageViewController 没有使用 HDChatViewController 的时候
在HDMessageViewController 的 viewDidLoad 方法中, 将 [self tableViewDidTriggerHeaderRefresh]; 的注释打开,再在这句代码之前加上 self.showRefreshHeader = YES; 
 
3、发送表情却显示字符串

访客端表情符号.png


把下面这段代码添加到appdelegate中就可以了
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapePattern:@"\\[[^\\[\\]]{1,3}\\]"];
[[HDEmotionEscape sharedInstance] setEaseEmotionEscapeDictionary:[HDConvertToCommonEmoticonsHelper emotionsDictionary]];
 
4、文本消息,收发双方的布局不一样,如图

文本消息布局错误1.png


参考一下截图修改即可

文本消息布局错误2.png



5、客服能收到访客的消息,访客收不到客服的消息
(1)客服和im同时使用的话,初始化sdk、登录、登出用的是im的api会出现这种情况。必须使用客服的api。
(2)IM sdk升级为客服sdk,不兼容导致的,这种情况可以线上发起会话咨询。
      
6、发送的消息,出现在聊天页面的左侧
一般是由于当前访客没有登录或者登录失败,断点仔细检查下。

7、修改聊天页面导航栏标题
修改_title的值


收起阅读 »

(客服云)iOS访客端集成常见报错(总有一款适合你)

注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。   1、很多同学在首次“导入...
继续阅读 »
注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。
 
1、很多同学在首次“导入SDK”或“更新SDK重新导入SDK”后,Xcode运行报以下的error:
dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate
  Referenced from: /Users/shenchong/Library/Developer/CoreSimulator/Devices/C768FE68-6E79-40C8-8AD1-FFFC434D51A9/data/Containers/Bundle/Application/41EA9A48-4DD5-4AA4-AB3F-139CFE036532/CallBackTest.app/CallBackTest
  Reason: image not found
       这个原因是工程未加载到 framework,正确的处理方式是在TARGETS → General → Embedded Binaries 中添加HelpDesk.framework和Hyphenate.framework依赖库,且 Linked Frameworks and Libraries中依赖库的Status必须是Required。

1访客端_image_not_found.png


 
2、运行之后,自变量为nil,这就有可能是因为上面所说的依赖库的status设置为了Optional,需要改成Required。

2访客端自变量为nil.png


 
3、打包后上传到appstore报错
(1)ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at 'Payload/toy.app/HelpDeskUIResource.bundle' does not contain a bundle executable. If this bundle intentionally does not contain an executable, consider removing the CFBundleExecutable key from its Info.plist and using a CFBundlePackageType of BNDL. If this bundle is part of a third-party framework, consider contacting the developer of the framework for an update to address this issue."
方法:把HelpDeskUIResource.bundle里的Info.plist删掉就即可。

3访客端打包90535.png


(2)This bundle is invalid. The value for key CFBundleShortVersionString ‘1.2.2.1’in the Info.plist must be a period-separated list of at most three non-negative integers. 

4访客端打包90060.png


把sdk里的plist文件的版本号改成3位数即可

5访客端打包1.2_.2_.1位置_.png


(3)Invalid Mach-O Format.The Mach-O in bundle “SMYG.app/Frameworks/Hyphenate.framework” isn’t consistent with the Mach-O in the main bundle.The main bundle Mach-O contains armv7(bitcode) and arm64(bitcode),while the nested bundle Mach-O contains armv7(machine code) and arm64(machine code).Verify that all of the targets for a platform have a consistent value for the ENABLE_BITCODE build setting.”

6访客端打包90636.png


将TARGETS-Build Settings-Enable Bitcode改为NO

7访客端打包bitcode改为NO.png


(4)还有很多同学打包失败,看不出什么原因

8访客端打包需剔除.png


那么可以先看看有没有按照文档剔除x86_64 i386两个平台
文档链接:http://docs.easemob.com/cs/300visitoraccess/iossdk#%E4%B8%8A%E4%BC%A0appstore%E4%BB%A5%E5%8F%8A%E6%89%93%E5%8C%85ipa%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
 
4、那么剔除x86_64 i386时会遇到can't open input file的错误,这是因为cd的路径错误,把“/HelpDesk.framework”删掉。是cd到framework所在的路径,不是cd到framework

9访客端剔除cd错误.png


 
5、下图中的报错,需要创建一个pch文件,并且在pch文件添加如下判断,将环信的和自己的头文件都引入到#ifdef内部,参考文档:iOS访客端sdk集成准备工作
   #ifdef __OBJC__
   #endif
(swift项目也需这样操作)

10pch加判断1.png



11pch加判断2.png



pch加判断3.png



6、集成环信HelpDeskUI的时候,由于HelpDeskUI内部使用了第三方库,如果与开发者第三方库产生冲突,可将HelpDeskUI中冲突的第三方库删除,如果第三方库中的接口有升级的部分,请酌情进行升级。

12第三方库冲突.png


 
7、集成1.2.2版本demo中的HelpDeskUI,Masonry报错:Passing ‘CGFloat’(aka ‘double’) to parameter of incompatible type ‘__strong id’
需要在pch中添加#define MAS_SHORTHAND_GLOBALS
注意:要在#import "Masonry.h"之前添加此宏定义

13访客端Masonry报错.png


 
8、Xcode11运行demo,PSTCollectionView第三方库会有如下报错

iOS13中PSTCollectionView报错.png


标明下类型就行了,如图

iOS13中PSTCollectionView报错1.png


 
9、Xcode12.3编译报错(Building for iOS, but the linked and embedded framework......)

xcode12.3报错1_.jpg
解决方案:

 或者打开xcode,左上方点击File --- Workspace Settings,按照截图修改试下(不建议)

xcode12.3报错2_.jpg


 
收起阅读 »

(客服云)iOS访客端怎么判断会话是否结束

1、联系商务开通【会话创建、接起、结束】功能 2、在 cmdMessagesDidReceive 方法中做如下判断,返回ServiceSessionClosedEvent,则是会话已结束,如图: 代码: if ([message.body isKin...
继续阅读 »
1、联系商务开通【会话创建、接起、结束】功能
2、在 cmdMessagesDidReceive 方法中做如下判断,返回ServiceSessionClosedEvent,则是会话已结束,如图:

判断会话是否结束.png


代码:
if ([message.body isKindOfClass:[EMCmdMessageBody class]]) {
EMCmdMessageBody *_bb = (EMCmdMessageBody *)message.body;
if ([_bb.action isEqualToString:@"ServiceSessionCreatedEvent"]) {
NSLog(@"hhhhh--Creat");
} else if ([_bb.action isEqualToString:@"ServiceSessionOpenedEvent"]) {
NSLog(@"hhhhh--Open");
} else if ([_bb.action isEqualToString:@"ServiceSessionClosedEvent"]) {
NSLog(@"hhhhh--Close");
}
}
  收起阅读 »

(客服云)iOS访客端点击订单消息

1、在HDMessageCell.m 的 - (void)_setupSubviewsWithType:(EMMessageBodyType)messageType                       isSender:(BOOL)isSender  ...
继续阅读 »
1、在HDMessageCell.m 的
- (void)_setupSubviewsWithType:(EMMessageBodyType)messageType
                      isSender:(BOOL)isSender
                         model:(id)model
方法中给orderBgView 添加手势

点击订单消息1.png

UITapGestureRecognizer *tapRecognizer3 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(orderImageViewTapAction:)];
[_bubbleView.orderBgView addGestureRecognizer:tapRecognizer3];
 
2、在HDMessageCell.m 中添加手势点击事件

点击订单消息2.png

- (void)orderImageViewTapAction:(UITapGestureRecognizer *)tapRecognizer
{
if ([_delegate respondsToSelector:@selector(messageCellSelected:)]) {
[_delegate messageCellSelected:_model];
}
}
 
3、在HDMessageViewController的 
   - (void)messageCellSelected:(id)model 方法中添加订单消息的判断

点击订单消息3.png

代码:
if ([HDMessageHelper getMessageExtType:model.message] == HDExtOrderMsg) {
// 订单消息携带的扩展
NSDictionary *dic = model.message.ext;
NSLog(@"点击了订单消息");
}
收起阅读 »

(客服云)IOS访客端设置访客昵称头像

1.在HDMessageViewController.h 中添加访客昵称、头像的属性 // 访客昵称 @property (nonatomic, strong) NSString *sendName; // 访客头像(url) @property (n...
继续阅读 »
1.在HDMessageViewController.h 中添加访客昵称、头像的属性

1.png

// 访客昵称
@property (nonatomic, strong) NSString *sendName;
// 访客头像(url)
@property (nonatomic, strong) NSString *sendAvatarUrl;
// 访客头像(本地图片)
@property (nonatomic, strong) UIImage *sendAvatarImage;
 
2.在HDMessageViewController.m   - (NSArray *)formatMessages:(NSArray *)messages 方法中添加判断

2.png

if (isSender) {
if (self.sendName) {
model.nickname = self.sendName;
}
// 加载网络头像
if (self.sendAvatarUrl) {
model.avatarURLPath = self.sendAvatarUrl;
}
// 加载本地头像
if (self.sendAvatarImage) {
model.avatarImage = self.sendAvatarImage;
model.avatarURLPath = nil;
}
}
 
3.在初始化聊天页面的时候,传入访客的昵称、头像即可。
(可选择url或者本地头像图片)

3.png

ctrl.sendName = @"访客昵称";
ctrl.sendAvatarImage = [UIImage imageNamed:@"测试图片"];
// chat.sendAvatarUrl = @"";
收起阅读 »

(客服云)iOS访客端设置客服系统头像(调度员消息显示企业logo)

0、在客服系统内 管理员模式--设置--企业基本信息 处上传企业logo    在 管理员模式--设置--系统开关--系统开关--访客端显示客服昵称 处打开开关 1、在HDIMessageModel.h 中添加客服系统头像url属性 ...
继续阅读 »
0、在客服系统内 管理员模式--设置--企业基本信息 处上传企业logo   

客服系统头像1.png


管理员模式--设置--系统开关--系统开关--访客端显示客服昵称 处打开开关

客服系统头像2.png



1、在HDIMessageModel.h 中添加客服系统头像url属性

客服系统头像3.png

@property (strong, nonatomic) NSString *officialAccountURL;
 
2、在HDMessageModel.h 中添加客服系统头像url属性

客服系统头像4.png

@property (strong, nonatomic) NSString *officialAccountURL;
 
3、在HDMessageModel.m类  - (instancetype)initWitMessage:(HDMessage *)message方法中添加  

客服系统头像5.png

NSDictionary *officialAccount = [NSDictionary dictionary];
if ([weichat objectForKey:@"official_account"]) {
officialAccount = [weichat valueForKey:@"official_account"];
if ([officialAccount objectForKey:@"img"]) {
self.officialAccountURL = [[@"https:" stringByAppendingString:[officialAccount objectForKey:@"img"]] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
}
 
4、在HDBaseMessageCell.m类  - (void)setModel:(id)model方法中 修改代码 
(“系统消息”要改成您自己客服系统中设置的调度员昵称)

客服系统头像6.png

if (model.avatarURLPath) {
if (model.nickname) {
if ([model.nickname isEqualToString:@"系统消息"]) {
if (model.officialAccountURL) {
[self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.officialAccountURL] placeholderImage:model.avatarImage];
}
} else {
[self.avatarView sd_setImageWithURL:[NSURL URLWithString:model.avatarURLPath] placeholderImage:model.avatarImage];
}
}

} else {
self.avatarView.image = model.avatarImage;
}
收起阅读 »

(客服云)iOS访客端获取机器人欢迎语

注意: 0、代码中有两个拼接的url显示不全,我在评论有补充。 1、此文档只支持获取单机器人的欢迎语,多机器人会获取第一个机器人的欢迎语。 2、此文档暂不支持获取多媒体和图文消息类型的机器人欢迎语。 3、老版机器人与新版机器人集成方法不同,集成前需区分好。 ...
继续阅读 »
注意:
0、代码中有两个拼接的url显示不全,我在评论有补充。
1、此文档只支持获取单机器人的欢迎语,多机器人会获取第一个机器人的欢迎语。
2、此文档暂不支持获取多媒体和图文消息类型的机器人欢迎语。
3、老版机器人与新版机器人集成方法不同,集成前需区分好。

机器人欢迎语1.png



机器人欢迎语2.png


 
一、先到客服系统配置机器人欢迎语
1、老版机器人设置方案:
管理员模式--智能机器人--机器人设置--自动回复--欢迎语,开启开关并添加欢迎语

机器人欢迎语3.png


2、新版(企业版)机器人设置方案:
(1)管理员模式--智能机器人--机器人设置--基础设置,点击“机器人管理”跳转到机器人管理页面
(2)机器人设置--自动回复--欢迎语,开启开关并添加欢迎语

机器人欢迎语4.png


 
二、iOS端获取、解析机器人欢迎语
在HDMessageViewController.m类的 -(void)viewDidLoad 方法最后调用robotWelcome或者newRobotWelcome 方法即可。其余客户端插入消息的逻辑自行处理
//获取老版机器人欢迎语
- (void)robotWelcome
{
// kDefaultTenantId:租户id
// kDefaultCustomerName:IM服务号
NSString *urlStr = [NSString stringWithFormat:@"https://kefu.easemob.com/v1/Tenants/%@/robots/visitor/greetings/app", kDefaultTenantId];
NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:newStr];
NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
//异步链接(形式1,较少用)
[NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

// 解析
NSString *result =[[ NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//同样的可以替换字符
NSLog(@"result-----%@", result);
NSString *str = [result stringByReplacingOccurrencesOfString:@""" withString:@"\""];
NSString *str1 = [str stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];
NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];
// JSON字符串转字典
NSDictionary *dic = [self dictionaryWithJsonString:str2];
// 取消息的ext
NSLog(@"dic---%@",dic);

NSString *robotText = nil;
NSDictionary *dicExt = [NSDictionary dictionary];
if ([[dic objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {

robotText = [dic objectForKey:@"greetingText"];
} else {
dicExt = [[dic objectForKey:@"greetingText"] objectForKey:@"ext"];
}

//构建消息
EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
NSString *from = [[HDClient sharedClient] currentUsername];
HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
message.ext = dicExt;
message.direction = 1;
message.status = HDMessageStatusSuccessed;
// 消息添加到UI
[self addMessageToDataSource:message progress:nil];
// 消息插入到会话
HDError *pError;
[self.conversation addMessage:message error:&pError];
}];
}



//获取新版(企业版)机器人欢迎语
- (void)newRobotWelcome
{
[[HDClient sharedClient] accessToken];
// 以下信息换成自己的
// kDefaultTenantId:租户id
// kDefaultOrgName:appkey中#的前半部分
// kDefaultAppName:appkey中#的后半部分
// kDefaultCustomerName:IM服务号
NSString *imToken = [HDClient sharedClient].accessToken;
NSString *urlStr = [NSString stringWithFormat:@"https://kefu.easemob.com/v1/webimplugin/tenants/robots/welcome?channelType=easemob&originType=app&tenantId=%@&orgName=%@&appName=%@&userName=%@&token=%@",kDefaultTenantId, kDefaultOrgName, kDefaultAppName, kDefaultCustomerName, imToken];
NSString *newStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"newStr---%@", newStr);
NSURL *url = [NSURL URLWithString:newStr];
NSURLRequest *requst = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
//异步链接(形式1,较少用)
[NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// 解析
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//同样的可以替换字符
NSString *str = [result stringByReplacingOccurrencesOfString:@"&" withString:@"&"];

NSString *str1 = [str stringByReplacingOccurrencesOfString:@""" withString:@"\""];

NSString *str2 = [str1 stringByReplacingOccurrencesOfString:@"\"{" withString:@"{"];

NSString *str3 = [str2 stringByReplacingOccurrencesOfString:@"}\"" withString:@"}"];

// JSON字符串转字典
NSDictionary *dic = [self dictionaryWithJsonString:str3];
// 取消息的ext
NSString *robotText = nil;
NSDictionary *dicExt = [NSDictionary dictionary];
if ([[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] isKindOfClass:[NSString class]]) {
robotText = [[dic objectForKey:@"entity"] objectForKey:@"greetingText"];
} else {
dicExt = [[[dic objectForKey:@"entity"] objectForKey:@"greetingText"] objectForKey:@"ext"];
}
//构建消息
NSLog(@"dicExt---%@",dicExt);
EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:robotText];
NSString *from = [[HDClient sharedClient] currentUsername];

HDMessage *message = [[HDMessage alloc] initWithConversationID:kDefaultCustomerName from:from to:kDefaultCustomerName body:bdy];
message.ext = dicExt;
message.direction = 1;
message.status = HDMessageStatusSuccessed;
// 消息添加到UI
[self addMessageToDataSource:message progress:nil];
// 消息插入到会话
HDError *pError;
[self.conversation addMessage:message error:&pError];
}];
}


// JSON字符串转化为字典
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString
{
if (jsonString == nil) {
return nil;
}
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
if(err)
{
NSLog(@"json解析失败:%@",err);
return nil;
}
return dic;
}


收起阅读 »

客服满意度评价设置

1、怎么设置会话结束时自动发送评价邀请     管理员模式­----­­设置­­­----满意度评价邀请设置 第一项:会话结束自动发送满意度评价邀请,勾选需要设置的访客渠道 2、访客是否能多次评价    访客的评价次数是不限制的,但是可以设置客服后台统计的评...
继续阅读 »

1、怎么设置会话结束时自动发送评价邀请
    管理员模式­----­­设置­­­----满意度评价邀请设置 第一项:会话结束自动发送满意度评价邀请,勾选需要设置的访客渠道

2、访客是否能多次评价
   访客的评价次数是不限制的,但是可以设置客服后台统计的评为为第一次还是最后一次,可以在管理员模式­----­­设置­­­----  满意度评价邀请设置 第三项:多次满意度评价只取第一次
3、系统结束的会话是否自动发送满意度评价
  管理员模式­----­­设置­­­----满意度评价邀请设置  开关打开时,因访客超时未回复、不活跃超时而自动结束的会话,系统不会自动推送满意度评价

4、如何设置满意度邀请提示语
管理员模式­----­­设置­­­----满意度评价邀请设置  第6项

  收起阅读 »

android客服云如何获取机器人欢迎语

1.APP端要想获取到机器人菜单欢迎语,首先在移动客服管理员模式下,智能机器人,机器人设置——自动回复——欢迎语配置菜单欢迎语或者是欢迎语消息。 2、在会话分配规则中,渠道指定一定要指定机器人,APP端才能获取到机器人欢迎语,否则获取不到 具体的代码如下附件 ...
继续阅读 »
1.APP端要想获取到机器人菜单欢迎语,首先在移动客服管理员模式下,智能机器人,机器人设置——自动回复——欢迎语配置菜单欢迎语或者是欢迎语消息。
2、在会话分配规则中,渠道指定一定要指定机器人,APP端才能获取到机器人欢迎语,否则获取不到
具体的代码如下附件
图1中的
MyApplication.tenantId记得替换为自己的tenantId
new Thread(new Runnable() {
                    @Override
                    public void run() {
                        HttpClient httpClient = new DefaultHttpClient();
                        HttpGet httpGet = new HttpGet("http://kefu.easemob.com/v1/Tenants/"+MyApplication.tenantId+"/robots/visitor/greetings/app");
                        try {
                            HttpResponse response = httpClient.execute(httpGet);
                            int code = response.getStatusLine().getStatusCode();
                            if (code == 200) {
                                final String rev  = EntityUtils.toString(response.getEntity());

                                JSONObject obj = new JSONObject(rev);
                                int type = obj.getInt("greetingTextType");
                                final String rob_welcome = obj.getString("greetingText");
                                //type为0代表是文字消息的机器人欢迎语
                                //type为1代表是菜单消息的机器人欢迎语
                                if(type == 0){
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(rob_welcome);
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+rob_welcome,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }else if(type == 1){
                                    final String str = rob_welcome.replaceAll("&quot;","\"");
                                    JSONObject json = new JSONObject(str);
                                    JSONObject ext = json.getJSONObject("ext");
                                    final JSONObject msgtype = ext.getJSONObject("msgtype");
                                    //把解析拿到的string保存在本地
                                    shareUtil.saveRobot(msgtype.toString());
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(getApplicationContext(),"rob_welcome="+msgtype,Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }

                            }
                        }catch(final Exception e){
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(getApplicationContext(),"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                                }
                            });
                        }
                    }
                }).start();


图2中的:
//创建消息插入本地
            Message message = Message.createReceiveMessage(Message.Type.TXT);
            //从本地获取保存的string
            String str = shareUtil.getRobot();
            EMTextMessageBody body = null;
            //判断是否是菜单消息的string,这是自己实现的一个方法
            if(!isRobotMenu(str)){
                //文字消息直接去设置给消息
            body = new EMTextMessageBody(str);
            }else{
                //菜单消息需要设置给消息扩展
                try{
                    body = new EMTextMessageBody("");
                    JSONObject msgtype = new JSONObject(str);
                    message.setAttribute("msgtype",msgtype);
                }catch (Exception e){
                    Toast.makeText(this,"exception="+e.getMessage(),Toast.LENGTH_SHORT).show();
                }
}

            message.setFrom(MyApplication.imService);
            message.addBody(body);
            message.setMsgTime(System.currentTimeMillis());
            message.setStatus(Message.Status.SUCCESS);
            message.setMsgId(UUID.randomUUID().toString());
ChatClient.getInstance().chatManager().saveMessage(message);



图3中的:
private boolean isRobotMenu(String str){
        try {
            JSONObject json = new JSONObject(str);
            JSONObject obj = json.getJSONObject("choice");
        }catch (Exception e){
            return false;
        }
        return true;
}
 
  收起阅读 »

(客服云)知识库添加了附件,怎么发送的时候附件没有发送出去?

附件需要单独发送给访客。发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右上角的“发送”按钮。  
附件需要单独发送给访客。发送知识时,图文消息仅包含知识标题和知识内容,不包含知识的附件。若发送知识中的附件,需打开知识详情,并点击附件右上角的“发送”按钮。
 

(客服云)iOS访客端插入企业欢迎语

0、前提配置 (1)在客服系统内 管理员模式--设置--系统开关--企业欢迎语 处设置欢迎语,并开启开关 (2)联系商务开通会话创建、接起、结束的透传事件功能 1、在HDMessageViewController.m中添加企业欢迎语属性 /...
继续阅读 »
0、前提配置
(1)在客服系统内 管理员模式--设置--系统开关--企业欢迎语 处设置欢迎语,并开启开关

企业欢迎语0.png


(2)联系商务开通会话创建、接起、结束的透传事件功能

1、在HDMessageViewController.m中添加企业欢迎语属性

企业欢迎语1.png

// 企业欢迎语
@property (nonatomic, strong) NSString *companyWelcome;

2、在HDMessageViewController.m中的 viewdidload 方法的最后调用 [self judge]; 

企业欢迎语2.png



企业欢迎语3.png

// 这个方法是属于逻辑判断,用UD去记录参数,只有第一次进入聊天页面和会话结束之后再进入到聊天页面才插入消息。
- (void)judge
{
// 判断会话中是否有消息
if (self.conversation.latestMessage == nil) {
// 如果会话中没有消息,那么UD的值变成NO
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
}
// 根据以前的判断,如果UD值为NO则插入消息欢迎语
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"isGetWelcome"]) {
// 插入企业欢迎语
[self welcome];
}

[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
}

// 这个方法是构建消息以及向UI页面以及本地数据库插入消息
- (void)welcome
{
// 此方法是SDK获取企业欢迎语的
__weak typeof(self) weakself = self;
[[HDClient sharedClient].chatManager getEnterpriseWelcomeWithCompletion:^(NSString *welcome, HDError *error) {
// 判断客服系统中是否设置了企业欢迎语并且开关有没有打开,兼容误操作客服关闭了企业欢迎语开关,会插入一条空消息,那么自己选择是否插入一条默认的欢迎语,自行修改 ‘您好,欢迎光临!’ 的内容
if (![welcome isEqualToString:@""]) {
weakself.companyWelcome = welcome;
} else {
weakself.companyWelcome = @"您好,欢迎光临!";
}
// 构建消息
EMTextMessageBody *bdy = [[EMTextMessageBody alloc] initWithText:weakself.companyWelcome];
NSString *from = [[HDClient sharedClient] currentUsername];
// _converID就是IM服务号,可以自己替换下
HDMessage *message = [[HDMessage alloc] initWithConversationID:weakself.conversation.conversationId from:from to:weakself.conversation.conversationId body:bdy];
// 构建的消息用ext来标记此条消息是插入的欢迎语,然后在cell里面根据消息的ext来修改插入消息的昵称和头像
NSDictionary *welcomeExt = @{@"insertWelcome":@"插入的欢迎语"};
[message addAttributeDictionary:welcomeExt];
message.direction = 1;
message.status = HDMessageStatusSuccessed;
// 更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 消息添加到数据源,刷新UI
[weakself addMessageToDataSource:message progress:nil];
});
// 消息插入到会话
HDError *pError;
[weakself.conversation addMessage:message error:&pError];

}];
}
 
3、在HDMessageViewController.m中的cmdMessagesDidReceive方法中调用 messageExtwithEventName

企业欢迎语4.png

// 这个方法是判断接收到客服消息的ext里面有没有ServiceSessionClosedEvent,如果有就把UD记录改变参数,下次进入聊天页面的时候插入欢迎语
// 如果是ServiceSessionOpenedEvent,客服系统接入了会话,那么就不插入欢迎语

// 这个方法是接收透传消息里面调用,客服系统会话被接入会给app端发送透传消息通知,如果没有接收到透传消息,那么找对接的环信商务开通这个功能
[self messageExtwithEventName:message];

- (void)messageExtwithEventName:(HDMessage *)message
{
if (![[[message.ext objectForKey:@"weichat"] objectForKey:@"event"] isKindOfClass:[NSNull class]]) {
NSDictionary *dict = [[message.ext objectForKey:@"weichat"] objectForKey:@"event"];
if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionClosedEvent"]) {
//如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isGetWelcome"];
}
if ([[dict objectForKey:@"eventName"] isEqualToString:@"ServiceSessionOpenedEvent"]) {
//如果接收到客服的消息ext中有 ServiceSessionClosedEvent,表示会话已经结束,那么UD的值变成NO
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isGetWelcome"];
}

}
}
 
4、在cell中设置插入消息的昵称头像,根据插入消息ext中的key去判断。

企业欢迎语5.png

if (!self.model.isSender) {
if (![model.nickname isKindOfClass:[NSNull class]]) {
// 判断此条消息是否为插入的企业欢迎语,是的话赋值昵称和头像
if ([self.model.message.ext objectForKey:@"insertWelcome"]) {
_nameLabel.text = @"系统消息";
self.avatarView.image = [UIImage imageNamed:@"测试图片"];
}
}
}

效果图

企业欢迎语效果图.png


  收起阅读 »

Android MVP架构从入门到精通-真枪实弹

一. 前言 你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼? 你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况? 你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈? 如果你现在还是用MVC架构模式在...
继续阅读 »


0.jpeg


一. 前言

你是否遇到过Activity/Fragment中成百上千行代码,完全无法维护,看着头疼?

你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况?

你是否遇到过用MVC架构写的项目进行单元测试时的深深无奈?

如果你现在还是用MVC架构模式在写项目,请先转到MVP模式!

二. MVC架构

MVC架构模式最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,再后来因Android项目由XML和Activity/Fragment组成,慢慢的Android开发者开始使用类似MVC的架构模式开发应用.


0_(1).jpeg


 
M层:模型层(model),主要是实体类,数据库,网络等存在的层面,model将新的数据发送到view层,用户得到数据响应.

V层:视图层(view),一般指XML为代表的视图界面.显示来源于model层的数据.用户的点击操作等事件从view层传递到controller层.

C层:控制层(controller),一般以Activity/Fragment为代表.C层主要是连接V层和M层的,C层收到V层发送过来的事件请求,从M层获取数据,展示给V层.

从上图可以看出M层和V层有连接关系,而Activity有时候既充当了控制层又充当了视图层,导致项目维护比较麻烦.
 
1. MVC架构优缺点
A. 缺点

M层和V层有连接关系,没有解耦,导致维护困难.

Activity/Fragment中的代码过多,难以维护.

Activity中有很多关于视图UI的显示代码,因此View视图和Activity控制器并不是完全分离的,当Activity类业务过多的时候,会变得难以管理和维护.尤其是当UI的状态数据,跟持久化的数据混杂在一起,变得极为混乱.

B. 优点

控制层和View层都在Activity中进行操作,数据操作方便.

模块职责划分明确.主要划分层M,V,C三个模块.

三. MVP架构


0_(2).jpeg


 
MVP,即是Model,View,Presenter架构模式.看起来类似MVC,其实不然.从上图能看到Model层和View层没有相连接,完全解耦.

用户触碰界面触发事件,View层把事件通知Presenter层,Presenter层通知Model层处理这个事件,Model层处理后把结果发送到Presenter层,Presenter层再通知View层,最后View层做出改变.这是一整套流程.

M层:模型层(Model),此层和MVC中的M层作用类似.

V层:视图层(View),在MVC中V层只包含XML文件,而MVP中V层包含XML,Activity和Fragment三者.理论上V层不涉及任何逻辑,只负责界面的改变,尽量把逻辑处理放到M层.

P层:通知层(Presenter),P层的主要作用就是连接V层和M层,起到一个通知传递数据的作用.

1. MVP架构优缺点
A. 缺点

MVP中接口过多.

每一个功能,相比于MVC要多写好几个文件.

如果某一个界面中需要请求多个服务器接口,这个界面文件中会实现很多的回调接口,导致代码繁杂.

如果更改了数据源和请求中参数,会导致更多的代码修改.

额外的代码复杂度及学习成本.

B. 优点

模块职责划分明显,层次清晰,接口功能清晰.

Model层和View层分离,解耦.修改View而不影响Model.

功能复用度高,方便.一个Presenter可以复用于多个View,而不用更改Presenter的逻辑.

有利于测试驱动开发,以前的Android开发是难以进行单元测试.

如果后台接口还未写好,但已知返回数据类型的情况下,完全可以写出此接口完整的功能.

四. MVP架构实战(真枪实弹)
1. MVP三层代码简单书写

接下来笔者从简到繁,一点一点的堆砌MVP的整个架构.先看一下XML布局,布局中一个Button按钮和一个TextView控件,用户点击按钮后,Presenter层通知Model层请求处理网络数据,处理后Model层把结果数据发送给Presenter层,Presenter层再通知View层,然后View层改变TextView显示的内容.
 


0.gif


 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.SingleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>

接下来是Activity代码,里面就是获取Button和TextView控件,然后对Button做监听,先简单的这样写,一会慢慢的增加代码.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

}
}

下面是Model层代码.本次网络请求用的是wanandroid网站的开放api,其中的文章首页列表接口.SingleInterfaceModel文件里面有一个方法getData,第一个参数curPage意思是获取第几页的数据,第二个参数callback是Model层通知Presenter层的回调.
 
public class SingleInterfaceModel {

public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
Callback文件内容如下.里面一个成功一个失败的回调接口,参数全是泛型,为啥使用泛型笔者就不用说了吧.
 
public interface Callback<K, V> {
void onSuccess(K data);

void onFail(V data);
}
再接下来是Presenter层的代码.SingleInterfacePresenter类构造函数中直接new了一个Model层对象,用于Presenter层对Model层的调用.然后SingleInterfacePresenter类的方法getData用于与Model的互相连接.
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码

}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
至此,MVP三层简单的部分代码算是完成.那么怎样进行整个流程的相互调用呢.我们把刚开始的SingleInterfaceActivity代码改一下,让SingleInterfaceActivity持有Presenter层的对象,这样View层就可以调用Presenter层了.修改后代码如下.
 
public class SingleInterfaceActivity extends AppCompatActivity {

private Button button;
private TextView textView;
private SingleInterfacePresenter singleInterfacePresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

singleInterfacePresenter = new SingleInterfacePresenter();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});

}
}
从以上所有代码可以看出,当用户点击按钮后,View层按钮的监听事件执行调用了Presenter层对象的getData方法,此时,Presenter层对象的getData方法调用了Model层对象的getData方法,Model层对象的getData方法中执行了网络请求和逻辑处理,把成功或失败的结果通过Callback接口回调给了Presenter层,然后Presenter层再通知View层改变界面.但此时SingleInterfacePresenter类中收到Model层的结果后无法通知View层,因为SingleInterfacePresenter未持有View层的对象.如下代码的注释中有说明.(如果此时点击按钮,下方代码LP.w()处会打印出网络请求成功的log)
 
public class SingleInterfacePresenter {
private final SingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码

}
});
}
}
代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第一次修改"克隆此时的代码.

2. P层V层沟通桥梁

现在P层未持有V层对象,不能通知V层改变界面,那么就继续演变MVP架构.
在MVP架构中,我们要为每个Activity/Fragment写一个接口,这个接口需要让Presenter层持有,P层通过这个接口去通知V层更改界面.接口中包含了成功和失败的回调,这个接口Activity/Fragment要去实现,最终P层才能通知V层.
 
public interface SingleInterfaceIView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
一个完整的项目以后肯定会有许多功能界面,那么我们应该抽出一个IView公共接口,让所有的Activity/Fragment都间接实现它.IVew公共接口是用于给View层的接口继承的,注意,不是View本身继承.因为它定义的是接口的规范, 而其他接口才是定义的类的规范(这句话请仔细理解).
public interface IView {
}
这个接口中可以写一些所有Activigy/Fragment共用的方法,我们把SingleInterfaceIView继承IView接口.
public interface SingleInterfaceIView extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}
同理Model层和Presenter层也是如此.
public interface IModel {
}
public interface IPresenter {
}
现在项目中Model层是一个SingleInterfaceModel类,这个类对象被P层持有,对于面向对象设计来讲,利用接口达到解耦目的已经人尽皆知,那我们就要对SingleInterfaceModel类再写一个可继承的接口.代码如下.
public interface ISingleInterfaceModel extends IModel {
void getData(int curPage, final Callback callback);
}
如此,SingleInterfaceModel类的修改如下.
 
public class SingleInterfaceModel implements ISingleInterfaceModel {

@Override
public void getData(int curPage, final Callback callback) {
NetUtils.getRetrofit()
.create(Api.class)
.getData(curPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ArticleListBean>() {
@Override
public void onCompleted() {
LP.w("completed");
}

@Override
public void onError(Throwable e) {
callback.onFail("出现错误");
}

@Override
public void onNext(ArticleListBean bean) {
if (null == bean) {
callback.onFail("出现错误");
} else if (bean.errorCode != 0) {
callback.onFail(bean.errorMsg);
} else {
callback.onSuccess(bean);
}
}
});
}
}
同理,View层持有P层对象,我们也需要对P层进行改造.但是下面的代码却没有像ISingleInterfaceModel接口继承IModel一样继承IPresenter,这点需要注意,笔者把IPresenter的继承放在了其他处,后面会讲解.
 
public interface ISingleInterfacePresenter {
void getData(int curPage);
}

然后SingleInterfacePresenter类的修改如下:
 
public class SingleInterfacePresenter implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
}
});
}
}
3. 生命周期适配

至此,MVP三层每层的接口都写好了.但是P层连接V层的桥梁还没有搭建好,这个慢慢来,一个好的高楼大厦都是一步一步建造的.上面IPresenter接口我们没有让其他类继承,接下来就讲下这个.P层和V层相连接,V层的生命周期也要适配到P层,P层的每个功能都要适配生命周期,这里可以把生命周期的适配放在IPresenter接口中.P层持有V层对象,这里把它放到泛型中.代码如下.
 
public interface IPresenter<T extends IView> {

    /**
     * 依附生命view
     *
     * @param view
     */
    void attachView(T view);

    /**
     * 分离View
     */
    void detachView();

    /**
     * 判断View是否已经销毁
     *
     * @return
     */
    boolean isViewAttached();

}
 
这个IPresenter接口需要所有的P层实现类继承,对于生命周期这部分功能都是通用的,那么就可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected T mView;

@Override
public void attachView(T view) {
mView = view;
}

@Override
public void detachView() {
mView = null;
}

@Override
public boolean isViewAttached() {
return mView != null;
}
}
此时,SingleInterfacePresenter类的代码修改如下.泛型中的SingleInterfaceIView可以理解成对应的Activity,P层此时完成了对V层的通信.
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceIView> implements ISingleInterfacePresenter {
private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}
此时,P层和V层的连接桥梁已经搭建,但还未搭建完成,我们需要写个BaseMVPActvity让所有的Activity继承,统一处理Activity相同逻辑.在BaseMVPActvity中使用IPresenter的泛型,因为每个Activity中需要持有P层对象,这里把P层对象抽出来也放在BaseMVPActvity中.同时BaseMVPActvity中也需要继承IView,用于P层对V层的生命周期中.代码如下.
 
public abstract class BaseMVPActivity<T extends IPresenter> extends AppCompatActivity implements IView {

protected T mPresenter;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}

protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}

@Override
protected void onDestroy() {
if (mPresenter != null) {
mPresenter.detachView();
}
super.onDestroy();
}

/**
* 创建一个Presenter
*
* @return
*/
protected abstract T createPresenter();

protected abstract void init();

}
接下来让SingleInterfaceActivity实现这个BaseMVPActivity.
public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter> implements SingleInterfaceIView {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
到此,MVP架构的整个简易流程完成.

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第二次修改"克隆此时的代码.

4. 优化MVP架构


0_(3).jpeg


 
 
上面是MVP的目录,从目录中我们可以看到一个功能点(网络请求)MVP三层各有两个文件需要写,相对于MVC来说写起来确实麻烦,这也是一些人不愿意写MVP,宁愿用MVC的原因.

这里我们可以对此优化一下.MVP架构中有个Contract的概念,Contract有统一管理接口的作用,目的是为了统一管理一个页面的View和Presenter接口,用Contract可以减少部分文件的创建,比如P层和V层的接口文件.

那我们就把P层的ISingleInterfacePresenter接口和V层的SingleInterfaceIView接口文件删除掉,放入SingleInterfaceContract文件中.代码如下.
 
public interface SingleInterfaceContract {


interface View extends IView {
void showArticleSuccess(ArticleListBean bean);

void showArticleFail(String errorMsg);
}

interface Presenter {
void getData(int curPage);
}


}
此时,SingleInterfacePresenter和SingleInterfaceActivity的代码修改如下.
 
public class SingleInterfacePresenter extends BasePresenter<SingleInterfaceContract.View>
implements SingleInterfaceContract.Presenter {

private final ISingleInterfaceModel singleInterfaceModel;

public SingleInterfacePresenter() {
this.singleInterfaceModel = new SingleInterfaceModel();
}

@Override
public void getData(int curPage) {
singleInterfaceModel.getData(curPage, new Callback<ArticleListBean, String>() {
@Override
public void onSuccess(ArticleListBean loginResultBean) {
//如果Model层请求数据成功,则此处应执行通知View层的代码
//LP.w()是一个简单的log打印
LP.w(loginResultBean.toString());
if (isViewAttached()) {
mView.showArticleSuccess(loginResultBean);
}
}

@Override
public void onFail(String errorMsg) {
//如果Model层请求数据失败,则此处应执行通知View层的代码
LP.w(errorMsg);
if (isViewAttached()) {
mView.showArticleFail(errorMsg);
}
}
});
}
}

public class SingleInterfaceActivity extends BaseMVPActivity<SingleInterfacePresenter>
implements SingleInterfaceContract.View {

private Button button;
private TextView textView;

@Override
protected void init() {
setContentView(R.layout.activity_single_interface);
button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPresenter.getData(0);
}
});
}

@Override
protected SingleInterfacePresenter createPresenter() {
return new SingleInterfacePresenter();
}


@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}

代码写到这里,笔者先把这些代码提交到github(https://github.com/serge66/MVPDemo),github上会有一次提交记录,如果想看此时的代码,可以根据提交记录"第三次修改"克隆此时的代码.

5. 单页面多网络请求以及P层复用

上面的MVP封装只适用于单页面一个网络请求的情况,当一个界面有两个网络请求时,此封装已不适合.以及考虑到P层的复用.为此,我们再次新建一个MultipleInterfaceActivity来进行说明.XML中布局是两个按钮两个Textview,点击则可以进行网络请求.


0_(1).gif


 
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:context=".view.MultipleInterfaceActivity">

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100px"
android:text="点击" />

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50px"
android:text="请点击上方按钮获取数据" />
</LinearLayout>
MultipleInterfaceActivity类代码暂时如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

@Override
protected IPresenter createPresenter() {
return null;
}

}
此时我们可以想下,当一个页面中有多个网络请求时,Activity所继承的BaseMVPActivity的泛型中要写多个参数,那有没有上面代码的框架不变的情况下实现这个需求呢?答案必须有的.我们可以把多个网络请求的功能当做一个网络请求来看待,封装成一个MultiplePresenter,其继承至BasePresenter实现生命周期的适配.此MultiplePresenter类的作用就是容纳多个Presenter,连接同一个View.代码如下.
 
public class MultiplePresenter<T extends IView> extends BasePresenter<T> {
private T mView;

private List<IPresenter> presenters = new ArrayList<>();

@SafeVarargs
public final <K extends IPresenter<T>> void addPresenter(K... addPresenter) {
for (K ap : addPresenter) {
ap.attachView(mView);
presenters.add(ap);
}
}

public MultiplePresenter(T mView) {
this.mView = mView;
}

@Override
public void detachView() {
for (IPresenter presenter : presenters) {
presenter.detachView();
}
}

}
因MultiplePresenter类中需要有多个网络请求,现在举例说明时,暂时用两个网络请求接口.MultipleInterfaceActivity类中代码改造如下.
 
public class MultipleInterfaceActivity extends BaseMVPActivity<MultiplePresenter>
implements SingleInterfaceContract.View, MultipleInterfaceContract.View {

private Button button;
private TextView textView;
private Button btn;
private TextView tv;
private SingleInterfacePresenter singleInterfacePresenter;
private MultipleInterfacePresenter multipleInterfacePresenter;


@Override
protected void init() {
setContentView(R.layout.activity_multiple_interface);

button = findViewById(R.id.button);
textView = findViewById(R.id.textView);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
singleInterfacePresenter.getData(0);
}
});


btn = findViewById(R.id.btn);
tv = findViewById(R.id.tv);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
multipleInterfacePresenter.getBanner();
}
});
}

@Override
protected MultiplePresenter createPresenter() {
MultiplePresenter multiplePresenter = new MultiplePresenter(this);

singleInterfacePresenter = new SingleInterfacePresenter();
multipleInterfacePresenter = new MultipleInterfacePresenter();

multiplePresenter.addPresenter(singleInterfacePresenter);
multiplePresenter.addPresenter(multipleInterfacePresenter);
return multiplePresenter;
}

@Override
public void showArticleSuccess(ArticleListBean bean) {
textView.setText(bean.data.datas.get(0).title);
}

@Override
public void showArticleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}

@Override
public void showMultipleSuccess(BannerBean bean) {
tv.setText(bean.data.get(0).title);
}

@Override
public void showMultipleFail(String errorMsg) {
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
写到这里,MVP框架基本算是完成.如果想再次优化,其实还是有可优化的地方,比如当View销毁时,现在只是让P层中的View对象置为null,并没有继续对M层通知.如果View销毁时,M层还在请求网络中呢,可以为此再加入一个取消网络请求的通用功能.这里只是举一个例子,每个人对MVP的理解不一样,而MVP架构也并不是一成不变,适合自己项目的才是最好的.

6. 完整项目地址

完整项目已提交到github(https://github.com/serge66/MVPDemo).点击下方阅读原文即可访问.

五. 参考资料

[一步步带你精通MVP](https://mp.weixin.qq.com/s/DuNbl3V4gZY-ZCETbhZGug)

[从0到1搭建MVP框架](https://mp.weixin.qq.com/s/QFpHhC-5JkAb4IlMP0nKug)

[Presenter层如何高度的复用](https://juejin.im/post/599ce8016fb9a0247e4255f4)
 
六. 后续

MVVM架构从入门到精通-真枪实弹 敬请期待~~~
 


qrcode_for_gh_08bfa7313fb2_258.jpg


微信公众号:IT大前端
关注可了解更多的大前端领域技术
  收起阅读 »

React Native调用原生Android/iOS代码方案并实现拨号功能

一 前言 由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的...
继续阅读 »
一 前言

由于前几个月公司2.0项目开发技术选型为React Native,技术部相关人员开始学习React Native相关的技术,笔者是一名Android开发者,下文所描述的React Native调用Android/iOS模块中关于iOS的部分如有误的地方,请指出。为了让从Android或iOS学习React Native的同志更加清楚的了解另一移动端,笔者尽可能写的详细点。

二 效果

下面两张图分别为iOS和Android上效果图,其中iOS效果图中点击电话号码会打印log,并不会调起iOS拨号界面,因为iOS模拟器不支持此功能,所以要想看效果只能用真机查看。这里打印log是为了证明React Native成功调起了原生iOS模块功能。


640.gif


 


640_(1).gif


 
 
三 实现方案

关于调用拨号功能以及调用浏览器、短信、邮箱等功能,可有两种实现方案。

一种是按照React Native提供的调用原生的过程方案来调用,这种适合大部分React Native调用原生功能的需求,掌握这种后,基本以后再有调用原生需求即可按照此过程方案解决,此文也会选用这种方案进行描述。

另一种是React Native帮我们封装的Linking模块可以实现这类的需求,这种相比上一种来说相对简单,主要适用于调用原生的电话、短信、邮箱、浏览器等功能。

四 实现原生Android模块

1.在自己新建的Reacat Native项目中android/app/src/main/java/xxx(项目包名)/ 目录下(为了和其他文件分离,笔者又在此目录下新建一个native文件夹),需要新建一个java类文件,例如文件名为CallPhoneModule.java,这个java类一定要继承RN提供的ReactContextBaseJavaModule抽象类,然后实现其构造函数,其中的参数要为ReactApplicationContext reactContext。
 
public class CallPhoneModule extends ReactContextBaseJavaModule {
public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}

 
2.然后实现NativeModule中定义的getName()方法,返回一个String类型字符串,这个返回结果将要在JavaScript中使用,例如返回“CallPhoneModule”,则可以在JavaScript中通过React.NativeModules.CallPhoneModule调用。
 
注意,如果返回的字符串中有RCT前缀,则会自动移除RCT前缀。例如返回“RCTCallPhoneModule”,则在JavaScript中依然可以通过React.NativeModules.CallPhoneModule调用。CallPhoneModule继承ReactContextBaseJavaModule,ReactContextBaseJavaModule继承BaseJavaModule,BaseJavaModule实现了NativeModule接口,这是CallPhoneModule与NativeModule的关系。 
@Override
public String getName() {
return "CallPhoneModule";
}

 
3.然后在CallPhoneModule类中写一个方法,这个方法提供给JavaScript调用,例如方法名为callPhone,里面传递String类型参数(非必须),特别的这个方法要使用@ReactMethod注解,以及返回类型必须为void。然后在callPhone方法中写入要实现的功能,这里写入了拨号功能的实现。
 
Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);

CallPhoneModule.java文件的全部代码如下:
package com.zhuku02;

import android.content.Intent;
import android.net.Uri;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.lang.String;

public class CallPhoneModule extends ReactContextBaseJavaModule {

public ReactApplicationContext reactContext;

public CallPhoneModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void callPhone(String phoneString) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneString));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.reactContext.startActivity(intent);
}

@Override
public String getName() {
return "CallPhoneModule";
}
}

4.新建一个java类文件,例如文件名为CallPhoneReactPackage.java,这个类必须实现ReactPackage接口,然后实现createViewManagers、createNativeModules两个方法,特别的要在createNativeModules方法的返回值中add进刚才新建的CallPhoneModule类。

CallPhoneReactPackage.java全部代码如下:
package com.zhuku02;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.zhuku02.CallPhoneModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallPhoneReactPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CallPhoneModule(reactContext));

return modules;
}
}

5.最后在MainApplication.java文件中的getPackages方法中加上刚才新建的CallPhoneReactPackage类。至此,原生Android模块书写完毕。关于JavaScript调用原生Android模块代码会在文末和调用原生iOS一起写出。

修改后的MainApplication.java文件代码如下:
package com.zhuku02;

import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import com.zhuku02.CallPhoneReactPackage;

import java.util.Arrays;
import java.util.List;


public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CallPhoneReactPackage()
);
}
};

@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

 
五 实现原生iOS模块

1.在自己新建的Reacat Native项目中ios/xxx(项目包名)/ 目录下,需要新建一个后缀为m和一个后缀为h的文件。为了和其他文件分离以及和Android保持一致,笔者又在此目录下新建一个native文件夹。
 
在Xcode的此文件夹上右键New File,然后在弹出的页面中Cocoa Touch Class选项输入文件名,这样会建立出相同文件名的m和h文件。如果New File时,分别选择Objective-C File和Header File,则这两个文件名要相同。例如文件名称为CallPhoneModuleIos。

2.在CallPhoneModuleIos.h文件中,类要实现RN提供的RCTBridgeModule协议。RCT是ReaCT的缩写,React Native中Object-C相关的命名均以RCT开头。
 
RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到Object-C对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信。这样,通过Object-C Bridge和JavaScript Bridge就可以实现JavaScript和Object-C的相互调用。

CallPhoneModuleIos.h文件如下:
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface CallPhoneModuleIos : NSObject <RCTBridgeModule>

@end

3.CallPhoneModuleIos.m文件中,类需要包含RCT_EXPORT_MODULE()宏,作用是自动注册一个module。这个宏可以添加一个参数,用来指定在JavaScript调用这个模块的名字,类似于上文中说的getName()方法。如果不添加这个参数,则默认就是这个类的名字。

4.然后需要在此类中写一个方法,提供给RN调用,方法通过RCT_EXPORT_METHOD()宏来实现。
RCT_EXPORT_METHOD(callPhone: (NSString *)phone){
NSLog(@"======%@",phone);
}

CallPhoneModuleIos.m完整代码:
#import "CallPhoneModuleIos.h"
#import <Foundation/Foundation.h>

@implementation CallPhoneModuleIos

RCT_EXPORT_MODULE(CallPhoneModuleIos);

RCT_EXPORT_METHOD(callPhone: (NSString *)phone){

NSLog(@"======%@",phone);
//去掉注释,下面代码就是实现拨号功能代码,但还未真机测试
// NSMutableString * str = [[NSMutableString alloc] initWithFormat:@"telprompt://%@",phone];
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:str]];

}

// -(dispath_queue_t)methodQueue{
// return dispath_get_main_queue();
// }

@end

至此,则原生iOS代码书写完成,现在即将开始调用。

六 React Native调用Android、iOS原生模块

为了在JavaScript端同时访问Android、iOS原生模块更加方便,笔者把原生模块的调用封装在一个JavaScript文件中,例如文件名为CallPhone.js,这样在需要调用的地方直接调用此JavaScript文件既可,同时在此文件中,处理好Android、iOS、Web(若有)的分别调用。
import {Platform, NativeModules} from 'react-native';

var module = null;
if (Platform.OS == 'ios') {
module = NativeModules.CallPhoneModuleIos;
} else if (Platform.OS == 'android') {
module = NativeModules.CallPhoneModule;
} else if (Platform.OS == 'web') {
//暂未实现web功能
}
export default module;

然后在JavaScript文件中这样调用:
import CallPhone from '../../native/CallPhone';
CallPhone.callPhone('40077731xx');

到这里,整篇文章就结束了,疑问、建议或者指教欢迎讨论。

七 参考资料

native-modules-ios (https://facebook.github.io/react-native/docs/native-modules-ios.html)
native-modules-android (https://facebook.github.io/react-native/docs/native-modules-android.html) 



qrcode_for_gh_08bfa7313fb2_258.jpg


微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

EaseUI 3.5.3 DexArchiveMergerException

集成easeui3.5.3报错
集成easeui3.5.3报错

【活动推荐】2019安卓巴士千人开发者大会-NEW●无界

本次将邀请到BAT等知名互联网公司的16位优秀演讲嘉宾坐镇,内容上除了主会场全天的Android技术干货分享,分会场还将覆盖到大数据、机器学习、跨平台开发以及小程序等方向内容。 三人行两人免单团购超便宜报名速戳https://www.hdb.com/dis/h...
继续阅读 »
   本次将邀请到BAT等知名互联网公司的16位优秀演讲嘉宾坐镇,内容上除了主会场全天的Android技术干货分享,分会场还将覆盖到大数据、机器学习、跨平台开发以及小程序等方向内容。

三人行两人免单团购超便宜报名速戳https://www.hdb.com/dis/htn3yi6fpo
活动时间:2019年4月20号
活动地点:深圳南山区圣淘沙酒店
 
*入现场群说明:

购票成功后,请添加安卓巴士官方微信号 shzsbgbs ,备注【开发者大会】通过后提供购票成功截图,小编邀请您进入420开发者大会现场群。

备注【拼团】邀请您进入拼团群,和小伙伴一起享受超低团购价。

*付费说明:

本次活动经费用于场租、设备、物料、礼品、嘉宾交通住宿费等。

*退款说明:

由于本活动各项资源需提前采购,一经售出不接受退款,请确认后购买。 
 


41551673499360_article4_58412.jpg




41551673130966_article4_57367.jpg




微信图片_20190305183217.jpg


  收起阅读 »

WIFI 考勤打卡 浅析

一、背景 最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。 二、名词术语解释 下面的理解全是建立在无线路由器的基础上。如有错...
继续阅读 »
一、背景

最近产品部提出了在WEB端设置wifi考勤打卡新需求,根据管理员设置的wifi相关信息(主要是WIFI名称和MAC地址),员工用户利用移动端相连接的wifi进行wifi考勤打卡。

二、名词术语解释

下面的理解全是建立在无线路由器的基础上。如有错误请指出。

1、SS

SS(Service set)即服务集,是无线局域网中的一个术语,用以描述802.11无线网络的构成单位(一组互相有联系的无线设备),使用服务集标识符(SSID)作为识别。

可以分为独立基本服务集(IBSS)、基本服务集(BSS)和扩展服务集(ESS)三类。其中IBSS属于对等拓扑模式(又称Ad-Hoc模式、无线随意网络),而BSS和ESS属于基础架构模式。这些拓扑是原始的802.11规范中定义的,其他的如网桥、中继器等则是属于特定厂商的扩展或者WDS的拓扑模式。

2、SSID

SSID(Service Set Identifier)即服务集标识符,是一个或一组基础架构模式无线网络的标识,依照标识方式又可细分为两种:
基本服务集标识符(BSSID),表示的是AP的数据链路层的MAC地址。
扩展服务集标识符(ESSID),一个最长32字节区分大小写的字符串,ESSID标识与SSID相同的网络。术语SSID最常用。
在此可以理解为无线路由器发射的某个wifi的名称。(SSID=name of network)

3、BSS

BSS(Basic Service Set)即基本服务集,是一组能在PHY层相互通信的所有站。每个BSS都有一个称为BSSID的标识(ID),它是服务于BSS的接入点的MAC地址。
用在无线路由器发射出的wifi上可以这样理解:某一个无线路由器发射出的wifi信号所覆盖的范围可视为BSS。


WechatIMG363.jpeg


 
4、BSSID

BSSID(Basic Service Set Identifier)即基本服务集标识符。

在上面的基础上可以这样理解:对某一个BSS基本服务集的唯一标识。例如,某无线路由器发射了一个名称为A的wifi热点,同一区域另一个无线路由器也发出了一个名称为A的wifi热点,当手机连接A热点时,如何辨别连接的是由哪一个路由器发射的wifi呢?

这时候就要用到BSSID了。一般情况下BSSID可以理解为无线路由器的MAC地址,通过查看手机连接wifi的MAC地址即可知道连接的是哪一个路由。(BSSID=AP MAC address)
其实准确来说手机得到的BSSID并不是路由器的基准(出厂)MAC地址。

例如,笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,或者是XX:XX:XX:XX:XX:F3。

5、ESS ESSID

ESS(Extended Service Set )即扩展的基本服务集。
ESSID(Extended Service Set Identifier)即扩展的基本服务集标识符。
BSS+BSS+BSS+BSS+…=ESS。ESS为多个BSS的集合。ESS使用指定的ESSID作识别。

通过将多个BSS比邻安置,可以扩展网络的范围,如果这些BSS通过各种分布系统互联(无论是有线的还是无线的),拥有一致的ESSID,并且对于逻辑链路控制层来说可以认为是一个BSS的话,那么这些BSS可以被统一为一个ESS。

在同一个ESS中的不同BSS之间切换的过程称为漫游。一般而言,一个ESS中的BSS都会使用相同的SSID和安全机制以提供接近于无缝漫游的可能。两个BSS之间通常有15%左右的重叠范围来保证漫游时信号不会长时间丢失,并且设置在不同频段来防止相互干扰。


WechatIMG362.jpeg


 
6、MAC

MAC地址采用十六进制数表示,共六个字节(48位)。(XX:XX:XX:XX:XX:XX )其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。

三、历程

当产品部提出wifi考勤打卡需求时,普遍认为一个路由器有一个mac地址,手机连接wifi可以根据mac地址等信息进行打卡。当我们用多个手机连接公司名称为A(SSID)的wifi时,发现手机上展示的mac地址并不是一致的,这个就尴尬了,打翻了原有理念。
然后发现我们公司共有五个无线路由器,wifi名称都是A。哦,这时候才感觉到原来以前的知识还是靠谱的,可能是多个手机具体连接的路由器不是同一个。

然后把五个路由器wifi热点名称改为A、B、C、D、E,多个手机连接A热点时,发现手机得到的mac地址是一致的,到这里可以得出的结论是手机连接同一个wifi热点得到的mac地址是一致的。但是…..又尴尬了。

当多款手机连接B热点时,发现又出现了不一致的mac地址,查找原因发现,原来B无线路由器中可以设置2.4G Hz和5G Hz两个不同频段的wifi热点。B路由器中默认是开启2.4G Hz和5G Hz频段的wifi热点,并且wifi名称(SSID)是同一个。经过检查还有个问题是B路由器的出厂mac地址和手机连接得到的mac地址不一致。

例如上面举得例子:笔者公司的某款无线路由器B的出厂MAC地址为 XX:XX:XX:XX:XX:F1,当手机连接此wifi查看mac地址时发现是XX:XX:XX:XX:XX:F2,另一款手机连接时是XX:XX:XX:XX:XX:F3。由此可得出的结论是,路由器有一个基准(出厂)mac地址,然后发射出wifi的mac在基准mac地址上按照一定的算法进行变动,具体的变动算法不清楚,有清楚的请告知我,非常感谢。

另外还有一个问题是,C路由器设备后面所写的出厂说明mac地址是XX:XX:XX:XX:XX:56,但是通过路由器后台看到的出厂mac地址是XX:XX:XX:XX:XX:57,手机连接后得到的mac地址是XX:XX:XX:XX:XX:56。这就尴尬了,是厂家写错了还是根据特定的算法算的?

除了根据wifi设备分析外,我们也对具有wifi考勤打卡功能的软件进行了分析。比如现在比较火爆的由阿里团队研发的钉钉,以及纷享销客APP,在Android端,他们的处理都是获取周围wifi信息(并不是当前手机连接的wifi)进行打卡。在iOS端,他们的处理都是根据当前手机连接的wifi信息进行打卡。据iOS同事说,iOS获取周围wifi信息需要申请此功能,并最低支持版本是iOS 9。另外据可靠消息,分享逍客对mac地址的处理也是通过忽略低4位进行匹配。

四、结论

经过上述分析,手机获取的无线路由器MAC地址的低4位是变化的。那我们实现这个需求时,除了匹配虚拟位置、手机信息、wifi相关等其他信息外,只针对mac地址,我们可以忽略mac地址的低4位来做匹配。

五、参考资料

http://www.juniper.net/documentation/en_US/junos-space-apps12.3/network-director/topics/concept/wireless-ssid-bssid-essid.html 



qrcode_for_gh_08bfa7313fb2_258.jpg


微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

2019年3月3日凌晨阿里云宕机引发事故公告

环信的小伙伴,您好,         首先,环信对2019年3月3日凌晨的事故为您造成的影响深表歉意,故障公告如下: 事故概述:        3月2日晚23:55,阿里云华北2地域ECS出现了大面积宕机,导致使用这些服务的环信即时通讯云北京集群和VIP...
继续阅读 »
环信的小伙伴,您好,

        首先,环信对2019年3月3日凌晨的事故为您造成的影响深表歉意,故障公告如下:

事故概述:

       3月2日晚23:55,阿里云华北2地域ECS出现了大面积宕机,导致使用这些服务的环信即时通讯云北京集群和VIP6集群服务不可用。
 



微信图片_20190304154115.jpg


(阿里云事故公告)



环信客服云的用户,如果添加了受影响的IM集群上的APP Key作为APP渠道接入的关联并且使用环信IM SDK,也会出现收发消息失败的现象。

使用系统默认的APP渠道接入关联,支持第二通道的客户互动云访客端SDK和网页端的环信客户互动云用户均不受影响。

处理过程:
  • 环信运维团队监控到异常后,第一时间联系了阿里云,在阿里云ECS服务全面恢复之前,利用少量可用的实例提前进行了服务恢复
  • 3月3日凌晨3:10,环信即时通讯云的北京集群和VIP6集群服务全部恢复
后续改进:
  • 我们会同阿里云一起排查事故原因,防止此类事故再次发生,对您造成的影响深表歉意,感谢您对环信的支持。

收起阅读 »

老司机带你一文读懂Android运行时权限

  老司机发车了,未到终点请勿下车. 嘟嘟嘟~~~  运行时权限从Android 6.0版本开始的,如果你的项目中 targetSdkVersion/compileSdkVersion 大于等于23,那么你就必须要考虑动态权限了。 权限又分为普通权限和危险权...
继续阅读 »
 
老司机发车了,未到终点请勿下车. 嘟嘟嘟~~~
 运行时权限从Android 6.0版本开始的,如果你的项目中 targetSdkVersion/compileSdkVersion 大于等于23,那么你就必须要考虑动态权限了。

权限又分为普通权限和危险权限。
普通权限如下:
android.permission.ACCESS LOCATIONEXTRA_COMMANDS 
android.permission.ACCESS NETWORKSTATE
android.permission.ACCESS NOTIFICATIONPOLICY
android.permission.ACCESS WIFISTATE
android.permission.ACCESS WIMAXSTATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE NETWORKSTATE
android.permission.CHANGE WIFIMULTICAST_STATE
android.permission.CHANGE WIFISTATE
android.permission.CHANGE WIMAXSTATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND STATUSBAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET PACKAGESIZE
android.permission.INTERNET
android.permission.KILL BACKGROUNDPROCESSES
android.permission.MODIFY AUDIOSETTINGS
android.permission.NFC
android.permission.READ SYNCSETTINGS
android.permission.READ SYNCSTATS
android.permission.RECEIVE BOOTCOMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST INSTALLPACKAGES
android.permission.SET TIMEZONE
android.permission.SET_WALLPAPER
android.permission.SET WALLPAPERHINTS
android.permission.SUBSCRIBED FEEDSREAD
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE SYNCSETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

普通权限是当需要用到时,只需要在清单文件中声明就可。危险权限除了需要在清单文件中声明外,还需在代码中动态进行判断申请。

危险权限如下:
android.permission-group.CALENDAR   
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR

android.permission-group.CAMERA
android.permission.CAMERA

android.permission-group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS

android.permission-group.LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION

android.permission-group.MICROPHONE
android.permission.RECORD_AUDIO

android.permission-group.PHONE
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
com.android.voicemail.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS

android.permission-group.SENSORS
android.permission.BODY_SENSORS

android.permission-group.SMS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
android.permission.READ_CELL_BROADCASTS

android.permission-group.STORAGE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE

危险权限是按组划分的,每一组中当某一个权限被允许或者拒绝后,同一组的其他权限也相应的自动允许或者拒绝。

当targetSdkVersion/compileSdkVersion大于等于23时,我们用到危险权限时,应按照这样的逻辑去处理。先判断当前应用是否具有权限,如果没有就去申请,当用户允许或者拒绝后,会回调相应的方法,我们在回调中处理自己的逻辑。


申请单个权限

在Activity/Fragment中,判断是否具有权限的方法是 checkSelfPermission(),申请权限的方法是requestPermissions(),然后用户允许或者拒绝后会回调方法onRequestPermissionsResult()。
虽然Activity/Fragment提供了这些方法,如果我们用这些的话,要判断安卓版本是否大于等于23,代码如下: 
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//动态请求权限

}else{
//直接去调用代码
}

 
谷歌工程师当然考虑到了这些,于是给开发者提供了兼容包,在Activity/Fragment中用ContextCompat.checkSelfPermission()判断是否具有权限;
在Activity中用 ActivityCompat.requestPermissions()请求权限;
在Fragment中直接用 requestPermissions()请求权限,不要在前面加上ActivityCompat,否则会回调Fragment所在Activity的回调方法;
在Activity/Fragment中用户允许或者拒绝权限后会回调onRequestPermissionsResult()方法。

下面看一个平常的调用系统相机拍照功能的代码:
private void takePhoto() {
Intent photoIn = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(photoIn, TAKE_PHOTO_REQUEST);
}

 
如果我们项目targetSdkVersion/compileSdkVersion大于等于23,那么就需要动态申请权限了:
if (ContextCompat.checkSelfPermission(MySetupActivity.this,  Manifest.permission.CAMERA)!=  PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MySetupActivity.this,new String[]{Manifest.permission.CAMERA}, 100);
} else {
takePhoto();
}

 
先判断是否具有权限,如果有直接去执行相关代码,没有则去申请权限。
当用户点击允许或者拒绝权限时,会回调onRequestPermissionsResult方法,我们在此方法中进行处理:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {

if (requestCode == 100) {//相机
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
takePhoto();
} else {
// Permission Denied
AlertDialog mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝权限,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(true)
.create();
mDialog.show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

当用户允许权限后我们直接执行相关代码,若拒绝则提示用户,弹窗提示是否需要开启权限。其中的
ShowAppSetDetails.showInstalledAppDetails(MySetupActivity.this, "user.zhuku.com");

是开启当前app信息设置界面的代码。

具体代码如下:
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
public class ShowAppSetDetails {
private static final String SCHEME = "package";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.1及之前版本)
*/
private static final String APP_PKG_NAME_21 = "com.android.settings.ApplicationPkgName";
/**
* 调用系统InstalledAppDetails界面所需的Extra名称(用于Android 2.2)
*/
private static final String APP_PKG_NAME_22 = "pkg";
/**
* InstalledAppDetails所在包名
*/
private static final String APP_DETAILS_PACKAGE_NAME = "com.android.settings";
/**
* InstalledAppDetails类名
*/
private static final String APP_DETAILS_CLASS_NAME = "com.android.settings.InstalledAppDetails";

/**
* 调用系统InstalledAppDetails界面显示已安装应用程序的详细信息。 对于Android 2.3(Api Level
* 9)以上,使用SDK提供的接口; 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)。
*
* @param context
* @param packageName 应用程序的包名
*/
public static void showInstalledAppDetails(Context context, String packageName) {
Intent intent = new Intent();
final int apiLevel = Build.VERSION.SDK_INT;
if (apiLevel >= 9) { // 2.3(ApiLevel 9)以上,使用SDK提供的接口
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts(SCHEME, packageName, null);
intent.setData(uri);
} else { // 2.3以下,使用非公开的接口(查看InstalledAppDetails源码)
// 2.2和2.1中,InstalledAppDetails使用的APP_PKG_NAME不同。
final String appPkgName = (apiLevel == 8 ? APP_PKG_NAME_22
: APP_PKG_NAME_21);
intent.setAction(Intent.ACTION_VIEW);
intent.setClassName(APP_DETAILS_PACKAGE_NAME,
APP_DETAILS_CLASS_NAME);
intent.putExtra(appPkgName, packageName);
}
context.startActivity(intent);
}
}

当app申请权限时,如果用户点击了“不再提醒”,则会直接回调拒绝权限,为此谷歌工程师提供了shouldShowRequestPermissionRationale()方法。兼容包中方法是ActivityCompat.shouldShowRequestPermissionRationale(),如果是在Fragment中使用请直接用shouldShowRequestPermissionRationale()。

ActivityCompat.shouldShowRequestPermissionRationale()方法返回值是boolean类型,当第一次申请权限时,此方法会返回false,如果用户点击“不再提醒”,则此方法会返回true。
 
详细代码如下:
if (ContextCompat.checkSelfPermission(MySetupActivity.this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(MySetupActivity.this, Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(MySetupActivity.this)
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
ActivityCompat.requestPermissions(MySetupActivity.this,
new String[]{Manifest.permission.CAMERA},
100);
}

} else {
takePhoto();
}

 
如果是在Fragment中,则代码如下:
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
//已经禁止提示了
mDialog = new AlertDialog.Builder(getContext())
.setTitle("友好提醒")
.setMessage("您已拒绝相机权限,此功能需要开启,是否开启?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}
})
.setNegativeButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.setCancelable(true)
.create();
mDialog.show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA},
PERMISSIONS_CAMERA);
}

} else {
selectPicFromCamera();
}

特别要注意的是,如果在Fragment中请求权限,若在Activity中也重写了onRequestPermissionsResult(),则onRequestPermissionsResult()方法中一定要写
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);

这句代码,若不写,则Activity不会分发执行Fragment中的权限回调方法。

因为Fragment中requestPermissions()源码如下:
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHost == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mHost.onRequestPermissionsFromFragment(this, permissions, requestCode);
}

其实在Fragment请求权限也是在它Activity中请求,只是把回调结果传递给了Fragment。


一次申请多个权限

例如需要申请的权限如下:
    /**
* 需要进行检测的权限数组
*/
protected String[] permissionList = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
};

 
我们要先判断每一个权限是否已经允许或者拒绝,当有某一个权限未被允许时,则申请未被允许的权限。
protected void onStart() {
super.onStart();

if (PermissionUtils.checkSelfPermission(SplashActivity.this, permissionList)) {
PermissionUtils.checkPermissions(this, 0, permissionList);
} else {
//处理业务逻辑
}

}

其中PermissionUtils类的代码如下:
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import java.util.ArrayList;
import java.util.List;

public class PermissionUtils {
/**
* 检查权限
*/
public static void checkPermissions(Activity activity, int permissRequestCode, String... permissions) {
List<String> needRequestPermissonList = findDeniedPermissions(activity, permissions);
if (null != needRequestPermissonList
&& needRequestPermissonList.size() > 0) {
ActivityCompat.requestPermissions(activity,
needRequestPermissonList.toArray(
new String[needRequestPermissonList.size()]),
permissRequestCode);
}
}

/**
* 获取权限中需要申请权限的列表
*/
public static List<String> findDeniedPermissions(Activity activity, String[] permissions) {
List<String> needRequestPermissonList = new ArrayList<String>();
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(activity,
perm) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissonList.add(perm);
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity, perm)) {
needRequestPermissonList.add(perm);
}
}
}
return needRequestPermissonList;
}

public static boolean checkSelfPermission(Context context, String[] permissions) {
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}

public static boolean checkSelfResult(int[] grantResults) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}

 
onRequestPermissionsResult回调方法中则这样处理:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (requestCode == 0) {
if (PermissionUtils.checkSelfResult(grantResults)) {
// Permission Granted
//处理业务逻辑
} else {
// Permission Denied

if (null == mDialog)
mDialog = new AlertDialog.Builder(SplashActivity.this)
.setTitle("友好提醒")
.setMessage("没有权限将不能更好的使用,请开启权限!")
.setPositiveButton("开启", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ShowAppSetDetails.showInstalledAppDetails(SplashActivity.this, "user.zhuku.com");
LogPrint.logILsj(TAG, "开启权限设置");
}
})
.setCancelable(false)
.create();

if (!mDialog.isShowing()) {
mDialog.show();
}
}
}
}

 
如果项目需要在页面可见时进行权限申请,请放在onStart()方法中,不要写在onResume()中。
可以想象一下,如果写在onResume()中,当用户同意了权限,则无碍,若是点击拒绝,则会回调权限拒绝的方法,这时系统申请权限的对话框消失不见,会再次调用onResume()请求权限显示对话框,若是还拒绝,则会再次调用onResume()方法,一直处于死循环。
因为系统请求权限的对话框其实一个开启了一个Activity。部分源码如下:  
 
 public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can reqeust only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}

至此,老司机本次发车已到终点,这一程体验,你还不会封装自己的运行时权限库吗?
 
 


qrcode_for_gh_08bfa7313fb2_258.jpg


微信公众号:IT大前端
关注可了解更多的大前端领域技术 收起阅读 »

2018,环信是如何C位出道的!(分享你和环信的故事赢千元奖励)

六年,筚路蓝缕,环信走过了一段从无至有的征程; 六年,栉风沐雨,见证了中国SaaS从0到1到1++的幸运; 六年,砥砺前行,从IM云1.0到IM云4.0,从移动客服到全媒体客服再到智能客服; 六年,峥嵘岁月,又一个全新的起点等待环信人去超越,从“心”出发...
继续阅读 »
六年,筚路蓝缕,环信走过了一段从无至有的征程;

六年,栉风沐雨,见证了中国SaaS从0到1到1++的幸运;

六年,砥砺前行,从IM云1.0到IM云4.0,从移动客服到全媒体客服再到智能客服;

六年,峥嵘岁月,又一个全新的起点等待环信人去超越,从“心”出发;

六载春秋,陪伴是最长情的告白!感恩有你!!!



新年广告_01.jpg




新年广告_02.jpg




新年广告_03.jpg




新年广告_05.jpg




新年广告_06.jpg




新年广告_07.jpg




新年广告_08.jpg




新年广告_09.jpg




新年广告_10.jpg




新年广告_11.jpg


分享你和环信的故事赢千元奖励

欢迎在评论区分享你和环信的故事,评论区点赞前3名各送200元京东卡一张,第4-10名各送100元京东卡一张。(春节后第一个工作日2月11日公布获奖名单)

评论地址:https://mp.weixin.qq.com/s/Tij4kpquSUSeB04lkcepXQ 收起阅读 »