楚楚街10亿元C轮融资,90后移动电商独角兽的背后!
楚楚街专注于商品特卖领域,定位为时尚化、年轻化、全球化的移动电商平台,提供的服务主要包括 “全球购”、“逛啦” 及 “限时抢购” 等板块。目前,楚楚街在移动端的安装量已经超过1亿,年销售额超过50亿,平均每天超过1300万成交额。近期楚楚街将推出“609”大促来回馈客户,谁都可以“买买买”,但不是谁都可以“买的漂亮”!
对于楚楚街来说,每天千万级的成交额意味着每天有百万量级的下单量。这么庞大的交易规模下,需要非常稳定且能支持千万级高并发的客服技术来支撑楚楚街数万家商户完成销售闭环,这其中环信移动客服功不可没。更重要的是,快速成长的楚楚街,在客服业务上的需求以及环信提供的全媒体智能云客服解决方案,对于其他电商平台具有很强的借鉴意义,所谓窥一斑而知全豹。
在即将到来的609年中购物嘉年华中,楚楚街将带来5亿红包的史上最大力度的优惠,而环信提供的全媒体智能云客服解决方案也与楚楚街联手,为本次的“嘉年华”助力。6月7日到6月9日期间,楚楚街13大分会场+3个特别会场将全面启动,满足用户“买买买”的夏日疯狂购物需求,而“千款单品秒杀,爆款商品凑单”的重磅优惠,更是最大程度为消费者谋取福利。
初衷:帮助商户构建APP内销售闭环,用环信移动客服升级服务
楚楚街决心在自己平台中,植入在线客服功能,为商户打造一个在APP内就能完成销售闭环的客服系统。顾客无需跳出APP,在商品展示页或是订单页即可一键呼唤客服,这样就使顾客和商家之间无缝连接,让交易更加简单。
将客服功能内置入APP中,最基础的一点是要求APP具备即时通讯功能,能够实时进行商家和顾客之家的图文消息传递。但楚楚街希望为商户打造的是一个专业的客服系统,这就要求有更多的专业功能。
梳理业务需求,确定客服功能需求
在确定植入在线客服系统后,下一步要做的就是根据楚楚街的业务模式,确定客服系统功能需求。对于楚楚街这样的电商平台而言,客服系统除了会话排队、客服质检等基础功能外,又有独特的功能需求:
· 多租户支持
由于楚楚街上有数万家商户,如果让每个商户自己搭建客服系统,不仅技术上难以实现,经济上也不现实。因此,楚楚街从一开始就选择了“统一平台接入,独立运作管理”的客服平台建设思路。所有商户共享同一客服平台,但每个商户又是以“租户”的形式独立运作管理的,这种集约化的建设方式为商户解决了技术和成本难题。
· 营销型客服
对电商而言,在线客服人员提供售后服务外,更多时候承担的是销售的角色,为客户提供售前咨询服务,这是和传统商业的一个很大不同。因此,在线客服系统应当从销售的角度,具备部分营销功能。
众多SaaS客服厂商,楚楚街为什么选择环信?
当前SaaS客服厂商众多,楚楚街在经过细致评比、测试后,最终选择环信。看中的是环信解决方案的先进性、稳定性以及与楚楚街电商经营理念的契合。
楚楚街选择的是环信多租户版的全媒体移动客服,可以在一个平台上对所有商户的客服进行管理,包括商户账号设置、权限分配等。同时,每个商户也可以自行进行配置调整,如客服人员数量,接待量设置等。这样既实现了统一部署的集约化优势,又满足商户的个性化需求。
对商户而言,无需任何技术投入,马上就可以享受全媒体客服带来的优势。首先是服务渠道的一体化,不论客户是来自楚楚街APP端,还是来自商户微信公众号、微博,甚至是商户自己门户网站,都可以实现客户请求的统一接入,统一分配和统一管理。其次是服务的专业化,可以进行客户画像、订单轨迹、服务质检、报表统计等,使商户拥有了媲美大型呼叫中心的的服务品质。环信提供的智能机器人更是帮助客服大幅降低了人工服务成本,80%的重复问题机器人都可以帮助解答。在下班时间,可以由智能机器人代替人工客服,提供7*24的不间断服务。
除了与楚楚街的客服需求匹配度最高外,环信还具备一系列与其它同类厂商相比的竞争优势:
· 独有的主动回呼技术,帮助商户实现轻松营销
环信基于自有的长连接技术优势,在移动客服产品上提供了一个主动外呼功能,帮助商户轻松实现客服营销。长连接技术的关键之处在于为每一个客户维持一个TCP连接,可以随时向客户端发送消息(当然,这种设计对后台系统资源和调度能力有很高要求)。客服人员可以把最新的促销以后台消息的形式发送给指定人群。例如,把一款儿童座椅的促销信息发给妈妈用户。如果客户端没有打开APP,那么会在系统通知一栏呈现出该消息,客户可以在闲暇时候再打开看。这是一种无干扰式的营销手段,配合客户画像,就能实现具有良好用户体验的精准式营销。
此外,主动回呼还可以帮助商家提高订单成交率。根据统计,楚楚街每日近百万订单中,问题订单大概占千分之五,也就是说每天就会产生数千个有问题的订单。有主动回呼功能,就可以在出席问题订单时,主动联系客户,从而提高成交率,同时提供了更好的客户体验。
· 强大技术支撑,满足楚楚街未来发展预期
随着交易平台的扩大,对客服系统的即时消息传递能力也将提出更高的要求。在所有SaaS客服厂商中,环信是唯一一家同时拥有即时通讯云PaaS产品和SaaS客服产品的厂商,即时通讯技术是环信的起家之本。截至2015年12月,环信即时通讯SDK已覆盖手机终端3.19亿,日发送消息2.1亿,这些数据展示了环信即时通讯技术的强劲性能和扩展性,这是和竞争对手在技术层面的最大差异,因为大多数友商并不具备即时通讯能力,需要借用第三方平台。对于楚楚街而言,选择环信,意味着性能将不会成为担心的问题,能够为楚楚街未来快速扩张扫除障碍。
· 历经市场检验,产品成熟度更高
楚楚街在产品选型时,除了考虑功能、性能外,还尤其关注产品的稳定性。在APP中,每一次和客服的对话对商户而言都是一个潜在的商机,一旦出现服务中断或是消息丢失,损失的不单是客户体验,更是销售机会。而环信在产品稳定性和成熟度上,则明显胜于竞品。根据易观智库2016年1月的数据,在移动端SaaS客服市场,环信的市场占有率高达77.4%,产品稳定性、成熟度已经经过了海量用户的检验。
帮助商户省钱、省时、省力,楚楚街成长为90后移动电商首个独角兽!
楚楚街能够持续增长,赢得资本青睐,背后靠的是数万家商户支撑起的商业生态。通过部署环信移动客服,楚楚街使自己平台的商业价值得以充分体现:
· 通过在一个平台内管理所有商户客服,楚楚街共创立商户客服账号20000多个,节省60%客服体系搭建成本。
· 强大的移动端客服SDK,保证了楚楚街高并发性、稳定性和可靠性,降低了20%的投诉率,提高了40%的访客转化率。
· 商户客服利用环信客服系统回呼500+次/日,减少退换货率79%。
对楚楚街而言,用移动客服帮助他的客户——也就是平台上数万家商户实现商业成功,是实现自己成功的基石,合作共赢,方能实现最大价值。 收起阅读 »
EMContactManager.getInstance().addContact 怎么判断是否发送成功啊
找一家靠谱SaaS服务商的五个关键因素
如今,企业都在争先恐后采用云来改造其混合架构以获取竞争优势,而SaaS(软件及服务)的出现使得企业能够轻松驾驭“云”这匹“快马”。SaaS目前被公认为最有效的帮助企业创新的方案,其通过分发IT和商业应用方案为企业解决问题,同时还能提供极佳的用户体验。
无论是通过公有云还是私有云,SaaS都能提供最及时的信息反馈和对资源的优化。而IT技术与业务范围实现精准对接使得使用者在云端拥有提供快速、高质量的应用开发的能力,这也让企业能够实现更高的运营效率。SaaS能够通过永远在线(always-on)、和即时启动(instant-on)应用分发管理工具帮助开发人员更快更高效地实现代码编写。
然而,企业管理者也不能过于随意地购买SaaS服务,您应当选择最适合您企业并能帮您的企业持续进步的服务供应商。
规划您自己的云服务实现路线
首先您要明白一点,实现混合云方案需要克服种种困难,这一点非常重要,而且您的云服务的实现路线与其他企业都会有差异。这条路线的规划受制于您的目标、成熟度、风险预测等等其他因素。所以从一开始您就要做好规划,以避免昂贵的试错成本。
挑选SaaS服务商 您需要关注哪些?
在挑选SaaS服务商的时候,您需要关注五个关键点,这五点都是SaaS应该能够给予您的看得见摸得着的对业务的帮助。在讨论这五点之前,我想要告诉您一个合格的SaaS服务提供商应该为您做什么?
一家合格的SaaS服务提供商应该能够接管您所有设施的维护和升级工作。他们应该能够高质量地保证服务的持续可用。
在选择SaaS服务商应关注以下这些方面
他们有稳健的全球性的服务拓展规划,以满足您企业在地区的和全球的商业需求;(环信全球拥有6个数据中心,包括北美、法兰克福、新加坡、日本、以及北京和杭州数据中心。海外AWS数据中心plus海外代理,拓展海外用户自此无忧。)
·拥有标准的、类似于Web服务的集成API,可以最大化地利用市场上可用的一切技术和能力;(环信免费提供上百种API功能,包括单聊、发送文字/表情/语音/地理位置/照片/视频/名片/自定义扩展消息、消息回执、集成第三方用户体系、群聊、离线消息、离线消息推送、实时音频/视频......)
·这个合作伙伴与您的业务共同成长进步,这样您就可以在需要获得服务时发挥非核心职能,同时能够获得最为精确的相应等级和数量的服务。(环信CSM团队和技术支持团队7*12小时帮助客服成功)
避免碎片化的SaaS解决方案极其关键,因为它们会导致您管理复杂度和成本的上升。您应该及时获取您所需要的服务,从而在操作成本和经济成本上的双重获益。
好的SaaS服务商应给予您的五大帮助
1、在您需要帮助的时候,它可以立刻给您相应的帮助。我们相信一个好的SaaS提供商应该保证您把所有注意力放在业务上,而从不需要为支持业务的软件和系统的维护发愁。需要什么,它就立刻能给您什么,不多也不少。
2、可以简便快速地访问SaaS服务。我们相信好的SaaS服务可以快速便捷的得以访问,就像使用内部应用一样。这些服务应该与已有的工作流程和谐共存。这一点的实现靠的是SaaS的提供商能够简化业务的集成流程,在增加SaaS服务的同时保护您对于已有业务的投资,并扩展您的混合网络架构。
3、保证您一直能用上最新的软件,没有延迟,也没有升级带来的麻烦。企业应用的升级通常既费时又费钱,因此许多时候因为困难和经费导致升级不及时,企业就没有办法享受到最新的功能。
4、选择已获得安全认证的企业。我们承认找一家世界级的SaaS服务提供商利用其数十年的专业运营经验来提供企业级的服务的要求有点离谱了,很少有供应商能有10年以上的SaaS服务经验并拥有服务多个客户的全球数据中心,且能够立即扩展满足您的业务需求。
5、在数据中心的管理之中追求最高服务质量和创新。我们相信数据中心的建设和管理实践对于维持最佳的服务和创新同等关键,您应该充分信任SaaS提供商的专业能力,而不是对于所有事情都亲力亲为。(环信编译自 http://community.hpe.com/ 如有转载请注明出处。)
环信成立于2013年4月,是一家全通讯能力云服务提供商。产品包括全球最大的即时通讯云PaaS平台——环信即时通讯云,以及全球首创的全媒体智能云客服平台——环信移动客服。现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、快牙、随手记、猎聘、海尔、神州专车等。截至2015年底,环信共服务了50833家 App 客户,SDK覆盖手机终端3.19亿台,平台日均发送消息2.1亿条。
收起阅读 »
Fmpeg惊爆漏洞:环信现有用户完全不受此漏洞影响!
据国内媒体报道,近日全球领先的多媒体框架FFmpeg被曝出漏洞,通过该漏洞可在播放漏洞视频或在转码过程中触发本地文件,读取获得指定文件。FFmpeg已于4月发布更新,但仍有大量Android及iOS APP使用该开源程序用于播放功能。因为开放源码的便利性和强大的多媒体功能,FFmpeg被广泛用于Android及iOS APP的播放功能,百度云、爱奇艺视频、网易云音乐、斗鱼TV、疯狂猜词等多款手机用户常用的APP均使用了FFmpeg库文件,大量用户可能受此漏洞威胁。
使用率最高的FFmpeg文件库top10
市场占有率高真不是宝宝的错
360互联网安全中心对国内主流应用市场的124371款app进行扫描,发现有超过6000款应用受此漏洞影响,占到总数的5%,受影响APP类型涵盖各个类型,其中仅通讯社交、便捷生活、影音视听三类就占到一半以上,进一步对受漏洞影响的6314款app所使用的ffmepg库文件分析发现,使用率最高的libeasemod_jni.so属于环信SDK的库文件,排名第二的libcyberplay-core.so均为目前最流行的视频SDK,拥有千万级的用户量。
本地打开m3u8文件并使用FFmpeg解析时就会触发漏洞
该漏洞与HLS协议的.m3u8文件相关。攻击者可以制作一个特殊的.m3u8或其他视频文件,当ffmpeg播放此特殊文件时会把本地文件内容传送到远程服务器上。
环信SDK只使用FFmpeg作解码和录制之用,没有包含HLS相关功能,也没有对外提供任何播放视频文件的api,所以该漏洞对环信用户是没有任何影响的!
环信音视频专家通过跟360互联网安全中心工程师沟通发现,360互联网安全中心是通过检测FFmpeg版本号来判断app是否受此漏洞影响,由于环信SDK所用的FFmpeg版本较老,被360互联网安全中心误认为受此漏洞影响。
综上,环信现有用户完全不受此漏洞影响!!!环信现有用户完全不受此漏洞影响!!!环信现有用户完全不受此漏洞影响!!!重要事情说三遍!!!
后续环信的SDK也会接受360互联网安全中心建议升级到FFmpeg新版本。
漏洞新闻: http://www.ccstock.cn/finance/minshengxiaofei/2016-05-24/A1464060408687.html
俄罗斯工程师最先发现此漏洞新闻:https://habrahabr.ru/company/mailru/blog/274855/ 收起阅读 »
android中如何显示开发者服务器上的昵称和头像
本文方法已经废弃!请看作者另外一篇文章:
在android中5分钟实现昵称头像的显示 http://www.imgeek.org/article/825308757
无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称和头像。运行环信的demo app,注册用户是直接使用环信ID(username)作为用户名,但是在我们实际应用中,需要将自有用户体系的UserId生成GUID作为环信ID(username)【参考:http://docs.easemob.com/im/100serverintegration/20users】,这时候如果不经过处理,则会显示如下界面:
那么如何处理,才能显示正确的用户昵称和头像呢?
其实官方已经提供有解决方案了,只不过没有给出示例代码而已。
http://docs.easemob.com/im/490integrationcases/10nickname
引用一下关键文字:
方法二:从消息扩展中获取昵称和头像
昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。
昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。
没错,官方提供了两种思路,鉴于项目的实际情况,我选择了【方法二】。
于是,无论安卓,还是IOS,我们都是这样处理的:
【1】.APP间传递用户属性信息:发送(文本、图片...)消息时,要在消息扩展(message.ext)中附带当前用户的属性信息;
【2】.本地缓存用户信息:在接收消息的回调函数里,读取消息扩展(message.ext)里用户属性(键值对)信息;如果本地缓存(sqlite)不存在该用户,则新增缓存记录,如果存在,则更新记录;(用户登录或注册成功后,也要更新用户缓存信息)
【3】.获取用户属性信息:在需要显示昵称的地方,根据环信ID,读取sqlite缓存数据,获取用户昵称和头像;
用户属性信息:ChatUserId(环信ID),ChatUserNick(用户昵称), ChatUserPic(用户头像,完整的url地址);
关键代码:
/***************** 环信用户缓存信息 *******************************/
public static final String ChatUserId = "ChatUserId";// 用户的环信ID
public static final String ChatUserPic = "ChatUserPic";
public static final String ChatUserNick = "ChatUserNick";
/***************** 环信用户缓存信息***********end ********************/
UserInfoCacheSvc.java是环信用户信息缓存管理类
/**首先要在用户登录或注册成功后,返回用户登录信息Model时,缓存一下用户信息,@DatabaseField注解是ormlite库的特性,文章后面的附件包含了此类库:
* 缓存用户信息(主要用于聊天显示昵称和头像)
*/
public class UserInfoCacheSvc {
public static List<UserApiModel> getAllList(){
Dao<UserApiModel, Integer> daoScene = SqliteHelper.getInstance().getUserDao();
try {
List<UserApiModel> list = daoScene.queryBuilder().query();
return list;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static UserApiModel getByChatUserName(String chatUserName){
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
try {
UserApiModel model = dao.queryBuilder().where().eq("EaseMobUserName", chatUserName).queryForFirst();
return model;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static UserApiModel getById(long id){
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
try {
UserApiModel model = dao.queryBuilder().where().eq("Id", id).queryForFirst();
return model;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean createOrUpdate(String chatUserName, String userNickName, String avatarUrl){
try {
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
UserApiModel user = getByChatUserName(chatUserName);
int changedLines = 0;
if (user == null){
user = new UserApiModel();
user.setUsername(userNickName);
user.setHeadImg(avatarUrl);
user.setEaseMobUserName(chatUserName);
changedLines = dao.create(user);
}else {
user.setUsername(userNickName);
user.setHeadImg(avatarUrl);
user.setEaseMobUserName(chatUserName);
changedLines = dao.update(user);
}
if(changedLines > 0){
Log.i("UserInfoCacheSvc", "操作成功~");
return true;
}
} catch (SQLException e) {
e.printStackTrace();
Log.e("UserInfoCacheSvc", "操作异常~");
}
return false;
}
public static boolean createOrUpdate(UserApiModel model){
if(model == null) return false;
try {
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
UserApiModel user = getById(model.Id);
if (!StringUtils.isNullOrEmpty(model.getHeadImg())){
String fullPath = "http://image.baidu.com" + model.getHeadImg();
//特别注意:这里用是图片的完整链接地址,如果要取缩略图,需要服务端配合;
model.setHeadImg(fullPath);
}
int changedLines = 0;
if (user == null){
changedLines = dao.create(model);
}else {
model.setRecordId(user.getRecordId());
changedLines = dao.update(model);
}
if(changedLines > 0){
Log.i("UserInfoCacheSvc", "操作成功~");
return true;
}
} catch (SQLException e) {
e.printStackTrace();
Log.e("UserInfoCacheSvc", "操作异常~");
}
return false;
}
}
public class UserApiModel implements Serializable {
@DatabaseField(generatedId=true)
private int RecordId;
@DatabaseField
public long Id;
@DatabaseField
public String Username;
@DatabaseField
public String Email;
@DatabaseField
public String HeadImg;
@DatabaseField
public String EaseMobUserName;
@DatabaseField
public String EaseMobPassword;
}
private static void SaveUserInfo(UserApiModel userInfo){然后在接收环信消息的回调函数里保存用户信息,ActyMain.java是我们项目的主框架,我们在这里写了回调函数,无论群聊还是单聊消息,都会调用这里:
if(userInfo == null) return;
// 缓存用户信息
PrefUtils.setUserId(userInfo.Id);
PrefUtils.setUserEmail(userInfo.Email);
PrefUtils.setUserName(userInfo.Username);
PrefUtils.setUserPic(userInfo.HeadImg);
PrefUtils.setUserChatId(userInfo.EaseMobUserName);
PrefUtils.setUserChatPwd(userInfo.EaseMobPassword);
UserInfoCacheSvc.createOrUpdate(userInfo);
}
private EMMessageListener mMessageListener = new EMMessageListener() {在安卓项目里,环信页面显示昵称和头像时,都会统一从DemoHelper的getUserInfo函数里获取信息,所以我们要在这里从缓存取用户头像和昵称:
@Override
public void onMessageReceived(List<EMMessage> messages) {
// 提示新消息
for (EMMessage message : messages) {
// 先将头像和昵称保存在本地缓存
try {
String chatUserId = message.getStringAttribute(SharePrefConstant.ChatUserId);
String avatarUrl = message.getStringAttribute(SharePrefConstant.ChatUserPic);
String nickName = message.getStringAttribute(SharePrefConstant.ChatUserNick);
UserInfoCacheSvc.createOrUpdate(chatUserId, nickName, avatarUrl);
} catch (HyphenateException e) {
e.printStackTrace();
}
ChatHelper.getInstance().getNotifier().onNewMsg(message);
}
refreshUIWithMessage();
}
// 此处省略N行代码....
};
private EaseUser getUserInfo(String username){最后,为了让另外一个客户端也能正确显示头像和昵称,app发送消息时,要在消息扩展里附带用户信息,代码写在
//获取user信息,demo是从内存的好友列表里获取,
//实际开发中,可能还需要从服务器获取用户信息,
//从服务器获取的数据,最好缓存起来,避免频繁的网络请求
EaseUser user = null;
// 从缓存里取昵称和头像
UserApiModel userInfo = UserInfoCacheSvc.getByChatUserName(username);
if (userInfo != null){
user = new EaseUser(username);
user.setAvatar(userInfo.getHeadImg());
user.setNick(userInfo.getUsername());
}
return user;
}
ChatFragment.java里:
@Override有不当之处,欢迎指正~~~谢谢
public void onSetMessageAttributes(EMMessage message) {
setUserInfoAttribute(message);
}
/**
* 设置用户的属性,
* 通过消息的扩展,传递客服系统用户的属性信息
* @param message
*/
private void setUserInfoAttribute(EMMessage message) {
try {
message.setAttribute(SharePrefConstant.ChatUserId, PrefUtils.getUserChatId());
message.setAttribute(SharePrefConstant.ChatUserNick, PrefUtils.getUserName());
message.setAttribute(SharePrefConstant.ChatUserPic, "http://image.baidu.com" + PrefUtils.getUserPic()) ;//这里用是图片的完整链接地址,如果要取缩略图,需要服务端配合;
} catch (Exception e) {
e.printStackTrace();
}
}
部分源码因为是公司项目代码,所以没有在文章里提供(而且代码量太多,也不便于阅读)。需要源码的童鞋,可以联系我。等我有空,我整理一下公司项目,然后再share开源给大家。
相关类文件下载:
本文方法已经废弃!请看作者另外一篇文章:
在android中5分钟实现昵称头像的显示 http://www.imgeek.org/article/825308757
如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063,
或者加本人QQ:364223587,加Q请认准以下正宗小马头像:
收起阅读 »
IOS中如何显示开发者服务器上的昵称和头像
那么如何处理,才能显示正确的用户昵称和头像呢?
其实官方已经提供有解决方案了,只不过没有给出示例代码而已。
http://docs.easemob.com/im/490integrationcases/10nickname
引用一下关键文字:
方法二:从消息扩展中获取昵称和头像
昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。
昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。
没错,官方提供了两种思路,鉴于项目的实际情况,我选择了【方法二】。
于是,无论安卓,还是IOS,我们都是这样处理的:
【1】.APP间传递用户属性信息:发送(文本、图片...)消息时,要在消息扩展(message.ext)中附带当前用户的属性信息;
【2】.本地缓存用户信息:在接收消息的回调函数里,读取消息扩展(message.ext)里用户属性(键值对)信息;如果本地缓存(sqlite)不存在该用户,则新增缓存记录,如果存在,则更新记录;(用户登录或注册成功后,也要更新用户缓存信息)
【3】.获取用户属性信息:在需要显示昵称的地方,根据环信ID,读取sqlite缓存数据,获取用户昵称和头像;
用户属性信息:ChatUserId(环信ID),ChatUserNick(用户昵称), ChatUserPic(用户头像,完整的url地址);
IOS关键代码:
// 环信聊天用的昵称和头像(发送聊天消息时,要附带这3个属性)
#define kChatUserId @"ChatUserId"// 环信账号
#define kChatUserNick @"ChatUserNick"
#define kChatUserPic @"ChatUserPic"
ChatUserCacheInfo是环信用户信息缓存管理类
ChatUserCacheInfo.h
#import <Foundation/Foundation.h>
@interface ChatUserCacheInfo : NSObject
@property(nonatomic,copy)NSString* Id;
@property(nonatomic,copy)NSString* NickName;
@property(nonatomic,copy)NSString* AvatarUrl;
@end
@interface ChatUserCacheUtil : NSObject
+(void)saveInfo:(NSString *)openId
imgId:(NSString*)imgId
nickName:(NSString*)nickName;
+(void)saveDict:(NSDictionary *)userinfo;
+(void)saveModel:(UserApiModel*)user;
+(ChatUserCacheInfo*)queryById:(NSString *)userid;
@end
ChatUserCacheUtil.m
#import "ChatUserCacheUtil.h"首先要在用户登录或注册成功后,返回用户登录信息时,缓存一下用户信息:
#import "FMDB.h"
#define DBNAME @"cache_data.db"
@implementation ChatUserCacheInfo
@end
@implementation ChatUserCacheUtil
+(void)createTable:(FMDatabase *)db
{
if ([db open]) {
if (![db tableExists :@"userinfo"]) {
if ([db executeUpdate:@"create table userinfo (userid text, username text, userimage text)"]) {
NSLog(@"create table success");
}else{
NSLog(@"fail to create table");
}
}else {
NSLog(@"table is already exist");
}
}else{
NSLog(@"fail to open");
}
}
+ (void)clearTableData:(FMDatabase *)db
{
if ([db executeUpdate:@"DELETE FROM userinfo"]) {
NSLog(@"clear successed");
}else{
NSLog(@"fail to clear");
}
}
+(FMDatabase*)getDB{
NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docsPath stringByAppendingPathComponent:DBNAME];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
[self createTable:db];
return db;
}
+(void)saveModel:(UserApiModel*)user{
[ChatUserCacheUtil saveInfo:user.EaseMobUserName imgId:user.HeadImg nickName:user.Username];
}
+(void)saveInfo:(NSString *)openId
imgId:(NSString*)imgId
nickName:(NSString*)nickName{
NSMutableDictionary *extDic = [NSMutableDictionary dictionary];
[extDic setValue:openId forKey:kChatUserId];
[extDic setValue:@"[url=http://img.baidu.com"]http://img.baidu.com/"[/url]+imgId forKey:kChatUserPic];//完整图片路径"http://img.baidu.com/1234"。如果imgId是相对路径,那完整路径就是类似"http://img.baidu.com/abc.jpg"
[extDic setValue:nickName forKey:kChatUserNick];
[ChatUserCacheUtil saveDict:extDic];
}
+(void)saveDict:(NSDictionary *)userinfo{
FMDatabase *db = [self getDB];
NSString *userid = [userinfo objectForKey:kChatUserId];
if ([db executeUpdate:@"DELETE FROM userinfo where userid = ?", userid]) {
DLog(@"删除成功");
}else{
DLog(@"删除失败");
}
NSString *username = [userinfo objectForKey:kChatUserNick];
NSString *userimage = [userinfo objectForKey:kChatUserPic];
if ([db executeUpdate:@"INSERT INTO userinfo (userid, username, userimage) VALUES (?, ?, ?)", userid,username,userimage]) {
DLog(@"插入成功");
}else{
DLog(@"插入失败");
}
// NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]);
FMResultSet *rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo where userid = ?",userid];
if ([rs next]) {
NSString *userid = [rs stringForColumn:@"userid"];
NSString *username = [rs stringForColumn:@"username"];
NSString *userimage = [rs stringForColumn:@"userimage"];
DLog(@"查询一个 %@ %@ %@",userid,username,userimage);
}
rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo"];
while ([rs next]) {
NSString *userid = [rs stringForColumn:@"userid"];
NSString *username = [rs stringForColumn:@"username"];
NSString *userimage = [rs stringForColumn:@"userimage"];
DLog(@"查询所有 %@ %@ %@",userid,username,userimage);
}
[rs close];
// NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]);
[db close];
}
+(ChatUserCacheInfo*)queryById:(NSString *)userid{
FMDatabase *db = [self getDB];
if ([db open]) {
FMResultSet *rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo where userid = ?",userid];
if ([rs next]) {
ChatUserCacheInfo *userInfo = [[ChatUserCacheInfo alloc] init];
userInfo.Id = [rs stringForColumn:@"userid"];
userInfo.NickName = [rs stringForColumn:@"username"];
userInfo.AvatarUrl = [rs stringForColumn:@"userimage"];
DLog(@"查询一个 %@",userInfo);
return userInfo;
}else{
return nil;
}
}else{
return nil;
}
}
@end
@protocol UserApiModel <NSObject>
@end
@interface UserApiModel : BaseJSONModel
@property(nonatomic, assign)int Id;
@property(nonatomic, copy)NSString *Username;
@property(nonatomic, copy)NSString *Email;
@property(nonatomic, copy)NSString *HeadImg;
@property(nonatomic, copy)NSString *EaseMobUserName;
@property(nonatomic, copy)NSString *EaseMobPassword;
@end
// 登录成功后返回用户model,需要为环信聊天窗口缓存用户信息
[ChatUserCacheUtil saveModel:user];
然后在接收环信消息的回调函数里保存用户信息,HsMainViewController.m是我们项目的主框架,我们在这里写了回调函数,无论群聊还是单聊消息,都会调用这里:
// 收到消息回调环信页面主要是在ChatViewController和ConversationListViewController里显示用户对话,所以我们要在这两个页面里从缓存取用户头像和昵称,先从ChatViewController开始:
-(void) didReceiveMessage:(EMMessage *)message
{
[ChatUserCacheUtil saveDict:message.ext];
BOOL needShowNotification = (message.messageType != eMessageTypeChat) ? [self needShowNotification:message.conversationChatter] : YES;
if (needShowNotification) {
#if !TARGET_IPHONE_SIMULATOR
BOOL isAppActivity = [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
if (!isAppActivity) {
[self showNotificationWithMessage:message];
}else {
[self playSoundAndVibration];
}
#endif
}
}
- (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController然后是ConversationListViewController:
modelForMessage:(EMMessage *)message
{
id<IMessageModel> model = [[EaseMessageModel alloc] initWithMessage:message];
ChatUserCacheInfo *userinfo = [ChatUserCacheUtil queryById:model.nickname];
if (userinfo != nil) {
model.nickname = userinfo.NickName;
model.avatarURLPath = userinfo.AvatarUrl;
}
model.avatarImage = [UIImage imageNamed:@"EaseUIResource.bundle/user"];
model.failImageName = @"imageDownloadFail";
return model;
}
#pragma mark - EaseConversationListViewControllerDataSource最后,为了让另外一个客户端也能正确显示头像和昵称,app发送消息时,要在消息扩展里附带用户信息,代码写在EaseSDKHelper.m里:
- (id<IConversationModel>)conversationListViewController:(EaseConversationListViewController *)conversationListViewController
modelForConversation:(EMConversation *)conversation
{
EaseConversationModel *model = [[EaseConversationModel alloc] initWithConversation:conversation];
if (model.conversation.conversationType == eConversationTypeChat) {
ChatUserCacheInfo *userinfo = [ChatUserCacheUtil queryById:model.conversation.chatter];
if (userinfo != nil) {
model.title = userinfo.NickName;
model.avatarURLPath = userinfo.AvatarUrl;
}
model.avatarImage = PlaceholderImgChatUser;
} else if (model.conversation.conversationType == eConversationTypeGroupChat) {
// 此处省略100行代码........
}
}
// 重新消息扩展组织
+(NSMutableDictionary*)reGetMessageExt:(NSDictionary *)messageExt{
NSMutableDictionary *extDic = [NSMutableDictionary dictionaryWithDictionary:messageExt];
[extDic setValue:[SettingData share].UserChatId forKey:kChatUserId];
[extDic setValue:[SettingData share].UserHeadImg.ServerThumbUrlStr forKey:kChatUserPic];
[extDic setValue:[SettingData share].UserName forKey:kChatUserNick];
return extDic;
}
+ (EMMessage *)sendTextMessage:(NSString *)text
to:(NSString *)toUser
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
{
// 表情映射。
NSString *willSendText = [EaseConvertToCommonEmoticonsHelper convertToCommonEmoticons:text];
EMChatText *textChat = [[EMChatText alloc] initWithText:willSendText];
EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithChatObject:textChat];
EMMessage *message = [[EMMessage alloc] initWithReceiver:toUser bodies:[NSArray arrayWithObject:body]];
message.requireEncryption = requireEncryption;
message.messageType = messageType;
message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:nil];
return retMessage;
}
+ (EMMessage *)sendImageMessageWithImage:(UIImage *)image
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
return [self sendImageMessageWithImage:image to:to messageType:messageType requireEncryption:requireEncryption messageExt:messageExt quality:0.6 progress:progress];
}
+ (EMMessage *)sendImageMessageWithImage:(UIImage *)image
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
quality:(float)quality
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略9行代码....
message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];
return retMessage;
}
+ (EMMessage *)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....
message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];
return retMessage;
}
+ (EMMessage *)sendVideoMessageWithURL:(NSURL *)url
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....
message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];
return retMessage;
}
+ (EMMessage *)sendFileMessage:(EMChatFile *)chatFile
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....
message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];
return retMessage;
}
有不当之处,欢迎指正~~谢谢。QQ:364223587
以上代码为SDK V2版本,如果集成的是V3版本,请移步源码:
http://git.oschina.net/markies/ChatDemo-UI3.00-Simple
思路其实跟V2差不多,最大区别V3的回调方法didReceiveMessages比V2多了个【s】。
ChatUserCacheUtil我已经重命名为:UserCacheManager
如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063
收起阅读 »
环信编程大赛优秀开源项目系列之二:“图忆”一款基于地理位置信息的社交APP
5月14日,由环信联合猿圈共同推出的“首届环信编程大赛”颁奖典礼在中关村义创空间隆重举行。本次环信编程大赛历时两个月,由线上初赛、决赛和颁奖典礼三个环节组成,总计报名人数2000+,收到决赛项目100+。最终由评委会认定的13个优秀开源项目及开发者集体亮相颁奖典礼。其中“方圆十里”、“高仿微信“和“咚咚”三个开源项目名列前三,共同分享了15000元奖金和价值12000元的专属表情包。
优秀项目开发者合影
“图忆”项目负责人梁桂栋分享技术开发细节
其余入围的十余个优秀开源项目同样引起了到场开发者的热烈追捧,环信将分期将入围的优秀项目代码免费开源给小伙伴们。今天我们带来的是一款基于地理位置信息的社交分享应用——“图忆”。图忆是一款基于地理位置信息的社交分享应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投的好友,并且用户记事可以分享到公共社区平台,分享乐趣的同时也发现了更多的乐趣,社区推荐策略让用户发现更多有价值的乐趣。
“图忆”APP界面截图
1.软件介绍
图忆是一款基于地理位置信息的社交分享应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投的好友,并且用户记事可以分享到公共社区平台,分享乐趣的同时也发现了更多的乐趣,社区推荐策略让用户发现更多有价值的乐趣。
2.功能介绍
【记录记忆】你可以记录自己的生活点滴在地图之上,可以公开给别人看,也可以保存为自己的私有记忆。
【离线记录】没有网络也可以轻松保存离线记录,WIFI连接后直接批量上传,省心
【地图附近】你将通过地图查看到附近用户公开的说有分享记录,当然是直接在地图上展示的哟,很直观的说,还有五个标签分类查询哟,就等你来发现了。
【雷达】发现同时在附近开启雷达的小伙伴,自定义雷达显示的内容,让小伙伴更容易发现你
【聊天圈子】与TA尽情畅聊,兴趣小伙伴建圈子一起聊。
【图忆社区】点赞,评论,分享,收藏Ta的分享
3.使用技术
环信IM
百度地图API
有盟API
4.作者心得
IM正越来越得到开发者重视,也逐渐成为APP标配,绝大部分App中都集成了即时通讯功能。将APP的核心功能紧密与即时通讯良好结合,将更有利于APP的用户体验和留存。
APP的多元发展中需要使用多功能的有机结合。而作为一个完整的SDK需要越少的干涉APP原本的逻辑,而不降低功能与体验,这些方面环信的IM SDK都做的挺好。
特别感谢以下企业的大力支持:
义创空间提供颁奖场地
萌岛从自有形象库中授权一套价值12000元的表情包
Emokit赞助Apple Watch一台
猿圈全程提供技术评测支持
git源码下载https://github.com/donlan/Tuyi
更多开源项目请点击http://community.easemob.com/article/825307813
图忆项目作者演讲PPT下载↓↓↓
收起阅读 »
开源了一个简单实用的http服务压力测试工具Alex,自带web ui,使用golang实现
=================
Alex是基于vegeta library和boom封装的压力测试web UI。Vegeta提供稳定的qps压力源,boom提供稳定的并发数压力源。
github地址 https://github.com/ireaderlab/alex
English
Alex架构图
Alex ArchitectureAlex 主要功能
1. 保存压力测试参数以便反复压测
2. 保存压力测试报告以便后续查看和分享
3. 提供了简单直接的图形和文字报告
4. 可以同时对多个http接口进行压力测试
5. 可以同时对集群内多个host:port对进行压测
6. 使用多组调用参数避免压测时出现的数据热点问题
7. 使用步骤设置,生成渐进式的压力源
8. 提供简单的压测机器系统状态实时显示功能
Alex Limitations
1. Alex运行在单一进程里,如果你需要分布式的压测环境,就得部署多个节点,压测时需要多人同时操作。
2. Vegeta在压力过载时没有提供立即停止的方法。这就需要你细心设计压测步骤,仔细观察系统状态避免系统过载。
3. Qps和并发数不宜过大。我曾经使用Alex工具单进程测试了HelloWorld的web程序每个请求吐出1500字节,qps最多可以达到60000,基本让千兆网卡打满。
4. 在大型压力测试下,尽量避免Gzip解压缩。解压缩会消耗大量的cpu资源,会导致压测报告不准确。你可以通过部署多个节点来进行大型压力测试。
5. 只支持Http协议。Https协议不打算支持,因为加密解密也同样会消耗大量cpu资源,导致报告不准确。
6. 报告只是提供一种性能参考,要勇于对报告进行质疑。
7. Alex虽然有如此诸多限制,这不影响它的日常使用。
安装
install mongodb
install golang # 1.4+ is required
go get github.com/go-martini/martini
go get github.com/tsenart/vegeta
go get gopkg.in/mgo.v2
go get github.com/shirou/gopsutil # godep restore
git clone https://github.com/shellquery/alex.git
cd alex
go build
./alex
./alex -c config.json
open browser http://localhost:8000/
配置
config.json{
"BindAddr": "localhost:8000",
"MongoUrl": "mongodb://localhost:27017/alex",
"Teams": [ "python", "java", "php", "go" ]
}
引用
棒棒的vegeta https://github.com/tsenart/vegeta
简单直接的boom https://github.com/rakyll/boom
截屏
Randomize Host:ports
Randomize Parameters
Step Settings
Benchmark Reports 收起阅读 »
【公告】IOS SDK 2.2.5版本已支持IPV6-only
因为6.1日起苹果要求IPV6-only,不支持ipv6的app将无法通过appstore审核。我们于昨天(5.18)发布了支持ipv6版本的IOS SDK 2.2.5版本
建议进行升级,以免因为这个问题导致app新版本通不过苹果审核。
新版本介绍http://community.easemob.com/article/825307836
新版SDK下载http://www.easemob.com/download/im
13个基于环信集成的开源项目集体登场http://community.easemob.com/article/825307813 收起阅读 »
优秀程序员的十个习惯
学无止境
- 学无止境。就算是你有了10年以上的程序员经历,你也得要使劲地学习,因为你在计算机这个充满一创造力的领域,每天都会有很多很多的新事物出现。你需要跟上时代的步伐。你需要去了解新的程序语言,以及了解正在发展中的程序语言,以及一些编程框架。还需要去阅读一些业内的新闻,并到一些热门的社区去参与在线的讨论,这样你才能明白和了解整个软件开发的趋势。在国内,一些著名的社区例如:CSDN,ITPUB,CHINAUINX等等,在国外,建议你经常上一上digg.com去看看各种BLOG的聚合。
掌握多种语言
- 掌握多种语言。程序语言总是有其最适合的领域。当你面对需要解决的问题时,你需要找到一个最适合的语言来解决这些问题。比如,如果你需要性能,可能C/C++是首选,如果你需要跨平台,可能Java是首选,如果你要写一个Web上的开发程序,那么PHP,ASP,Ajax,JSP可能会是你的选择,如果你要处理一些文本并和别的应用交互,可能Perl, Python会是最好的。所以,花一些时间去探索一下其它你并熟悉的程序语言,能让你的眼界变宽,因为你被武装得更好,你思考问题也就更为全面,这对于自己和项目都会有好的帮助。
理性面对不同的操作系统或技术
- 理性面对不同的操作系统或技术。程序员们总是有自己心目中无可比拟的技术和操作系统,有的人喜欢Ubuntu,有的人喜欢Debian,还有的人喜欢Windows,以及FreeBSD,MacOSX或Solaris等等。只有一部分优秀的程序员明白不同操作系统的优势和长处和短处,这样,在系统选型的时候,才能做到真正的客观和公正,而不会让情绪影响到自己。同样,语言也是一样,有太多的程序员总是喜欢纠缠于语言的对比,如:Java和Perl。哪个刚刚出道的程序员没有争论去类似的话题呢?比如VC++和Delphi等等。争论这些东西只能表明自己的肤浅和浮燥。优秀的程序并不会执着于这些,而是能够理性的分析和理心地面对,从而才能客观地做出正确的选择。
- 别把自己框在单一的开发环境中。 再一次,正如上面所述,每个程序员都有自己忠爱的工具和技术,有的喜欢老的(比如我就喜欢Vi编辑程序),而有的喜欢新的比如gedit或是Emacs等。有的喜欢使用像VC++一样的图形界面的调试器,而我更喜欢GDB命令行方面的调式器。等等等等。程序员在使用什么样的工具上的争论还少吗?到处都是啊。使用什么样的工具本来无所谓,只要你能更好更快地达到你的目的。但是有一点是优秀程序员都应该了解的——那就是应该去尝试一下别的工作环境。没有比较,你永远不知道谁好谁不好,你也永远不知道你所不知道的。
使用版本管理工具管理你的代码。
- 使用版本管理工具管理你的代码。千万不要告诉我你不知道源码的版本管理,如果你的团队开发的源代码并没有版本管理系统,那么我要告诉你,你的软件开发还处于石器时代。赶快使用一个版式本管理工具吧。CVS 是一个看上去平淡无奇的版本工具,但它是被使用最广的版本管理系统,Subversion 是CVS的一个升级版,其正在开始接管CVS的领地。Git 又是一个不同的版本管理工具。还有Visual SourceSafe等。使用什么样的版本管理工具依赖于你的团队的大小和地理分布,你也许正在使用最有效率或最没有效率的工具来管理你的源代码。但一个优秀的程序员总是会使用一款源码版本管理工具来管理自己的代码。如果你要我推荐一个,我推荐你使用开源的Subversion。
是一个优秀的团队成员
- 是一个优秀的团队成员。 除非你喜欢独奏,除非你是孤胆英雄。但我想告诉你,今天,可能没有一个成熟的软件是你一个人能做的到的,你可能是你团队中最牛的大拿,但这并不意味着你就是好的团队成员。你的能力只有放到一个团队中才能施展开来。你在和你的团队成员交流中有礼貌吗?你是否经常和他们沟通,并且大家都喜欢和你在一起讨论问题?想一想一个足球队吧,你是这个队中好的成员吗?当别人看到你在场上的跑动时,当别人看到你的传球和接球和抢断时,你的团员成员能因为你的动作受到鼓舞吗?
- 把你的工作变成文档。 这一条目当然包括了在代码中写注释,但那还仅仅不够,你还需要做得更多。有良好的注释风格的代码是一个文档的基础,他能够让你和你的团队容易的明白你的意图和想法。写下文档,并不仅仅是怕我们忘了当时的想法,而且还是一种团队的离线交流的方法,更是一种知识传递的方法。记录下你所知道的一切会是一个好的习惯。因为,我相信你不希望别人总是在你最忙的时候来打断你问问题,或是你在休假的时候接到公司的电话来询问你问题。而你自己如果老是守着自己的东西,其结果只可能是让你自己长时间地深陷在这块东西内,而你就更本不可以去做更多的事情。包括向上的晋升。你可能以为“教会徒弟能饿死师父”,但我告诉你,你的保守会让你失去更多更好的东西,请你相信我,我绝不是在这里耸人听闻。
- 注意备份和安全。 可能你觉得这是一个“废话”,你已明白了备份的重要性。但是,我还是要在这里提出,丢失东西是我们人生中的一部份,你总是会丢东西,这点你永远无法避免。比如:你的笔记本电脑被人偷了,你的硬盘损坏了,你的电脑中病毒了,你的系统被人入侵了,甚至整个大楼被烧了,等等,等等。所以,做好备份工作是非常非常重要的事情,硬盘是不可信的,所以定期的刻录光盘或是磁带可能会是一个好的方法,网络也是不可信的,所以小心病毒和黑客,不但使用软件方面的安全策略,你更需要一个健全的管理制度。此外,尽量的让你的数据放在不同的地方,并做好定期(每日,每周,每月)的备份策略。
- 设计要足够灵活。 可能你的需求只会要求你实现一个死的东西,但是,你作为一个优秀的程序,你应该随时在思考这个死的东西是否可以有灵活的一面,比如把一些参数变成可以配置的,把一些公用的东西形成你的函数库以便以后重用,是否提供插件方面的功能?你的模块是否要以像积木一样随意组合?如果要有修改的话,你的设计是否能够马上应付?当然,灵活的设计可能并不是要你去重新发明轮子,你应该尽可能是使用标准化的东西。所谓灵话的设计就是要让让考虑更多需求之外的东西,把需求中这一类的问题都考虑到,而不是只处理需求中所说的那一特定的东西。比如说,需要需要的屏幕分辨率是800×600,那么你的设计能否灵活于其他的分辨率?程序设计总是需要我们去处理不同的环境,以及未来的趋势。我们需要用动态的眼光去思考问题,而不是刻舟求剑。也许有一天,你今天写的程序就要移植到别的环境中去,那个时候你就能真正明白什么是灵活的设计了。
- 不要搬起石头砸自己的脚。程序员总是有一种不好的习惯,那就是总是想赶快地完成自己手上的工作。但情况却往往事已愿违。越是想做得快,就越是容易出问题,越是想做得快,就越是容易遗漏问题,最终,程序改过来改过去,按下葫芦起了瓢,最后花费的时间和精力反而更多。欲速而不达。优秀程序员的习惯是前面多花一些时间多作一些调查,试验一下不同的解决方案,如果时间允许,一个好的习惯是,每4个小时的编程,需要一个小时的休息,然后又是4个小时的编码。当然,这因人而异,但其目的就是让你时常回头看看,让你想一想这样三个问题:1)是否这么做是对的?2)是否这么做考虑到了所有的情况?3)是否有更好的方法?想好了再说,时常回头看看走过的路,时常总结一下过去事,会对你有很大的帮助。
以上是十条优秀程序员的习惯或行为规范,希望其可以对你有所帮助。
本文来源于网上phil的BLOG,但我在写作过程中使用了自己的语言和方法重新描述了一下这十条,所以,我希望你在转载的时候能够注明作者和出处以表示对我的尊重。谢谢! 收起阅读 »
检测一下大家的OC基础,最新ios面试题,你能回答上哪些?
哈哈,没有地址,此时我的内心的meng 比的!
1.说说内存管理
2、ASIRequest是什么;
3、怎么输出json字符串;
4、说说http头部有哪些内容;
5、说说OC生命周期;
6、运用第三方框架,到时候出了问题,谁来负责
7、自己写一个strcpy函数
8、字母统计(如,输入字符串“aabbbccddddaaaaa”,输出“2a3b2c4d5a”)
9、你用过哪些框架
10、进程与线程的区别
11、开辟线程的方式有哪些
12、实现进程同步的方式有哪些,或者说你怎么实现进程同步
13、请你谈谈同步和异步,用操作系统知识解释一下。
14、请你谈谈多态
15、怎么将数据写入文件(归档,解当)
16、写一个set方法(retain和copy权限)
17
Int* fun()
{
Int a=5;
Int * p=&a;
Return p;
}
请问:在主函数里面调用fun函数,这样可以吗?如果不可以,请说明为什么,并给出一种解决方案。
18、在颜色中,有GB8888和 GB565标准,前者32位,其中R占8位,G占8位,B占8位,透明度占8位,后者16位,其中,R占5位,G占6位,B占5位。现在要将一个GB8888类型颜色转换成GB565类型,怎么转
19、判断一个数是否为素数
20、优化代码
1、int a=b*4;
2、int a=b/8;
3、int a=b%1;
4、int a=b;
5、int a=(b*3)/8;
21、什么是内联函数?
22、assign,retain,copy的区别
23、面向对象的特性
24、实现一个view从顶部移到底部的动画
25、#ff3344转换成uicolor
26、判断一个链表是否有循环
27、写一个代理类
28、进程之间是怎么通信的
29、oc有哪些优点和缺点
30、什么时候用delegate,什么时候用Notification?
31、写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
32、MVC模式的理解
33、堆和栈的区别
34、自动释放池是什么,如何工作
35、写一个委托的interface
36、objective-c的内存管理
37、什么是Notification?
38、下面的声明都是什么意思?
constint a;
intconst a;
constint *a;
int* const a;
intconst * a const;
收起阅读 »
IT垂直领域的今日头条
纵观目前互联网垂直个性化聚合类新闻平台鱼龙混杂,真正能做到满足用户需求,戳中用户痛点的平台有几个!
如今互联网垂直领域的新闻聚合类平台和今日头条的差别在与范围的大小以及定位的不同,但核心意义仍不改变,就是根据推荐算法将更优质的更符合用户阅读习惯的内容呈现。
据最近了解来看 , 在此处能称为IT领域的今日头条, 当属浙江网新恒天软件公司的摘客。
虽然网新恒天是做外包服务为主的公司, 但是看到摘客这个产品的时候, 我便对他刮目相看。
1.首先我们来说下他的页面交互
身为一个外包公司来说,互联网产品的思想应该属于较弱的情况,但是他却采用较为先进的设计风格,将icon,色彩,排版都做了仔细斟酌。
2.其次我们来说下他爬来的内容
将内容分类,同时也可根据自己的喜好订阅相应类别。基于个人阅读的情况个性化推荐。
再也不用因为找不到想看的内容而急躁!
叔本华说,“每个人都将自身所感知的范围当做世界的范围”,我们能够在互联网上看到什么样的世界,就意味着自己会生活在哪类世界里,信息无价,用更好的工具来看世界,机器的来临,已然改变了信息的基本构成,你需要什么,平台就得往你需要的地方去。
垂直个性化聚合类平台热已来临,希望摘客能够继续坚挺。 收起阅读 »
IOS V2.2.5 AndroidV2.2.9 release ,支持ipv6,增加群红包、支持拼手气红包和普通群红包
Android V2.2.9 2016-5-18更新日志
这个版本主要对红包功能做了更新,用Eclipse导入项目的时候需要把demoui3.0根目录下的redpacketlibrary也导入到Eclipse中。1、增加群红包,可以发拼手气红包、普通群红包;
2、优化支付流程,支付更便捷;
3、优化绑卡流程,绑卡更安全;
4、HTML5的页面基于React重构,主要流程通过原生SDK实现,速度更快、交互体验更流畅;
5、增加了红包记录,可以查看收发的红包记录;
6、提供了红包产品的数据统计,App可登陆红包的管理后台查看;
7、增加了太平洋保险的账户安全险,因账户被盗导致的资金损失可以获得赔偿。
IOS V2.2.5 2016-05-18 更新日志
新功能:SDK支持ipv6
红包新版本:
增加群红包,支持拼手气红包和普通群红包;
优化支付流程,支付更便捷;
优化绑卡流程,绑卡更安全;
HTML5的页面基于React重构,主要流程通过原生SDK实现,提升速度和交互体验;
增加了红包历史记录;
提供了红包产品的数据统计,App可登陆红包的管理后台查看;
增加了太平洋保险的账户安全险。
bug fix:
SDK bug:正常网络下登录、退出偶尔超时问题
版本历史:Android SDK更新历史 IOS SDK更新历史
下载地址:SDK下载
关于新版sdk使用有任何问题或建议请在下方评论留言,我们将直接现金打赏。 收起阅读 »
【创业星生代】36 氪联合环信助力最优秀的创业公司
如果你也有这样的“也许”,那么 36 氪倾心打造的 “创业星生代” 舞台正是你的期许。在这里,创投助手 App、媒体、社群氪空间、融资平台、企业服务被集中整合,五大机构联体,九位大咖携手,联合打造一站式创业生态闭环,解决创业者面临的最大痛点。
“创业星生代” 导师将为你的项目打分并择优约谈,通过每周、每月项目打榜的方式,产生周十强以及月十强,充分挖掘项目商业潜力,助力最优秀的团队。我们事无巨细,竭尽全力,全方位为最优秀的你提供优质、匹配的服务。
活动时间:5月16日 全面开启
参与流程:
step1、报名,提交项目信息
step2、项目审核,通过后登陆「创投助手APP」
step3、投资人发起约谈
step4、依据数据产生周 10 强
step5、顶级投资机构进行评选,产生月10 强
活动奖励:
1、融资礼包:
项目入驻 36 氪「创投助手APP」
专享 36 氪轻 FA 服务
“创业星生代” 全国创业大赛线下直接参赛
氪空间诊疗专家 1 对 1 项目深度辅导
2、品牌曝光:
36 氪媒体独家优先报道
北京地铁 LCD 大屏广告
企业服务豪华礼包(价值 10 万元)
36 氪专栏报道套餐
3、创业者社群:
直入氪空间孵化器最终面试
直入氪空间俱乐部
直接参与 36Kr Demo Day 全国线下路演
合作机构及大咖投资人 :
IDG 创始合伙人:熊晓鸽
IDG 合伙人:李骁军
北极光创投董事总经理:姜皓天
峰瑞资本创始合伙人:李丰
峰瑞资本创始合伙人:林中华
高榕创始合伙人:高翔
高榕创始合伙人:张震
华创资本管理合伙人:吴海燕
华创资本合伙人:熊伟铭
合作伙伴:
合作伙伴环信简介:
环信成立于2013年4月,是一家全通讯能力云服务提供商。产品包括国内上线最早的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。公司产品现已覆盖电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、快牙、楚楚街、随手记、猎聘、海尔等。截至2015年底,环信共服务了50833家App客户,SDK覆盖手机终端3.19亿,平台日均发送消息2.1亿条。根据易观智库《2015中国SaaS客服市场专题研究报告》,在中国移动端SaaS客服市场中,环信市场占有率高达77.4%。也许你所有的坚持都在等待这一刻的爆发。现在开始,你要做的只是点击“阅读原文”,加入星生代!
阅读原文 收起阅读 »
android播放网络音频
很简单的一个获取网络音频播放器,有进度条,播放,暂停,停止,重新播放,支持缓存,以下是源码,希望可以帮到大家
布局文件很简单,就几个按钮,TextView,和SeekBar。
activity_audio_palyer.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"Player.Java文件
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:orientation="vertical" >
<TextView
android:id="@+id/tips"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="文件地址" />
<EditText
android:id="@+id/file_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://sc1.111ttt.com/2016/1/02/23/195231349486.mp3" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4.0dip"
android:orientation="horizontal" >
<Button
android:id="@+id/btnPlayUrl"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="播放" >
</Button>
<Button
android:id="@+id/btnPause"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="暂停" >
</Button>
<Button
android:id="@+id/btnStop"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="停止" >
</Button>
<Button
android:id="@+id/btnReplay"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="重播" >
</Button>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:orientation="horizontal" >
<SeekBar
android:id="@+id/skbProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
</LinearLayout>
</LinearLayout>
</FrameLayout>
public class Player implements OnBufferingUpdateListener, OnCompletionListener,MainActivity.java文件
MediaPlayer.OnPreparedListener {
public MediaPlayer mediaPlayer;
private SeekBar skbProgress;
private Timer mTimer = new Timer();
private String videoUrl;
private boolean pause;
private int playPosition;
public Player(String videoUrl, SeekBar skbProgress) {
this.skbProgress = skbProgress;
this.videoUrl = videoUrl;
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnPreparedListener(this);
} catch (Exception e) {
Log.e("mediaPlayer", "error", e);
}
mTimer.schedule(mTimerTask, 0, 1000);
}
/*******************************************************
* 通过定时器和Handler来更新进度条
******************************************************/
TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
if (mediaPlayer == null)
return;
if (mediaPlayer.isPlaying() && skbProgress.isPressed() == false) {
handleProgress.sendEmptyMessage(0);
}
}
};
Handler handleProgress = new Handler() {
public void handleMessage(Message msg) {
int position = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
if (duration > 0) {
long pos = skbProgress.getMax() * position / duration;
skbProgress.setProgress((int) pos);
}
};
};
/**
* 来电话了
*/
public void callIsComing() {
if (mediaPlayer.isPlaying()) {
playPosition = mediaPlayer.getCurrentPosition();// 获得当前播放位置
mediaPlayer.stop();
}
}
/**
* 通话结束
*/
public void callIsDown() {
if (playPosition > 0) {
playNet(playPosition);
playPosition = 0;
}
}
/**
* 播放
*/
public void play() {
playNet(0);
}
/**
* 重播
*/
public void replay() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(0);// 从开始位置开始播放音乐
} else {
playNet(0);
}
}
/**
* 暂停
*/
public boolean pause() {
if (mediaPlayer.isPlaying()) {// 如果正在播放
mediaPlayer.pause();// 暂停
pause = true;
} else {
if (pause) {// 如果处于暂停状态
mediaPlayer.start();// 继续播放
pause = false;
}
}
return pause;
}
/**
* 停止
*/
public void stop() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
}
@Override
/**
* 通过onPrepared播放
*/
public void onPrepared(MediaPlayer arg0) {
arg0.start();
Log.e("mediaPlayer", "onPrepared");
}
@Override
public void onCompletion(MediaPlayer arg0) {
Log.e("mediaPlayer", "onCompletion");
}
@Override
public void onBufferingUpdate(MediaPlayer arg0, int bufferingProgress) {
skbProgress.setSecondaryProgress(bufferingProgress);
int currentProgress = skbProgress.getMax()
* mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration();
Log.e(currentProgress + "% play", bufferingProgress + "% buffer");
}
/**
* 播放音乐
*
* @param playPosition
*/
private void playNet(int playPosition) {
try {
mediaPlayer.reset();// 把各项参数恢复到初始状态
mediaPlayer.setDataSource(videoUrl);
mediaPlayer.prepare();// 进行缓冲
mediaPlayer.setOnPreparedListener(new MyPreparedListener(
playPosition));
} catch (Exception e) {
e.printStackTrace();
}
}
private final class MyPreparedListener implements
android.media.MediaPlayer.OnPreparedListener {
private int playPosition;
public MyPreparedListener(int playPosition) {
this.playPosition = playPosition;
}
@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();// 开始播放
if (playPosition > 0) {
mediaPlayer.seekTo(playPosition);
}
}
}
}
public class MainActivity extends Activity {OK,在项目文件AndroidManifest.xml里面添加权限
private Button btnPause, btnPlayUrl, btnStop,btnReplay;
private SeekBar skbProgress;
private Player player;
private EditText file_name_text;
private TextView tipsView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_palyer);
this.setTitle("在线音乐播放---ouyangpeng编写");
btnPlayUrl = (Button) this.findViewById(R.id.btnPlayUrl);
btnPlayUrl.setOnClickListener(new ClickEvent());
btnPause = (Button) this.findViewById(R.id.btnPause);
btnPause.setOnClickListener(new ClickEvent());
btnStop = (Button) this.findViewById(R.id.btnStop);
btnStop.setOnClickListener(new ClickEvent());
btnReplay = (Button) this.findViewById(R.id.btnReplay);
btnReplay.setOnClickListener(new ClickEvent());
file_name_text=(EditText) this.findViewById(R.id.file_name);
tipsView=(TextView) this.findViewById(R.id.tips);
skbProgress = (SeekBar) this.findViewById(R.id.skbProgress);
skbProgress.setOnSeekBarChangeListener(new SeekBarChangeEvent());
String url=file_name_text.getText().toString();
player = new Player(url,skbProgress);
TelephonyManager telephonyManager=(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(new MyPhoneListener(), PhoneStateListener.LISTEN_CALL_STATE);
}
/**
* 只有电话来了之后才暂停音乐的播放
*/
private final class MyPhoneListener extends android.telephony.PhoneStateListener{
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING://电话来了
player.callIsComing();
break;
case TelephonyManager.CALL_STATE_IDLE: //通话结束
player.callIsDown();
break;
}
}
}
class ClickEvent implements OnClickListener {
@Override
public void onClick(View arg0) {
if (arg0 == btnPause) {
boolean pause=player.pause();
if (pause) {
btnPause.setText("继续");
tipsView.setText("暂停播放...");
}else{
btnPause.setText("暂停");
tipsView.setText("继续播放...");
}
} else if (arg0 == btnPlayUrl) {
player.play();
tipsView.setText("开始播放...");
} else if (arg0 == btnStop) {
player.stop();
tipsView.setText("停止播放...");
} else if (arg0==btnReplay) {
player.replay();
tipsView.setText("重新播放...");
}
}
}
class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener {
int progress;
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// 原本是(progress/seekBar.getMax())*player.mediaPlayer.getDuration()
this.progress = progress * player.mediaPlayer.getDuration()
/ seekBar.getMax();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// seekTo()的参数是相对与影片时间的数字,而不是与seekBar.getMax()相对的数字
player.mediaPlayer.seekTo(progress);
}
}
}
<?xml version="1.0" encoding="utf-8"?>这样做出来的就很原始了,1毛钱特效都没有的那种。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.netmusic"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 注意:这里要加入一个监听电话的权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.netmusic.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
按钮这些可以自己定义背景,有专门的媒体按钮,去http://www.iconfont.cn/上找,很多的。
最主要的是咱们的进度条SeekBar,,原始是不是太丑了?来,我们加个样式吧。
在style文件里面:
</style>新建seekbar_horizontal.xml,drawable里面的
<style name="Widget.SeekBar.Normal" parent="@android:style/Widget.SeekBar">
<item name="android:maxHeight">8.0dip</item>
<item name="android:indeterminateOnly">false</item>
<item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
<item name="android:progressDrawable">@drawable/seekbar_horizontal</item>
<item name="android:minHeight">8.0dip</item>
<item name="android:thumb">@drawable/seek_thumb</item>
<item name="android:thumbOffset">10.0dip</item>
</style>
<?xml version="1.0" encoding="UTF-8"?>ok,还有几个图片素材
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/seek_bkg" />
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2.0dip" />
<gradient android:startColor="#80ffd300" android:endColor="#a0ffcb00" android:angle="270.0" android:centerY="0.75" android:centerColor="#80ffb600" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip android:drawable="@drawable/seek" />
</item>
</layer-list>
seek.9.png
seek_bkg.9.png
seek_thumb.png
代码里面引用:
<SeekBar这样就可以了,看下效果:
android:id="@+id/skbProgress"
style="@style/Widget.SeekBar.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七
作者QQ:1453022932 收起阅读 »
Android Studio使用技巧
//logd + Enter
Log.d(TAG, "onCreate ");
//logm + Enter 可以快速输出方法中的参数log信息
Log.d(TAG, "onCreate() called with " + "savedInstanceState = [" + savedInstanceState + "]");
//loge + Enter
Log.e(TAG, "onCreate ");
快捷键及相关的使用:
【1】新建了Android library module -> settings.gradle多了':mylibrary'
【2】自动导入包:File->Settings->Editor->General->Auto Import->把Java的勾都打上
【3】设置快捷键类型:File->Settings->搜索keymap
【4】Ctrl+Alt+空格 代码提示
【5】Ctrl+Shift+↑或↓ 移动代码位置
【6】复制上一行代码,并显示在当行 Ctrl+D
【7】删除一行代码 Ctrl+Y
【8】在方法间快速移动 Alt+↑或↓
【9】移动滚动条 Ctrl+↑或↓
【10】Ctrl+W 选中代码,多次按会不同效果
【11】Ctrl+N 查找类
【12】查找文件,如xml Ctrl+Shift+N
【13】在本类中按Ctrl+U 查找本类的父类
【14】选中方法按Ctrl+Alt+h 查找这个方法被调用的地方
【15】查看一个方法的实现 选中方法按Ctrl+Shift+i
【16】在本类中按Ctrl+H 查看本类的层级结构
【17】Ctrl+Alt+← 返回代码跳转前的位置
【18】Alt+→或← 切换打开的文件
【19】光标在方法里,按Ctrl + -或+ 展开或折叠方法
【20】Alt+1 隐藏或显示左侧的工程面板
【21】Ctrl+Shift+Alt+N 查找本类中的方法
【22】Ctrl+F12 查看本类的结构,显示本类的方法和数据域等, 在此基础上按Ctrl+I或打勾右边,可查看匿名内部类
【23】Ctrl+O 覆盖父类的方法
【24】光标处于方法的一个大括号,按Ctrl+ [ 或 ] 跳转到方法大括号的另一端
【25】选中模块,按Ctrl+Alt+T,可快速生成try catch等语句
【26】Ctrl+鼠标左键点击Activity左边的布局图标,可快速打开与本Activity有关联的布局
【27】Ctrl+J 快捷生成判空、循环、findViewById、Toast等代码,同时能查看其他快捷键使用方式
【28】Alt + Enter 错误提示
【29】Ctrl + F 在本类中查找相同元素
【30】Shift+F6 在本类中整体修改元素( 牵一发而动全身 )
【31】Ctrl+R 在本类中整体查找,整体替换
【32】Ctrl+E 查看最近打开的文件
【33】格式化代码 Ctrl+Alt+L
Debug介绍:
F8单步调试,F7进入方法,Shift+F8跳到另一个断点位置
若想在"不修改"代码的前提下,在控制台Console而不是Logcat中输出log,则可采用以下方法:
右键断点
点击Suspend
在Log evaluated expression中输入要打印的log
输出效果如下,这种方法能 "避免修改代码",读者可常用
在debug过程中可快速修改变量值,在下面 "i=1" 处右键,点 Set Value 即可
也可以点击 Add to Watches 把要观察的变量添加到 Watches 中
点击上图的第二个红色按钮,View Breakpoints
左侧显示了已标注的断点位置,可通过取消勾选,来实现 ”不去除断点,但不运行已取消勾选的断点“ 。 收起阅读 »
环信编程大赛优秀开源项目系列之一:“文播”一款文字直播APP
5月14日,由环信联合猿圈共同推出的“首届环信编程大赛”颁奖典礼在中关村义创空间隆重举行。本次环信编程大赛历时两个月,由线上初赛、决赛和颁奖典礼三个环节组成,总计报名人数2000+,收到决赛项目100+。最终由评委会认定的13个优秀开源项目及开发者集体亮相颁奖典礼。其中“方圆十里”、“高仿微信“和“咚咚”三个开源项目名列前三,共同分享了15000元奖金和价值12000元的专属表情包。
优秀项目开发者合影
这枚可爱的小鲜肉竟然是本次环信编程大赛发起人,目前单身,私信可获得联系方式!
其余入围的十余个优秀开源项目同样引起了到场开发者的热烈追捧,环信将分期将入围的优秀项目代码免费开源给小伙伴们。今天我们带来的是一款基于环信sdk进行个性化改造的文字直播平台App——“文播”。典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在“文播”里找到对应的频道。
“文播”项目负责人董艺菲分享技术开发细节
“文播”APP界面截图
功能:
本项目是一款基于环信sdk进行个性化改造的文字直播平台性的安卓app。
在参赛报名的时候,曾想过这样一个问题:一款完全为IM而生的sdk,到底能有如何的潜力?因此,另辟蹊径将环信提供的IM群聊功能,通过重新设计,改造成了现在的文字直播的平台类型app。
每个直播间,其实就是一个“只有群创建者才能发言”的IM群组或讨论组,再进行一些界面上的改造,就可以实现一款类似于从早期非智能机时代流行至今的纯文字直播的app。
典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在《文播》里找到对应的频道。
提交的该版本目前为纯游客端,主播端另行实现。
技术:
·客户端使用DrCoSu工作室开源的dileber框架,MVP设计模式,整个项目冗余较低。
·融合环信SDK,并进行了个性化的改造。
·采用.9格式存储图片,ttf方式呈现界面与图标,各个机型兼容性较好。
·服务端采用Java(Spring),配合ngix和redis极大提升了访问响应速度。
·采用http通信和json、xml等数据格式,移植性和通用性好。
心得
重复造轮子虽然好,但是在实际开发中,往往可以使用更好的方式来加快你的节奏,从中获得更大的成就感。
环信SDK在即时通讯云领域是一款足够优秀的SDK。配合JPush和好的创意,能实现无限多的可能性。
创意是一款新型软件的核心竞争力。
介绍
文字的直播,一样精彩。
特别感谢以下企业的大力支持:
义创空间提供颁奖场地
萌岛从自有形象库中授权一套价值12000元的表情包
Emokit赞助Apple Watch一台
猿圈全程提供技术评测支持
项目托管地址:https://sourceforge.net/p/wenbo-im/git/ci/master/tree/
“文播”源码下载及演讲PPT下载↓↓↓ 收起阅读 »
环信“客户声音”后台谍照泄露
创业关闭大潮,它潜伏修炼
在去年通过朋友圈的推荐,下载了一款产品摘客,一开始被他们的独特的分类阅读所吸引,虽然产品当时还有很多的问题,但是对于用户来说,其独特的细分个性化阅读有别于其他APP,当时的体验还是不错的。还有其搜索功能,当时发现搜索时,很多时候都能意外的发现很多好文章,因此建立了该产品的第一映像。
后来机缘巧合,认识到这款产品的团队负责人,因此也特地对摘客与其进行了相关的交流和探讨。
惊讶的是整个团队不过10+人,就包括了后台、前端、测试、运营等人员,原本以为主打个性化推荐算法的产品,相对而言对产品的复杂度和门槛会高一点点,应该有几十人的团队在维护,如今日头条,虽然人家已经上千人的团队,但起步的时候也远远不止10人的团队。不过所有创业团队在起步的时候一定是精简的,一人能敌好几个人用。光这一点,对摘客又多了几分好感。
当然,我也从我的角度指出了很多产品存在的问题,比如界面相较于其他和产品定位而言,UI风格的混乱和不统一;用户评论机制的鸡肋和打造UGC社区氛围问题;产品人群定位等问题都进行了探讨;
就个性化推荐而言,摘客切入的是互联网资讯垂直行业,相较于目前已经做得较好的科技媒体,像36氪、虎嗅、极客公园,以及同样主打个性推荐的今日头像,摘客还是有明显的差异性,找到了细分领域。而就阅读而言,用户是否真的需要这么细分的需求呢?这是产品值得思考的问题。
摘客负责人告诉我们,已将上线的2.0.0版本会对目前的产品有一个较大的改进和提高,尤其是目前被人诟病的界面,因此当下就去下载更新了,大改版的白蓝为主色的界面,真实小清晰一脸啊,侧拉导航改为底部导航,也大大比旧版提高了用户的交互体验;
通过深入的了解后,整个团队给人的感觉相较一般的创业团队,更耐得住性子,沉下心来打磨一件他们认为有价值的产品。相比那些为了拿钱融资,冲业务指标而放弃初心的很多创业团队,更欣赏现在还可以静心做产品的团队。
值得恭喜的是,他们告诉我摘客已经在融资阶段,并且有了明确性的资金意向,具体的消息相信在不久的将来可以听到。 收起阅读 »
【猿团专访】|刘俊彦:PaaS、SaaS齐头并进 环信领军企业服务市场
作为环信CEO,刘俊彦认为,要保持行业竞争力,最重要的是远见、视野以及执行力。这个目光如炬的男人,用他独有的市场洞察力,带领环信成为国内最大的即时通讯云平台及国内最大的SaaS客服平台。本期猿团专访,笔者将带着大家一起来了解环信背后的故事。
以下为猿团记者专访内容,原创作品,如需转载请注明出处。
回望:创业伊始 环信面临招聘难
和所有创业公司一样,环信最初的创立也是艰难重重,最大的挑战还是人才的招聘。刘俊彦告诉笔者,环信是从中关村创业大街上的车库咖啡起家的。当时,几位创始人带着几位初出茅庐的年轻程序员,在一个咖啡桌边一坐就是一年。由于办公环境简陋,因此招聘特别困难。每次打电话过去,候选人一听说办公地点是在一个咖啡厅,就狐疑丛生,不是当场拒绝,就是约好的面试频频被放鸽子。幸运的是,襁褓中的环信,凝聚了一批坚定的创业者,怀揣着用技术改变世界的理想,团队在一点点的壮大成熟。更为幸运的是,当时那条咖啡桌边的初出茅庐的年轻程序员们,没有一位离开环信,而且全部都成长成熟起来,在各自的岗位上发挥着中坚作用。
如今的环信,团队规模已有数百人,早已搬进高大上的办公楼,但环信“团结最优秀的人,一起用技术改变世界”的初心一直不曾改变。谈到这里,刘俊彦表示:”最近经常有人问,’现在加入环信会不会晚了点,还有没有足够好的位置留给我?’。我想说的是,不管是创业还是加入一家公司,选择永远大于努力。如果有一张登上火箭飞船的票,你不需要问这是几等舱。”
两大产品线齐头并进 环信领军通讯市场
虽然前期遭遇了种种困难,但在团队的努力下,目前环信即时通讯云目前已经是国内最大的即时通讯云平台,共服务了50833家 App 客户,SDK覆盖手机终端3.19亿台,平台日均发送消息2.1亿条。而环信移动客服也已经成为国内最大的SaaS客服平台,覆盖了电商、O2O、金融保险、教育等多个行业的众多标杆客户,典型用户包括国美在线,58到家,神州专车,金融界,学而思等。环信初步完成了对连接人与人和连接人与商业的两个核心场景的全覆盖。
笔者了解到,目前环信有两条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。
为什么会有两条产品线呢?刘俊彦表示,在环信看来,IM天然就有2种场景。第一种是使用IM让人和人之间沟通。这个是做社交,也就是连接人与人。不管是微信、陌陌、还是环信即时通讯云平台上的6万多家APP,他们做的都是在连接人与人。在这方面,环信即时通讯云希望帮助开发者和企业更好的连接人与人,做即时通讯云领导者。
IM的第二种场景就是使用IM让人和商家之间沟通。这个就是连接人与商业。不管是淘宝旺旺,还是微信公共账号,其本质都是用IM的方式让人(消费者)和商家更好的沟通和服务。环信移动客服的愿景就是更好的连接人与商业,领军移动客服。这个产品的主要服务对象是各行各业的企业和商家。只要商家有和消费者沟通的需求,有服务消费者的需求,就可以使用环信移动客服。
好PaaS产生好SaaS 环信优势明显
环信是国内最早提供即时通讯云服务的企业,技术先发优势和获客先发优势非常明显,现在主流的大型APP市场基本都被环信垄断。在很多用户使用完环信的PaaS服务后,越来越多的涌现出客服使用的需求。而互联网的发展,也的确将客服服务从PC端转移到移动端,移动端客服成为整个客服软件市场的核心战场,谁打赢了移动端客服的战斗,谁就打赢了整个客服软件的战斗。这一点的发现,让刘俊彦坚持要把移动客服做下去。
说到环信移动客服这条产品线的开发,刘俊彦回想起当时的情景:“在最开始,我们完全没有想过要做环信移动客服这么一个SaaS形态的客服产品。甚至当我决定要做这个产品的时候,连股东内部都有不同的声音。但我顶住压力,不但做了这个产品,而且是‘all in’地去做。后来,环信移动客服一经推出,就获得了巨大成功,这也证明当初的决定是多么正确。“
根据易观国际的研究报告,环信移动客服在移动端SaaS客服市场的占有率高达77.4%。对于移动客服取得如此的成功,刘俊彦并不惊讶,因为在他看来,移动端客服软件最根本的核心技术就是IM技术。没有IM技术,就没法做移动端客服。环信作为中国最大的即时通讯云厂商,在IM技术的领先优势可想而知。这一优势直接造就了环信移动客服目前的市场地位。
目前环信是众多SaaS客服厂商中唯一一个拥有自主成熟IM技术的厂商,拥有专为移动互联网和手机终端深度优化的的私有通讯协议,同时,电信级高可靠、高并发的即时通讯公有云平台支撑移动客服的稳定性,经过实际验证,可支持亿级用户同时在线。“好PaaS必定产生好SaaS ”刘俊彦如是说。
加大智能投入 环信引领客户服务行业进入人工智能时代
和其他友商不同,环信极其重视在人工智能和大数据领域的投入。环信坚信,人工智能技术将在2年内彻底改变客户服务软件行业。环信很早就建立了人工智能和大数据研究院,数十名业界顶尖的科学家和工程师在人工智能与客户服务结合的这一新兴领域做了大量世界前沿的研究和产品化工作,尽管成本巨大,但刘俊彦认为非常值得。目前,环信已经推出了环信机器人,环信客户声音,环信智能质检,环信社交大数据等多个产品。使得环信在产品布局和产品能力上一直保持行业领先地位。
当被问及环信未来还有何发展时,刘俊彦淡淡一笑说:“当每天有几亿条消息在你的系统里流过,当每天有几百个客户,同时也是这个时代的一批最优秀的创业者向你提出他们最期待的新功能需求时,你已经‘被’站在了时代的风口浪尖,占据了制高点。只要你善于倾听,不畏惧改变,你一定不会错过时代的脉搏。所以我并不是特别确定今后我们环信公司还能为我们整个移动互联的大时代带来什么新的突破和惊喜,但我相信一点,当未来到来时,我们一定已经在那里。”
(文章来源:猿团 作者:瘦司)
收起阅读 »
iOS项目更新之升级Xcode7 & iOS9
Apple 的WWDC所发布内容在给大家带来惊喜之际,给各位iOS开发的同仁却也带来了不同程度的麻烦。首先不讲新功能,就单指原来老版本的项目升级、代码升级,就是一堆问题,而且是不得不面临的问题。下面就跟着笔者一起来回顾下,此次在项目升级过程中,所遇到的各个问题点,以及解决方案,与各位已经做过和正在做iOS代码升级的同仁共勉,也给各位将要做Xcode 7和iOS9兼容的同仁以参考。
开发环境安装
原本运行得好好的项目,要升级Xcode7,首先就得安装Xcode7,具体的可以从开发者官网下载(目前最新版本是Xcode_7_GM_seed).下载好后,就双击下载好的dmg包,当然,前提还是需要我们的Mac环境升级到Mac OS 10.10.4+(图1.1),就可以打开Xcode安装镜像,如图1.2:
图1.1 Mac OS 更新示意图
图1.2 Xcode 7 GM安装
接下来,我们只要将图1.1所示的Xcode拖动到指定文件夹,即可完成安装,接下来,我们只要双击运行即可。
开发环境运行
各位可能会觉得,笔者在此还要讲开发环境的运行,是不是多此一举。其实并非如此,综合笔者这几年iOS开发经验的总结,运行新版本,特别是测试版本的Xcode是一个需要格外小心的事情,讲起来都是血泪史。
在运行Beta 版本Xcode时,我们需要特别注意以下几个方面:
- 在运行Beta版本Xcode前,务必要退出原来正式版本Xcode(如Xcode 6.4)
- 在运行Beta版本Xcode时,务必要避免双击打开工程文件(也是为了避免新旧版本同时运行)。
- 如果要切换回原来版本时,一定要先退出Beta版本,而且尽可能将Xcode的缓存数据清除。
当然,可能在实际的过程中,还是会有不少朋友就这么干了,当然,如果我们App后续只需要使用新版本Xcode,自然是没有太大关系,只是对于还需要用旧版本来开发或者发布App的朋友,可能就会有点麻烦,可能在用旧版本编译App在运行的时候,就会出现各种诡异的现象(如打印信息明明是正常,App运行逻辑却不正常等)。这时,可能大家要考虑的就是把Xcode删除掉,重新来过,甚至是重装操作系统。当然,不知道是否有朋友有更好的方案。不过笔者是不再想经历这种事情了。
App 项目运行
待项目运行,首先会碰到的问题就是配置兼容,会出现如下错误
图2.1 BitCode 错误
当我们看到App编译报错的时候,首先想项目不兼容Xcode7,再仔细一看
ld: ‘/Volumes/MacintoshHD/…/AnimationDesk Universal/Sources/AnimaitonDesk Universal/Classes/Supporting Files/GoogleLibrary/libGoogleAnalyticsServices.a(TAGDataProvider.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64
其中 ENABLE_BITCODE 吸引了我们的注意,看结合其它的描述信息,基本可以确定是我们使用的第三方静态库(.a)不支持BitCode,当然,我们对应就有如下两种方案来解决:
方法一:更新对应的第三方静态库(现在更新的静态库,基本都能支持BitCode)
方法二:可以将Xcode7默认开启的BitCode功能关闭,如图2.2所示
图2.2 关闭BitCode 操作示意图
当然,除了上面的问题外,当我们在添加Framework的时候,会发现此前导入的动态链接库(dylib)他部变成了红色,如图2.3所示,所幸的是,就算不替换成Xcode 7新的动态库文件(.tbd),仍然可以正常运行.
图2.3 动态链接库丢失示意图
最后,部分App在编译的时候,可能还会收到如下报错,小编也遇到过一次
All interface orientations must be supported unless the app requires full screen.看到这句提示,就是说App默认是有开启了多任务功能,而多任务功能是需要App支持所有方向,如果我们App是有需要支持多任务,则需要开启App对各个方向(上、下、左、右)的支持;如果App不需要开启多任务,则只需要将如下示意图的 requires full screen 勾选上就ok(如图2.4)。
图2.4 勾选 Requires full screen示意图
不出意外,接下来,App应该是能正常编译运行(小编的AnimationDesk Cloud接下来是可以正常运行),但紧接着,发生了更诡异的事情,以前的的网络访问,现在完全访问不通;大家也许会觉得这可能是服务器挂了,或是外网被墙了,小编最初也是这么想的,但事实上,服务器(从Safari)还是能照常被访问,只是App访问不了,于是后来联想到iOS9 WWDC讲到的网络数据传输安全部分,经过一翻折腾,最终,网络访问的部分也恢复了正常。
其实只要在App的Info.plist里面加入如下信息就可以
<key>NSAppTransportSecurity</key>添加成功后的示意图如下图(图2.4)
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
图2.4 添加Transport Security 示意图
其它事项
可能还有部分朋友跟小编一样,有碰到另外一个现象,就是UITextView,无论怎么设置它的textColor显示的总会是黑色,小编已找到具体的原理,准确地讲,应该是Xcode的一个Bug。
当小编在App开发时,在Xib上面设置过UITextView的背景色(BackgroundColor)为非默认颜色(WhiteColor)时,UITextView的文字颜色(textColor)无论怎么设置,都将会是黑色,如果想要颜色值正常,可以在设置好文本后,再重设一次颜色即可正常。
本篇笔记由CocoonJin发表在个人博客,原文地址:http://www.cnblogs.com/CocoonJin/p/4798081.html 收起阅读 »
环信CEO刘俊彦:永远领先一步 用人工智能颠覆客服行业
环信现在有2条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。
环信CEO刘俊彦
通过IM连接人与人,连接人与商业
当艾媒网记者问到刘俊彦环信为什么要做环信移动客服这个产品时,刘俊彦提到,在移动互联网时代,消费者会通过很多渠道来找商家解决问题,比如电话,网页,微博,微信,手机APP,而环信提供的是一种不管通过什么渠道都能够得到服务的这样一款产品,这就是环信的全媒体客服。
刘俊彦说道:“我们做IM起家,我们发现IM天然有两种场景,一种是连接人与人,另一种是连接人与商业。”用IM做社交,像微信那样发送文字语音图片等功能,促进人和人之间沟通,这个是连接人和人;第二种是用IM连接人和商业,最典型的是淘宝旺旺,或者微信公众账号,消费者可以向商家提出问题。环信刚开始做即时通讯云来连接人与人,后来发现连接“人与商业”这个市场和连接“人与人”的即时通讯云市场一样大,于是又做了环信移动客服这个产品。现在环信已经初步完成了对连接人与人和连接人与商业的2个核心场景的全覆盖。
刘俊彦说:“我们的规模和服务用户数量是最高的。我们有六万个APP用户,覆盖了3.1亿部活跃手机。每天发送信息3亿条,高并发能力是被验证过的。”
从移动IM技术入手,垄断行业
客服软件行业很早就存在,随着技术的进步,发展也极其迅速,大致可以分为三个阶段。前互联网时代,互联网还没有得到发展和普及,主要是电话客服,消费者给商家打电话;第二个阶段是进入互联网时代,即在2000——2011年,这时客服行业多了一个渠道,消费者可以通过网站找到商家,线上服务开始了;第三个是在移动互联网时代,据双11天猫统计的结果,用户80%的订单是在手机上完成的,说明如今消费者不再是在PC端或者是在电话上解决问题,遇到问题的第一反应是怎么通过手机上的原生应用得到商家支持。
环信的定位选择在移动端客服入口。刘俊彦说:“客服软件厂家的主战场一定是移动端客服,在这个战场上打赢了就相当于打赢整个客服软件的战斗。所以环信是从我们最擅长的移动IM技术入手,然后进入到移动端的客服软件。在移动端客服软件这个市场,可以说我们环信是一个绝对垄断的地位。”刘俊彦表示环信具有非常深的技术壁垒,从移动端客服这个点做深做透,然后再向全媒体客服市场拓展,做电话的客服,做微信的客服,做网页的客服,以点带面,逐渐垄断整个客服软件市场。刘俊彦认为这也是环信相比于其他客服企业的第一个优势。
加大人工智能投入巅峰行业
环信的第二个优势是在人工智能技术上,刘俊彦在采访时说道:“我们认为人工智能在实体行业第一个落地领域一定是客户服务领域。”客服领域当前是一个非常劳动密集型领域,这个行业面临着中国人口红利消失,90后不愿从事这样的工作,招聘非常难等很多问题。而环信认为这些问题一定要通过技术手段来解决,一些简单的重复的问题由机器人解答,一些复杂的问题也可以改为在人机混合模式下,由机器人提供备选答案,由真人来选择答案。
人工智能能会改变这个行业,阿尔法Go和人类的围棋大战将人工智能市场的关注度推上了高潮,在云服务市场,人工智能发展尚未普及和成熟。值得注意的是App市场已经趋于饱和,人工智能聊天正成为新的入口和蓝海。“人工智能会改变这个行业,跟其他的公司比较起来,在人工智能,我们技术的投入在行业是比较靠前的。”刘俊彦坚定地答道。大数据和人工智能的发展对这个行业会有一个颠覆性的改变。我们在大数据方面的投入也是靠前的。环信已推出全媒体智能客服,通过完全自主或人机混合模式的智能机器人技术极大降低人工客服工作量。在这个市场若想实现可持续发展,一定不是陷入同质化产品的无谓竞争,而是基础IT能力的实力较量。一针见血。这个技术出身、目前仍自诩程序员的创业者,洞察市场和未来的时候却不失犀利。
剖析用户痛点 解决用户痛处,永远领先对手一步
刘俊彦在接受艾媒网记者采访时,深入剖析了用户的痛点。首先,他认为客户需要全媒体客服解决方案,去年之前,很多企业客服是以电话为主。去年之后,消费者很快速地向移动端转移。消费者只要不是紧急和复杂的情况,都不想打电话,因为电话是一个很重的沟通方式。很多企业需要同时服务四个渠道,包括电话、网页、微信、APP,以前没有厂家提供。
其次,中国的人口红利正在消失,现在的年轻人越来越不愿意做客服工作。很多公司的客服部门经历了从北京市区迁到郊区,再迁到合肥贵州,下一步可能就要迁到老挝越南了,这是一种不可持续的局面,必须要通过技术手段来解决人力的问题。
刘俊彦针对用户的这些痛点,介绍环信的解决方案。
第一,目前环信已经把全媒体客服产品打磨的很好了。“我们是第一家真正提供全媒体客服的厂家。用户需要的是电话加网页加微信加APP的四合一的整体客服解决方案,数据完全打通。环信这种全媒体客服就解决这种痛点。”刘俊彦说道。
第二,环信已经有一套比较好用的智能机器人系统。人工智能和大数据技术将改变客服行业,将客服行业从一个劳动密集型的行业变成一个高科技驱动的行业,用机器代替一部分的人工。
刘俊彦最后总结说:“我们希望可以永远领先对手一步。当对手气喘吁吁爬上一座山岗以为追上环信时,发现环信已经不在这里了,环信已经在另外一座更高的山岗了。所以我们做了很多有别竞争对手的差异化产品。比如环信反垃圾产品。”。很多社交产品用户被垃圾消息骚扰特别严重从而导致用户流失,我们帮助企业做反垃圾服务从而帮助提高用户留存。环信也是行业首个推出反垃圾服务的厂商。
另一个和对手差异化的产品是环信社交大数据。这个产品帮助企业分析IM用户的关系链和社交行为,从而提高APP的日活和粘性。还有环信业界首推的环信红包功能,这种产品能够帮助客户提高变现能力环信永远在功能上领先对手一个层面。 收起阅读 »
环信即时通讯单聊集成,添加好友,实现单聊
先上一下效果图吧
首先,我们要去环信官网注册账号,这个我就不多说了,注册完登录,创建应用,新建两个测试IM用户,
这里主要用到的是应用标示(Appkey)
好了,在环信官网下载对应的sdk,这个不多说了,最好下载一个文档,里面讲的很详细的。
好了,一下是源码
AppManager.Java
public class AppManager {BaseActivity.java
private static Stack<Activity> mActivityStack;
private static AppManager mAppManager;
private AppManager() {
}
/**
* 单一实例
*/
public static AppManager getInstance() {
if (mAppManager == null) {
mAppManager = new AppManager();
}
return mAppManager;
}
/**
* 添加Activity
*/
public void addActivity(Activity activity) {
if (mActivityStack == null) {
mActivityStack = new Stack<Activity>();
}
mActivityStack.add(activity);
}
/**
* 获取栈顶Activity
*/
public Activity getTopActivity() {
Activity activity = mActivityStack.lastElement();
return activity;
}
/**
* 结束栈顶Activity
*/
public void killTopActivity() {
Activity activity = mActivityStack.lastElement();
killActivity(activity);
}
/**
* 结束指定的Activity
*/
public void killActivity(Activity activity) {
if (activity != null) {
mActivityStack.remove(activity);
activity.finish();
activity = null;
}
}
/**
* 结束指定类名的Activity
*/
public void killActivity(Class<?> cls) {
for (Activity activity : mActivityStack) {
if (activity.getClass().equals(cls)) {
killActivity(activity);
}
}
}
/**
* 结束所有Activity
*/
public void killAllActivity() {
for (int i = 0, size = mActivityStack.size(); i < size; i++) {
if (null != mActivityStack.get(i)) {
mActivityStack.get(i).finish();
}
}
mActivityStack.clear();
}
/**
* 退出应用程序
*/
public void AppExit(Context context) {
try {
killAllActivity();
ActivityManager activityMgr = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityMgr.restartPackage(context.getPackageName());
System.exit(0);
} catch (Exception e) {}
}
}
public abstract class BaseActivity extends Activity {BaseApplication.java
protected Context context = null;
protected BaseApplication mApplication;
protected Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApplication = (BaseApplication) getApplication();
AppManager.getInstance().addActivity(this);
// check netwotk
context = this;
}
@Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
}
}
public class BaseApplication extends Application {ChatListAdapter.java
private static final String TAG = BaseApplication.class.getSimpleName();
private static BaseApplication mInstance = null;
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
/*
* (non-Javadoc)
*
* @see android.app.Application#onCreate()
*/
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果app启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process
// name就立即返回
if (processAppName == null
|| !processAppName.equalsIgnoreCase("com.xmliu.imsample")) {
Log.e(TAG, "enter the service process!");
// "com.easemob.chatuidemo"为demo的包名,换到自己项目中要改成自己包名
// 则此application::onCreate 是被service 调用的,直接返回
return;
}
// EMChat.getInstance().setAutoLogin(false);
EMChat.getInstance().init(getApplicationContext());
// 在做代码混淆的时候需要设置成false
EMChat.getInstance().setDebugMode(true);
initHXOptions();
mInstance = this;
}
protected void initHXOptions() {
Log.d(TAG, "init HuanXin Options");
// 获取到EMChatOptions对象
EMChatOptions options = EMChatManager.getInstance().getChatOptions();
// 默认添加好友时,是不需要验证的true,改成需要验证false
options.setAcceptInvitationAlways(false);
// 默认环信是不维护好友关系列表的,如果app依赖环信的好友关系,把这个属性设置为true
options.setUseRoster(true);
options.setNumberOfMessagesLoaded(1);
}
private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this
.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i
.next());
try {
if (info.pid == pID) {
CharSequence c = pm.getApplicationLabel(pm
.getApplicationInfo(info.processName,
PackageManager.GET_META_DATA));
// Log.d("Process", "Id: "+ info.pid +" ProcessName: "+
// info.processName +" Label: "+c.toString());
// processName = c.toString();
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}
@Override
public void onLowMemory() {
// TODO Auto-generated method stub
super.onLowMemory();
Log.i(TAG, "onLowMemory");
}
@Override
public void onTerminate() {
// TODO Auto-generated method stub
Log.i(TAG, "onTerminate");
super.onTerminate();
}
public static BaseApplication getInstance() {
return mInstance;
}
}
public class ChatListAdapter extends BaseAdapter {FriendListAdapter.java
Context mContext;
List<ChatListData> mListData;
public ChatListAdapter(Context mContext, List<ChatListData> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}
@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}
@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
Holder holder;
if (cView == null) {
holder = new Holder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.chat_listview_item, null);
holder.rAvatar = (Button) cView
.findViewById(R.id.listview_item_receive_avatar);
holder.rContent = (TextView) cView
.findViewById(R.id.listview_item_receive_content);
holder.chatTime = (TextView) cView
.findViewById(R.id.listview_item_time);
holder.sContent = (TextView) cView
.findViewById(R.id.listview_item_send_content);
holder.sAvatar = (Button) cView
.findViewById(R.id.listview_item_send_avatar);
holder.sName = (TextView) cView.findViewById(R.id.name1);
holder.sName1 = (TextView) cView.findViewById(R.id.name2);
cView.setTag(holder);
} else {
holder = (Holder) cView.getTag();
}
holder.chatTime.setVisibility(View.GONE);
if (mListData.get(index).getType() == 2) {
holder.rAvatar.setVisibility(View.VISIBLE);
holder.rContent.setVisibility(View.VISIBLE);
holder.sName.setVisibility(View.VISIBLE);
holder.sName.setText("您的朋友说:");
holder.sContent.setVisibility(View.GONE);
holder.sAvatar.setVisibility(View.GONE);
holder.sName1.setVisibility(View.GONE);
} else if (mListData.get(index).getType() == 1) {
holder.rAvatar.setVisibility(View.GONE);
holder.sName.setVisibility(View.GONE);
holder.rContent.setVisibility(View.GONE);
holder.sContent.setVisibility(View.VISIBLE);
holder.sAvatar.setVisibility(View.VISIBLE);
holder.sName1.setVisibility(View.VISIBLE);
holder.sName1.setText("我");
}
holder.chatTime.setText(mListData.get(index).getChatTime());
holder.rContent.setText(mListData.get(index).getReceiveContent());
holder.sContent.setText(mListData.get(index).getSendContent());
return cView;
}
class Holder {
Button rAvatar;
TextView rContent;
TextView chatTime;
TextView sContent;
TextView sName;
TextView sName1;
Button sAvatar;
}
}
public class FriendListAdapter extends BaseAdapter {ChatListData.java
Context mContext;
List<String> mListData;
public FriendListAdapter(Context mContext, List<String> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}
@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}
@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
FHolder holder;
if (cView == null) {
holder = new FHolder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.friend_listview_item, null);
holder.name = (TextView) cView
.findViewById(R.id.friend_listview_name);
cView.setTag(holder);
} else {
holder = (FHolder) cView.getTag();
}
holder.name.setText(mListData.get(index));
return cView;
}
class FHolder {
TextView name;
}
}
public class ChatListData {ChatListActivity.java
String receiveAvatar;
String receiveContent;
String chatTime;
String sendAvatar;
String sendContent;
/**
* 1 发送; 2接收
*/
int type;
/**
* 1 发送; 2接收
*/
public int getType() {
return type;
}
/**
* 1 发送; 2接收
*/
public void setType(int type) {
this.type = type;
}
public String getReceiveAvatar() {
return receiveAvatar;
}
public void setReceiveAvatar(String receiveAvatar) {
this.receiveAvatar = receiveAvatar;
}
public String getReceiveContent() {
return receiveContent;
}
public void setReceiveContent(String receiveContent) {
this.receiveContent = receiveContent;
}
public String getChatTime() {
return chatTime;
}
public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}
public String getSendAvatar() {
return sendAvatar;
}
public void setSendAvatar(String sendAvatar) {
this.sendAvatar = sendAvatar;
}
public String getSendContent() {
return sendContent;
}
public void setSendContent(String sendContent) {
this.sendContent = sendContent;
}
}
public class ChatListActivity extends BaseActivity {ChatLoginActivity.java
private EditText contentET;
private TextView topNameTV;
private Button sendBtn;
private NewMessageBroadcastReceiver msgReceiver;
private ListView mListView;
private List<ChatListData> mListData = new ArrayList<ChatListData>();
private ChatListAdapter mAdapter;
private InputMethodManager imm;
private String receiveName = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_main);
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0x00001:
imm.hideSoftInputFromWindow(
contentET.getApplicationWindowToken(), 0); // 隐藏键盘
mAdapter.notifyDataSetChanged(); // 刷新聊天列表
mListView.setSelection(mListData.size()); // 跳转到listview最底部
contentET.setText(""); // 清空发送内容
break;
default:
break;
}
}
};
receiveName = this.getIntent().getStringExtra("userid");
initView();
topNameTV.setText(receiveName);
// 只有注册了广播才能接收到新消息,目前离线消息,在线消息都是走接收消息的广播(离线消息目前无法监听,在登录以后,接收消息广播会执行一次拿到所有的离线消息)
msgReceiver = new NewMessageBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(EMChatManager
.getInstance().getNewMessageBroadcastAction());
intentFilter.setPriority(3);
registerReceiver(msgReceiver, intentFilter);
imm = (InputMethodManager) contentET.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
mAdapter = new ChatListAdapter(ChatListActivity.this, mListData);
mListView.setAdapter(mAdapter);
initEvent();
}
private void initView() {
contentET = (EditText) findViewById(R.id.chat_content);
topNameTV = (TextView) findViewById(R.id.chat_list_name);
sendBtn = (Button) findViewById(R.id.chat_send_btn);
mListView = (ListView) findViewById(R.id.chat_listview);
}
private void initEvent() {
// TODO Auto-generated method stub
sendBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
sendMsg();
}
});
contentET.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View arg0, int keycode, KeyEvent arg2) {
// TODO Auto-generated method stub
if (keycode == KeyEvent.KEYCODE_ENTER
&& arg2.getAction() == KeyEvent.ACTION_DOWN) {
sendMsg();
return true;
}
return false;
}
});
}
void sendMessageHX(String username, final String content) {
// 获取到与聊天人的会话对象。参数username为聊天人的userid或者groupid,后文中的username皆是如此
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
// 创建一条文本消息
EMMessage message = EMMessage.createSendMessage(EMMessage.Type.TXT);
// // 如果是群聊,设置chattype,默认是单聊
// message.setChatType(ChatType.GroupChat);
// 设置消息body
TextMessageBody txtBody = new TextMessageBody(content);
message.addBody(txtBody);
// 设置接收人
message.setReceipt(username);
// 把消息加入到此会话对象中
conversation.addMessage(message);
// 发送消息
EMChatManager.getInstance().sendMessage(message, new EMCallBack() {
@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送失败");
}
@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "正在发送消息");
}
@Override
public void onSuccess() {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送成功");
ChatListData data = new ChatListData();
data.setSendContent(content);
data.setType(1);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
}
});
}
private class NewMessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 注销广播
abortBroadcast();
// 消息id(每条消息都会生成唯一的一个id,目前是SDK生成)
String msgId = intent.getStringExtra("msgid");
// 发送方
String username = intent.getStringExtra("from");
// 收到这个广播的时候,message已经在db和内存里了,可以通过id获取mesage对象
EMMessage message = EMChatManager.getInstance().getMessage(msgId);
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
MessageBody tmBody = message.getBody();
ChatListData data = new ChatListData();
data.setReceiveContent(((TextMessageBody) tmBody).getMessage());
data.setType(2);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
Log.i("TAG", "收到消息:" + ((TextMessageBody) tmBody).getMessage());
// 如果是群聊消息,获取到group id
if (message.getChatType() == ChatType.GroupChat) {
username = message.getTo();
}
if (!username.equals(username)) {
// 消息不是发给当前会话,return
return;
}
}
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(msgReceiver);
}
private void sendMsg() {
String content = contentET.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(getApplicationContext(), "请输入发送的内容",
Toast.LENGTH_SHORT).show();
} else {
sendMessageHX(receiveName, content);
}
}
}
public class ChatLoginActivity extends BaseActivity {ChatRegisterActivity.java
private EditText mUsernameET;
private EditText mPasswordET;
private TextView mPasswordForgetTV;
private Button mSigninBtn;
private TextView mSignupTV;
private CheckBox mPasswordCB;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_login);
mUsernameET = (EditText) findViewById(R.id.chat_login_username);
mPasswordET = (EditText) findViewById(R.id.chat_login_password);
mPasswordForgetTV = (TextView) findViewById(R.id.chat_login_forget_password);
mSigninBtn = (Button) findViewById(R.id.chat_login_signin_btn);
mSignupTV = (TextView) findViewById(R.id.chat_login_signup);
mPasswordCB = (CheckBox) findViewById(R.id.chat_login_password_checkbox);
if (EMChat.getInstance().isLoggedIn()) {
Log.d("TAG", "已经登陆过");
EMGroupManager.getInstance().loadAllGroups();
EMChatManager.getInstance().loadAllConversations();
startActivity(new Intent(ChatLoginActivity.this,
MainActivity.class));
}
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
//动态设置密码是否可见
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSigninBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else {
EMChatManager.getInstance().login(userName, password,
new EMCallBack() {// 回调
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
EMGroupManager.getInstance()
.loadAllGroups();
EMChatManager.getInstance()
.loadAllConversations();
Log.d("main", "登陆聊天服务器成功!");
Toast.makeText(
getApplicationContext(),
"登陆成功", Toast.LENGTH_SHORT)
.show();
startActivity(new Intent(
ChatLoginActivity.this,
MainActivity.class));
// mApplication.mSharedPreferences
// .edit()
// .putString("loginName",
// userName).commit();
}
});
}
@Override
public void onProgress(int progress,
String status) {
}
@Override
public void onError(int code, String message) {
if (code == -1005) {
message = "用户名或密码错误";
}
final String msg = message;
runOnUiThread(new Runnable() {
public void run() {
Log.d("main", "登陆聊天服务器失败!");
Toast.makeText(
getApplicationContext(),
msg, Toast.LENGTH_SHORT)
.show();
}
});
}
});
}
}
});
mSignupTV.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startActivity(new Intent(ChatLoginActivity.this,
ChatRegisterActivity.class));
}
});
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
new AlertDialog.Builder(ChatLoginActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出"
+ getResources().getString(
R.string.app_name) + "客户端吗?")
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
AppManager.getInstance().AppExit(
ChatLoginActivity.this);
ChatLoginActivity.this.finish();
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
}
}).show();
}
return super.onKeyDown(keyCode, event);
}
}
public class ChatRegisterActivity extends BaseActivity {MainActivity.java
private EditText mUsernameET;
private EditText mPasswordET;
private EditText mCodeET;
private Button mSignupBtn;
private Handler mHandler;
private CheckBox mPasswordCB;
private TextView mBackTV;
private ImageView mCodeIV;
private String currCode;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_register);
mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1000:
Toast.makeText(getApplicationContext(), "注册成功",
Toast.LENGTH_SHORT).show();
break;
case 1001:
Toast.makeText(getApplicationContext(), "网络异常,请检查网络!",
Toast.LENGTH_SHORT).show();
break;
case 1002:
Toast.makeText(getApplicationContext(), "用户已存在!",
Toast.LENGTH_SHORT).show();
break;
case 1003:
Toast.makeText(getApplicationContext(), "注册失败,无权限",
Toast.LENGTH_SHORT).show();
break;
case 1004:
Toast.makeText(getApplicationContext(),
"注册失败: " + (String) msg.obj, Toast.LENGTH_SHORT)
.show();
break;
default:
break;
}
};
};
mUsernameET = (EditText) findViewById(R.id.chat_register_username);
mPasswordET = (EditText) findViewById(R.id.chat_register_password);
mCodeET = (EditText) findViewById(R.id.chat_register_code);
mSignupBtn = (Button) findViewById(R.id.chat_register_signup_btn);
mPasswordCB = (CheckBox) findViewById(R.id.chat_register_password_checkbox);
mBackTV = (TextView) findViewById(R.id.chat_register_back);
mCodeIV = (ImageView) findViewById(R.id.chat_register_password_code);
mCodeIV.setImageBitmap(IdentifyCode.getInstance().createBitmap());
currCode = IdentifyCode.getInstance().getCode();
mCodeIV.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
mCodeIV.setImageBitmap(IdentifyCode.getInstance()
.createBitmap());
currCode = IdentifyCode.getInstance().getCode();
Log.i("TAG", "currentCode==>" + currCode);
}
});
mBackTV.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
finish();
}
});
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSignupBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
final String code = mCodeET.getText().toString().trim();
if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(code)) {
Toast.makeText(getApplicationContext(), "请输入验证码",
Toast.LENGTH_SHORT).show();
} else if (!code.equals(currCode.toLowerCase())) {
Toast.makeText(getApplicationContext(), "验证码输入不正确",
Toast.LENGTH_SHORT).show();
} else {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
// 调用sdk注册方法
EMChatManager.getInstance()
.createAccountOnServer(userName,
password);
mHandler.sendEmptyMessage(1000);
} catch (final EaseMobException e) {
// 注册失败
Log.i("TAG", "getErrorCode:" + e.getErrorCode());
int errorCode = e.getErrorCode();
if (errorCode == EMError.NONETWORK_ERROR) {
mHandler.sendEmptyMessage(1001);
} else if (errorCode == EMError.USER_ALREADY_EXISTS) {
mHandler.sendEmptyMessage(1002);
} else if (errorCode == EMError.UNAUTHORIZED) {
mHandler.sendEmptyMessage(1003);
} else {
Message msg = Message.obtain();
msg.what = 1004;
msg.obj = e.getMessage();
mHandler.sendMessage(msg);
}
}
}
}).start();
}
}
});
}
}
public class MainActivity extends BaseActivity {IdentifyCode.java
private ListView mListView;
private Button mAddBtn;
private Button logoutBtn;
private View addView;
private EditText mIdET;
private EditText mReasonET;
private TextView mUserTV;
private TextView mGoTV;
private FriendListAdapter mAdapter;
private List<String> userList = new ArrayList<String>();
/* 常量 */
private final int CODE_ADD_FRIEND = 0x00001;
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_friends);
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case CODE_ADD_FRIEND:
Toast.makeText(getApplicationContext(), "请求发送成功,等待对方验证",
Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
};
EMContactManager.getInstance().setContactListener(
new MyContactListener());
EMChat.getInstance().setAppInited();
mListView = (ListView) findViewById(R.id.chat_listview);
mAddBtn = (Button) findViewById(R.id.chat_add_btn);
mUserTV = (TextView) findViewById(R.id.current_user);
mGoTV = (TextView) findViewById(R.id.friend_list_go);
logoutBtn = (Button) findViewById(R.id.chat_logout_btn);
mUserTV.setText(EMChatManager.getInstance().getCurrentUser());
initList();
mAddBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
addView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.chat_add_friends, null);
mIdET = (EditText) addView
.findViewById(R.id.chat_add_friend_id);
mReasonET = (EditText) addView
.findViewById(R.id.chat_add_friend_reason);
new AlertDialog.Builder(MainActivity.this)
.setTitle("添加好友")
.setView(addView)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String idStr = mIdET.getText()
.toString().trim();
String reasonStr = mReasonET.getText()
.toString().trim();
try {
EMContactManager.getInstance()
.addContact(idStr,
reasonStr);
mHandler.sendEmptyMessage(CODE_ADD_FRIEND);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG", "addContacterrcode==>"
+ e.getErrorCode());
}// 需异步处理
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
dialog.dismiss();
}
}).create().show();
}
});
logoutBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
showLogoutDialog();
}
});
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatListActivity.class).putExtra("userid",
userList.get(arg2)));
}
});
mListView.setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
// TODO Auto-generated method stub
showDeleteDialog(userList.get(arg2));
return true;
}
});
}
private void initList() {
try {
userList.clear();
userList = EMContactManager.getInstance().getContactUserNames();
mAdapter = new FriendListAdapter(MainActivity.this, userList);
mListView.setAdapter(mAdapter);
} catch (EaseMobException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
Log.i("TAG", "usernames errcode==>" + e1.getErrorCode());
Log.i("TAG", "usernames errcode==>" + e1.getMessage());
}// 需异步执行
}
private class MyContactListener implements EMContactListener {
@Override
public void onContactAgreed(String username) {
// 好友请求被同意
Log.i("TAG", "onContactAgreed==>" + username);
// 提示有新消息
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
Toast.makeText(getApplicationContext(), username + "同意了你的好友请求",
Toast.LENGTH_SHORT).show();
}
@Override
public void onContactRefused(String username) {
// 好友请求被拒绝
Log.i("TAG", "onContactRefused==>" + username);
}
@Override
public void onContactInvited(String username, String reason) {
// 收到好友添加请求
Log.i("TAG", username + "onContactInvited==>" + reason);
showAgreedDialog(username, reason);
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
}
@Override
public void onContactDeleted(List<String> usernameList) {
// 好友被删除时回调此方法
Log.i("TAG", "usernameListDeleted==>" + usernameList.size());
}
@Override
public void onContactAdded(List<String> usernameList) {
// 添加了新的好友时回调此方法
for (String str : usernameList) {
Log.i("TAG", "usernameListAdded==>" + str);
}
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
showExitDialog();
}
return super.onKeyDown(keyCode, event);
}
private void showLogoutDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要注销" + EMChatManager.getInstance().getCurrentUser()
+ "用户吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// EMChatManager.getInstance().logout();
logout(new EMCallBack() {
@Override
public void onSuccess() {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatLoginActivity.class));
}
@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
}
@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
}
});
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}
public void logout(final EMCallBack callback) {
// setPassword(null);
EMChatManager.getInstance().logout(new EMCallBack() {
@Override
public void onSuccess() {
// TODO Auto-generated method stub
if (callback != null) {
callback.onSuccess();
}
}
@Override
public void onError(int code, String message) {
// TODO Auto-generated method stub
}
@Override
public void onProgress(int progress, String status) {
// TODO Auto-generated method stub
if (callback != null) {
callback.onProgress(progress, status);
}
}
});
}
private void showAgreedDialog(final String user, String reason) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"用户 " + user + " 想要添加您为好友,是否同意?\n" + "验证信息:" + reason)
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMChatManager.getInstance().acceptInvitation(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
try {
EMChatManager.getInstance().refuseInvitation(user);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog2==>" + e.getErrorCode());
}
}
})
.setNeutralButton("忽略", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}
private void showDeleteDialog(final String user) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage("确定删除好友 " + user + " 吗?\n")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMContactManager.getInstance().deleteContact(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}
private void showExitDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出" + getResources().getString(R.string.app_name)
+ "客户端吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AppManager.getInstance().AppExit(MainActivity.this);
MainActivity.this.finish();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}
}
public class IdentifyCode {布局文件就相对简单很多了,登录页面很简单,还是贴出来吧。
private static final char CHARS = { '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
private static IdentifyCode bmpCode;
public static IdentifyCode getInstance() {
if (bmpCode == null)
bmpCode = new IdentifyCode();
return bmpCode;
}
// default settings
private static final int DEFAULT_CODE_LENGTH = 3;
private static final int DEFAULT_FONT_SIZE = 25;
private static final int DEFAULT_LINE_NUMBER = 2;
private static final int BASE_PADDING_LEFT = 5, RANGE_PADDING_LEFT = 15,
BASE_PADDING_TOP = 15, RANGE_PADDING_TOP = 20;
private static final int DEFAULT_WIDTH = 60, DEFAULT_HEIGHT = 40;
// settings decided by the layout xml
// canvas width and height
private int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;
// random word space and pading_top
private int base_padding_left = BASE_PADDING_LEFT,
range_padding_left = RANGE_PADDING_LEFT,
base_padding_top = BASE_PADDING_TOP,
range_padding_top = RANGE_PADDING_TOP;
// number of chars, lines; font size
private int codeLength = DEFAULT_CODE_LENGTH,
line_number = DEFAULT_LINE_NUMBER, font_size = DEFAULT_FONT_SIZE;
// variables
private String code;
private int padding_left, padding_top;
private Random random = new Random();
// 验证码图�?
public Bitmap createBitmap() {
padding_left = 0;
Bitmap bp = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas c = new Canvas(bp);
code = createCode();
c.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setTextSize(font_size);
for (int i = 0; i < code.length(); i++) {
randomTextStyle(paint);
randomPadding();
c.drawText(code.charAt(i) + "", padding_left, padding_top, paint);
}
for (int i = 0; i < line_number; i++) {
drawLine(c, paint);
}
c.save(Canvas.ALL_SAVE_FLAG);// 保存
c.restore();//
return bp;
}
public String getCode() {
return code;
}
// 验证�?
private String createCode() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
buffer.append(CHARS[random.nextInt(CHARS.length)]);
}
return buffer.toString();
}
private void drawLine(Canvas canvas, Paint paint) {
int color = randomColor();
int startX = random.nextInt(width);
int startY = random.nextInt(height);
int stopX = random.nextInt(width);
int stopY = random.nextInt(height);
paint.setStrokeWidth(1);
paint.setColor(color);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
private int randomColor() {
return randomColor(1);
}
private int randomColor(int rate) {
int red = random.nextInt(256) / rate;
int green = random.nextInt(256) / rate;
int blue = random.nextInt(256) / rate;
return Color.rgb(red, green, blue);
}
private void randomTextStyle(Paint paint) {
int color = randomColor();
paint.setColor(color);
paint.setFakeBoldText(random.nextBoolean()); // true为粗体,false为非粗体
float skewX = random.nextInt(11) / 10;
skewX = random.nextBoolean() ? skewX : -skewX;
paint.setTextSkewX(skewX); // float类型参数,负数表示右斜,整数左斜
// paint.setUnderlineText(true); //true为下划线,false为非下划线?
// paint.setStrikeThruText(true); //true为删除线,false为非删除线?
}
private void randomPadding() {
padding_left += base_padding_left + random.nextInt(range_padding_left);
padding_top = base_padding_top + random.nextInt(range_padding_top);
}
}
activity_chat_login.xml
<?xml version="1.0" encoding="utf-8"?>好友列表页
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/chat_login_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#CAFFFF"
android:orientation="vertical"
android:paddingBottom="30dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="60dp" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:orientation="vertical" >
<EditText
android:id="@+id/chat_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#00000000"
android:drawableLeft="@drawable/login_user"
android:drawablePadding="5dp"
android:ems="10"
android:hint="用户名"
android:inputType="textPersonName"
android:textColor="#fff"
android:textSize="12sp" />
<View
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:id="@+id/chat_login_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="#00000000"
android:drawableLeft="@drawable/login_password"
android:drawablePadding="5dp"
android:ems="10"
android:hint="密码"
android:inputType="textPassword"
android:textColor="#fff"
android:textSize="12sp" />
<CheckBox
android:id="@+id/chat_login_password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:button="@drawable/password_checkbox" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/chat_login_signin_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="#359D90"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/chat_login_signup0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#5D5D5D"
android:textSize="12sp" />
<TextView
android:id="@+id/chat_login_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/chat_login_signup0"
android:text="注册用户"
android:textColor="#6F6F6F"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/chat_login_forget_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="忘记密码"
android:textColor="#5D5D5D"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
activity_chat_friends.xml
<?xml version="1.0" encoding="utf-8"?>本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七 收起阅读 »
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#359D90"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingTop="5dp" >
<TextView
android:id="@+id/current_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我的好友"
android:textColor="#fff" />
<Button
android:id="@+id/chat_logout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:background="@drawable/chat_logout_icon" />
</RelativeLayout>
<TextView
android:id="@+id/friend_list_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textStyle="bold|italic"
android:textColor="#000fff"
android:text="好友列表" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="5dp"
android:background="#DDDDDD" />
<ListView
android:id="@+id/chat_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="none" />
<Button
android:id="@+id/chat_add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/send_btn_bg"
android:paddingBottom="12dp"
android:paddingLeft="10
Parse 移植:如何将 Parse 服务器迁移部署到 Heroku 或 AWS 上
作者:Gregg Mojica at AppCoda
原文日期:2016-04-16
译者:Crystal Sun
继续我之前的这篇文章 migrating a parse database to a self-host MongoDB instance,在这次的春季辅导教程中,我们看一下如何将 parse 服务器迁移到 Heroku 和 Amazon Web Service。
对于还不了解 Parse 之死的人来说,这意味着服务器(处理数据,与数据库互动,发送接收请求等待)需要迁移到其他地方了。Parse,后端即服务(BaaS),为开发者提供服务器和数据库的服务。然而,随着 Parse 即将在一月份关闭,官方建议,在2017年1月28日彻底停止服务之前,请迁移 Parse 应用。Parse 官方建议你先迁移数据库,然后在迁移服务器。本节教程会假定你一看完成了数据库的迁移,正如我们在上篇教程第一部分中所做的。
幸运的是,parse-server(GitHub 项目,由 Facebook 开源,伟大的 Parse 统治者)可以部署在大部分的云服务上。在本节教程里,我们会讲述如何将 parse-serve 部署到 Heroku,Salesforce 旗下知名的云服务供应商。在本篇文章的最后部分,我们会演示如何部署到 Amazon Web Services(AWS)上,世界上很多知名的 App 都在使用 AWS 的服务。
准备开始
首先到 Heroku.com 网站注册一个帐号。为了演示 demo,我选择了免费方案。你根据自己的需要,选择合适的方案,比如付费方案。你可以在这里看到所有的付费方案。
部署到 heroku 有两种方法可供选择。第一种是点击 Deploy to Heroku 按钮,然后出现一步接一步的提示流程,因为 Parse 已经在 Heroku 的服务器上设置过 parse-server 了,对非 Javascript 程序员来说,这可能是最简单的方法了。如果你熟悉 git 和命令行,请随意使用克隆应用然后用命令行完成。话虽如此,但是你不能一辈子都避免使用命令行。不管你选择那种方式,都会涉及到命令行的。
Option 1: 使用 Heroku 按钮
点击上面的按钮,创建一个新的 heroku 应用,你会看到类似下方图片的界面:
设置向导出现,让你输入应用名称(全部小写不允许有空格)。
接下来,选择 runtime 选项。如果你住在美国,选择 United States(美国),其他地方,选择 Europe(欧洲)。runtime 选项,就是你希望你的应用部署在哪个地方。考虑到性能和速度,最好将应用服务器部署在离你较近的地方。
接下来更新配置,填写 Parse 账户里对应的密钥(或者生成新的密钥,如果你不是迁移现存应用的话,这点以后再说)。安装路径为 /parse。
当你填完所有的字段后,点击 deploy 按钮,暂时先空着 MongoLab(也就做 mLab)开发。
可能需要你输入你的信用卡。
Option 2: 克隆 Heroku 应用
parse-server 是开源项目,目前可以在 GitHub 上下载。如果你选择的是命令行,而不是点击 heroku 按钮,那么继续下方的操作。开始前,先打开终端(Terminal),使用下方的命令来克隆应用:
cd ~
cd Desktop
git clone https://github.com/ParsePlatform/parse-server-example.git
git add .
git init
git commit -m "Initial Commit"
现在,你已经成功地将 parse-server 克隆到桌面上了。
修改数据库的 URI
不管你在上面选择了哪个方式,现在你的应用在一定程度上已经设置过了。如果你使用是 Option 1,你需要在你电脑里复制一份本地代码副本,首先用下列命令行(也会将 App 克隆到电脑桌面)。
注意:下方的选项适用于选择了 Option 1 的人
$ heroku login
$ cd ~/Desktop
$ heroku git:clone -a your-app-name
$ cd your-app-name
$ git add .
$ git commit -am "make it better"
$ git push heroku master
登录后,需要输入认证(之后会详细说明,不过现在只需要输入 Heroku 帐号的邮箱和密码,密码不会出现在屏幕上)。
现在,打开你最喜欢的文本编辑器(我比较喜欢 Sublime Text),打开新克隆的库(repository)(对于新手来说,你可以直接将整个文件夹拖到 sublime text 图标上,然后 sulime text 会自动文件,或者使用顶部菜单的 File -> Open)。
现在,我们需要打开 index.js 文件,修改 API 变量。注意第 14-23 行。
从第 14 行开始,我们需要修改 databaseURL 参数。替换路径,使用在本教程第一部分生成的路径,例如,我会使用下面的 url,不过你必须用你自己的 url 来替换。
mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test
接下来,我们需要填写 appId 和 masterKey 参数。如果你是在迁移一个已经存在的应用,到 parse.com 上找到对应的数据。如果这是你第一次使用 parse-server 创建一个新工程,你可以生成随机的字幕数字组成的密钥。
在 parse.com 网站上登录你的 Parse 帐号,找到 Settings(设置),在这里,选择 Security & Keys。复制粘贴你的 Application ID(这个应用的,不要复制成其他应用的)和 Master Key。下面的图片可供你参考(我的密钥出于安全考虑遮挡住了)。
注意:如果你选的是 Option 1,你已经设置了你的密钥,你可以直接跳过这一步。即使如此,我还是建议你看一下,这样你能对 parse-server 的工作机制有更深入的理解。在 index.js 文件里替换上你刚刚复制来的新密钥,你也可以添加 clientKey 作为一个参数,从 Parse 中获取。
最后,记住保存你的操作,快捷键 Command+S(Mac电脑上)。
如果你不是迁移应用,那么使用随机生成器(例如 random.org 或其他类似的东西)来生成字母数字密钥。
接下来,部署 Heroku。
将 Parse 服务器部署到 Heroku
首先在电脑上安装 Heroku 工具条,从链接中可以找到官方安装指南。安装完成后,在终端(Terminal)中输入下列命令行:
heroku login接下来输入登录 Heroku 信息,注意当你输入密码的时候,密码不会出现在屏幕上。
如果你选择的是 Option 1,就没有必要用下面的命令行创建一个 Heroku 应用了。如果你选择的是 Option 2,确保输入下列命令行来创建一个 Heroku 应用。
heroku createHeroku 会给你创建一个应用,现在提交修改内容,代码如下:
git add .现在,你已经成功部署了 Heroku!如果你遇到任何错误,请在下方的评论栏中留意,我将尽力帮助你。
git init
git commit -m "Updated api config"
git push heroku master
设置 Heroku 的环境变量
接下来,我们需要设置 Heroku 的环境变量,回到终端(Terminal),输入下列命令行(使用你的 MongoDB 实例中的URI,我们之前谈论过)。
heroku config:set DATABASE_URI=mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test回到 Heroku 网页上,点击你的应用,在 Settings tab 页下,点击 reveal config variables。
现在你应该可以看到 Heroku 的 config Variables 里有了 database URI。
恭喜你!你的 parse-server 已经成功地部署到了 Heroku。唯一的问题是:还没有连接到你的 iOS 应用上。
定位 Parse 服务器的 URL
为了能够将你的应用连接到新的 parse-server,首先要从 Heroku 应用设置里定位托管地址(hosting url)。
回到 index.js ,找到第 27 行,注意找 moutPath 变量是 /parse。
这个变量表示 parse 在 Heroku 服务器上的地址。目前来说,地址是 /parse。所以,可以在 yourapp.herokuapp.com/parse(改成你自己的域名) 中访问 parse-server。
设置 iOS 应用
现在,我们已经正确地配置和部署了服务器,是时候来设置 iOS 应用设置选项了,让 iOS 应用连接到新的 parse 服务器上。
在 Xcode 里,打开应用,选择 appdelegate.swift 文件,删除你以前的 app key 和 client key(然后写上你自己的密钥和服务器的 url)。
把下面这段代码删掉:
Parse.setApplicationId(“xxxxxxxxxxxxxxxxxxxxxxxx”, clientKey: “xxxxxxxxxxxxxxxxxxxxxxxx”)替换成:
let config = ParseClientConfiguration(block: {完成操作后,点击 Run 按钮,测试一下应用。正常情况下应用会和迁移以前一样运行。如果你使用的YY待命,你可能需要修改一下代码,来适应新的 parse 服务器环境。我们会在下一个教程中涉及这个话题。另外,在下一个教程里,我们还会介绍在服务器里托管 Parse 的 dashboard。不过现在而言,你可以继续使用 parse.com 的 dashboard,知道官方彻底关闭服务,也就是在 2017 年的一月。
(ParseMutableClientConfiguration) -> Void in
ParseMutableClientConfiguration.applicationId = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.clientKey = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.server = "xxxxxxxxxxxxxxxxxxxxxxxx.com/parse";
});
Parse.initializeWithConfiguration(config);
恭喜你!你已经成功地在 Heroku 上部署了 parse-server。
将 Parse 服务器部署到 AWS
注意:如果你已经将 parse-server 部署到了 Heroku 上,那么就不需要再部署到 AWS 上了,毕竟你的服务器只能使用一个云服务。这部分主要是用来参考的。如果你不想使用 Heroku,想使用 AWS,你可以继续阅读下面的章节。然而,我会假设你已经阅读过上面 Heroku 部分的教程内容,如果出现同样的设置内容,我不会再次赘述了。Amazon Web Services(AWS)是全球知名的云服务提供商,为科技界许多知名的大型公司提供云服务。实际上,很大大型科技公司都在使用 AWS 的服务,例如苹果公司的 iCloud,Hulu,AirBnb,Lyft,Adobe,Slack (这些都是国外知名的科技公司)等等,这些只是使用 AWS 云存储服务的众多公司中一小部分。
那么,为什么我先介绍 Heroku 呢?不同于 AWS 的是,Heroku 更容易设置。对于大部分的设置,你可以直接进行无需输入账单信息。AWS 则不一样,设置方法比较复杂。为了演示如何部署到 AWS 上,我们将使用另外一个部署按钮和设置向导,来让所有的工作简单流畅。
再次强调一下,如果你已经将应用部署到了 Heroku,而且对 Heroku 的服务比较满意,你可以直接跳过这部分了。然而,如果你对如何部署到 AWS 上感兴趣,那么让我们开始吧!
第一件事,到 AWS 上注册一个 AWS 帐号,需要提供你的付款信息,这样才能使用免费方案。
完成后,点击下方的按钮,创建一个新的 AWS 应用,AWS 提供一组云服务工具,每个工具都有自己的独特的功能,在本节教程中,我们使用 Elastic Beanstalk(和 Elastic Cloud Compute Engine 或简称 EC2 紧密相关)。
什么是 Elastic Beanstalk ?点击按钮后,会出现一个增加应用名称的界面,如下图。
根据 Amazon 上的简介,Elastic Beanstalk 是一个易于使用的,用于部署和扩展网页应用和服务,适用的语言有 Java、.NET、PHP、Node.js、Python、Ruby、Go、Docker,例如 Apache, Nginx, Passenger,和 IIS。
好炫的语言是吧?或许吧,总而言之,我们将使用这个服务来设置和运行我们的 parse 服务器。如果你想了解更多有关 Elastic Beanstalk 的信息,请参考官方网页。
下一步,确保你的设置如下图,然后继续。
在接下来的界面里使用正确的密钥上传 parse 设置,parse 装在 /parse 下。
恭喜你!你成功将 parse 服务器部署到了 AWS 上!剩下需要做的事情就是用适当的密钥和新的服务器 url 来设置 iOS 应用(后缀 /parse)。
结束
在本节教程中,我们深入了解了部署 parse 服务器的过程,估计现在你对部署过程已经掌握的比较牢固了。
然而,我们还留下了一些小细节没有处理(感觉这句话翻译的不对)。如果你使用的是云代码,你不得不修改代码,来保证运行正常。另外,你可能还想要一个 Parse dashboard 的替代品。幸运的是,Parse 团队已经将 dashboard 开源了,并提供了分步指南,供你更新云代码。在之后即将到来的教程中,我们会详细讨论这些内容。不过现在,你还是应用集中将应用部署到 AWS 或 Heroku 上!
你觉得本教程怎么样?请尽情地留下评论,分享你的想法。
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。
收起阅读 »
Android V2.2.8 ,ios V2.2.4release,视频通话时支持根据当前网速自动调整码率
更新简介:
1、视频通话时支持根据当前网速自动调整码率。(android&ios)
之前的版本中视频的码率是固定的,该版本增加了自动调整的API接口,可以设置成根据当时的网络状况自动调整码率,保证视频的流畅性。
2、修复消息扩展字段里包含特殊字符,接收方kill程序再进的时候消息不显示的bug(android)
3、修复一个登录相关的问题(android&ios)
4、修复一些ui相关的问题,如发送视频消息时,视频时长显示不对(android)
5、修复收到异常消息crash问题。(ios)
rest发的消息中value中的value为null的话之前会crash,现在是忽略掉这这种消息。
版本历史:ios更新日志 Android更新日志
下载地址:SDK下载
SDK接入过程中有任何问题建议请直接回复跟帖。 收起阅读 »
4.5亿用户,环信助力快牙打造跨平台传输神器
快牙简介
快牙是众多快速发展、跨平台的无线文件传输工具之一。它传输文件的速度远超蓝牙、NFC和Airdrop,让用户无需使用移动网络流量时即可分享apps、照片、音乐、视频或者其他任意一种文件。除了作为WI-FI文件传输工具外,快牙还提供可以本地安装的网络游戏供用户娱乐。
快牙的工作原理
快牙可以让任何内容(包括apps)从一个手机直接复制到另一个手机。但是,如果你朋友还未安装快牙,快牙也提供了简便的方式让你朋友直接从你的手机上把其复制过去,所以你能在任何情况下与你的朋友们分享任何文件。用一句话来说,如果你的朋友向你推荐了一款app而你并不想搜索整个app商店去下载,你只要直接使用快牙就能马上安装这个app。
快牙背后的故事
快牙起家于2011年,刚开始是为各个vendors在安卓API中提供无线热点功能的定制化开发。由于它不需要占用移动流量也不需要WI-FI网络连接就能进行大文件(包括图片和电影)分享,快牙迅速在学生及其他负担不起4G网络和大流量套餐的人群中流行开来。同时,其也在运营商信号不稳定的地方极其流行,比如说:中国、东南亚、中东和印度。它在发展中国家增长的速度最快,在想要花尽量少的钱又可以传输文件的劳动人群和学生人群里面最为流行。
快牙的进化和挑战:
使用环信之前 在刚开始,快牙就在寻找可以增长用户和让他们的用户更有参与感的方式。但他们发现一但学生或者工人身份的用户离开了他们群组的物理地点,对快牙的使用量就马上降低了。此外,文件分享也不是一个每日都会被使用到的工具,这也是快牙用户增长面临的难题之一。但是,快牙通过观察开始注意到这个app其实有着社交的因子,并且发现如果给他们的app加一层社交网络的元素会让app的使用体验更丰富。这也是他们首次开始思考给app增加即时通讯功能来服务他们的用户。同时,快牙还想在未来打造以兴趣为主题的用户群组。
快牙还发现在2014年底,他们现有的2亿用户已经给他们的后端系统带来了压力。例如在周末时间段,大量用户分享图片时,数据有达到峰值的趋势。所以,他们开始寻找更好的即时通讯解决办法来助力于他们的高速增长和足以应付突发的流量峰值。很多游戏公司告诉他们,应该要自主开发一套即时通讯功能供app使用,但他们不希望自主开发即时通讯系统使开发团队的重心从他们的核心功能上转移。在他们开始探索可能的解决方案时,最终发现如果执意要自主开发一个能够支持大流量、可扩展、高性价比的即时通讯系统将会是一个十分艰难的挑战。
在估算成本投入时,快牙发现如果他们的用户增长与流量继续按现在这种爆发式增长时,成本的投入将不可负担。在测试使用自主开发的即时通讯系统时发现,如果想要在每个服务器上满足接近一百万用户的连接量时,需要花费几乎不可承担的极高成本。
如果开发一个能满足其流量需求的系统,除了非常昂贵外,还将面临极大的系统风险。因为如果其中一个服务器瘫痪了,将由于资源不足无法导流而使服务中断。如果要消除这种隐患,必须使用更多的服务器,以降低每台服务器的工作负载。然而,这将使系统成本和复杂性急剧增加。
因此,快牙需要一个能处理峰值流量,具备扩展性能,同时拥有低成本及容错性的第三方即时通讯解决方案。
快牙的选择和收获:使用环信后
在对比自主研发和其他厂商的解决方案后,快牙选择了环信。环信在技术和成本上提供了最佳解决方案,同时还拥有一支行动快速、反馈及时的技术团队,能让快牙在最短时间内运行在高容错性、可扩展、低成本的企业级系统上。
如果当时没有环信,快牙将浪费很多时间、金钱,并且将使产品研发重心转移。此外,环信的IM长连接技术使快牙能够通过发送一些专业市场营销内容,大幅提升日活用户数。
把复杂的大规模即时通讯移动端优化问题交由环信专家解决后,快牙就能专注于产品和app的快速迭代。快牙现在每日能处理1.2亿并发的消息并且还在持续增长。与优秀的即时通讯厂商合作,使他们对于快速扩张没有任何顾虑。
快牙的挑战
减轻后端的数据存储负担
创建客户通讯能力
快速提升并发连接能力
控制成本的增长
系统具备容错能力
产品研发仍聚焦快牙核心竞争力
快速部署
解决方案:
环信提供的即时通讯云服务
具备1000台服务器规模的IM
系统部署能力
IM长连接让快牙能与用户主动沟通
扩展性—按需提供,弹性扩容
数据在服务器之间平均分配,更具容错性
即时通讯和后端扩展交由环信,快牙专注于自身产品
三个月即可就绪的企业级解决方案
结果:
快牙成为中国排名第一的工具类App--提供本地文件分享与游戏功能(BT和WIFI)。截止2015年,快牙月活跃用户8000万,日传输超1.2亿次。期间共计54个迭代版本,新增改进功能累计270余个。近期随着快牙4.0的发布,将开启一场自我颠覆的革命。新快牙,是分享,是朋友们发现和获取私密内容及猛料的服务,相信环信即时通讯云服务也将发挥更大的价值!
收起阅读 »
内容为王 一个好的摘客
如何发现互联网的内容时代来了?
“我是papi酱,一个集美貌与才华于一身的女子。”
没有看过papi酱的视频,那你也一定听过papi酱的段子,这就是新一代的网红papi酱,从秒拍开始,逐渐将短视频带领进新时代的网红。
360的总裁齐向东主导的新闻团队即将从360公司脱离出来,成立一个新的公司,专注打造新媒体产品“北京时间”;新一代网红Papi酱拿到了1200万人民币投资,罗辑思维公布了其与Papi酱的具体合作,即拍卖Papi酱视频贴片广告一次,并由罗辑思维全程策划监制服务,掀起内容运营的蓝海;爱奇艺《奇葩说2》首播破4000万,更是引领纯网内容进入黄金时代。
互联网正在越来越成为一个内容创业的平台。做好内容,产品才能有市场。
产品定位
正如应用商店所写的介绍那样,摘客是“个性化资讯推荐”软件。摘客的主要功能是阅读,却又不只是一个简单的阅读平台,应该说摘客是一个包含了推荐、分享、交流等功能的更加懂你的推荐阅读平台。
需求分析
摘客作为一款深度个性化定制的资讯阅读应用,整合了各大门户网站、博客、论坛的资讯,以杂志式的排版方式进行内容再现。做好内容,满足用户阅读的需求,这是摘客产品的初衷。
用户群体是广大对IT资讯感兴趣的读者。他们需要能够找到更多自己感兴趣的文章,获得有趣的阅读体验或者是学习专业知识。
用户真的需要个性化推荐?
网易云音乐的个性化推荐在推荐算法领域风生水起。推荐算法并不能保证推出的东西一定得到该用户的喜爱,但至少能保证推荐出的一些东西一定有用户喜爱的。这其实是一个快速筛选的过程,在互联网阅读碎片化且快节奏的环境下是非常凑效的。
大多数的资讯类产品都有“订阅”的功能,而个性化推荐其实正是订阅的一个隐形形式。所以摘客最主打的部分就是个性化推荐,同样作为摘客产品一个亮点的是热点推荐。
摘客APP首页
与竞争产品的优势?
摘客与网易新闻、新浪新闻的关系应该就像店铺与淘宝的关系。摘客将新闻像商品一样上架对其进行统一格式的整理,分类,最后尽量精确的分发给用户。各类知名网站就是我们商品的货源。
与摘客最相似的两个竞争产品应该是今日头条和推酷。大家都是做内容聚合的平台,且都“不生产内容,只做内容的搬运工”。这个竞争的问题留给摘客的挑战是什么样的呢?
答案是:再做一次有选择的聚合。摘客的重点还是归于“摘”,直接从新闻来源控制质量,再经过每一轮的算法筛选,以留下最精准和优质的文章为目标。相对于今日头条,摘客做更细分的领域;相对于推酷,摘客做更广的领域。
今日头条app首页
推酷市场介绍
摘客的价值在于?
既然作为一个产品就要有价值的体现,而资讯的价值就是摘客的价值。资讯的价值在于传播和学习,摘客作为一个聚合阅读的平台希望给用户正真“干”的东西。未来的摘客有很多可能,可能会侧重发掘展示UGC(用户原创)的内容,或者会借势网红经济的风口再成长?都是摘客作为内容型产品可能在思考的。
更长远来说,资讯本身就是一种商品,所以内容收费未来可能成为趋势,借鉴知乎最新的产品“值乎”。摘客的未来可能走内容收费模式,做高端定制内容需求,其商业模式的想象空间仍然很大。 最后说说这个团队 最后来说说这个产品的制作团队,前端后台一共10个人不到,都是研究型的人才。最开始做的是大数据分析系统,之后也做一些舆情分析,最后发现已经储藏了很多,于是摘客也就诞生了。
因为摘客中的好多技术其实在做旧的项目时都已经有雏形,有些甚至直接是研发人员学位研究课题,正好借摘客的平台付诸实践和尝试,所以摘客也是新思维碰撞的产物。 需要说明的是,产品的更新迭代一直没有停止,摘客也是有很大改进空间的,作为一个用户,最希望的就是web2.0,也就是用户原创内容,在摘客有展示平台就更好了。
最后再说一下,摘客的制作团队是网新恒天的HTA项目组,有兴趣可以来参观交流,可借鉴内容很多~
收起阅读 »
【公告】环信直播课堂内容、界面全面升级--本期暂停直播
环信直播课堂创建于2016-02-19,迄今为止,已经陪伴了我们走过九期,历史收看人数6191。
非常感谢为我们直播讲解的每一位老师,@beyond @zhuhy @一鸣 @FUccc @shenc @东海
同事也感谢小伙伴们一直以来的陪伴!
在历经9期的直播里,很高兴服务了那么多小伙伴,同时,也看到了很多我们的不足。
从4月份起,就一直在筹备环信直播课堂的升级,我们新版本将集投票、点播、订阅、聊天于一体,打造最人性化的环信直播课堂(环信小伙伴也可以申请当主播哦!)
暂停一期环信直播课堂,下次上线时间待通知。
您可以把对环信直播课堂的建议和想听的内容直接跟帖回复。
THX!
收起阅读 »
13个开源项目集体登场,这是一场开源的技术盛宴!
本次活动,我们邀请到了来自业内的开源大牛,分享他们对开源的见解,一同探讨开源项目的未来。
这里没有广告和套路只有满满的干货!有的是开源碰撞出来的火花,有的是对于开源美好未来的无限畅想,有的是环信对于开源理念的践行,还有一群志同道合的小伙伴!
开源项目介绍:
宅不住:发现城市精彩运动,认识周边潮人
宅男福利:一款美女直播应用
图忆:基于位置信息的分享与社交应用
咚咚:一款高效团队沟通的移动客户端
Cloud Developer:程序员之间互相交流学习的平台
美肤GO:专注于个人海外代购及护肤咨询分享的APP
文播:一款文章直播平台性的APP
方圆十里:关注方圆十里内的人和事
薅羊毛:针对技术人员的社交软件
高仿微信:基于环信SDK高仿微信
他乡:发现老乡,联络老乡,老乡互助的平台
公众号助手:未认证的公众号与用户之间的便捷联系
致敬传奇:怀恋偶像kobe
报名来到现场,共聚一堂,与开源大牛们面对面交流,
报名来到现场:http://www.easemob.com/event/hackathon_party/ 收起阅读 »
互联网周刊:环信——以朴素的技术坚守改变世界的价值
市场规模将实现从60亿美金到3000亿美金的跳跃
《互联网周刊》专访环信CEO刘俊彦
企业级信息消费是新的系统性机会。
“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服务市场打开了,这是最大的区别之一。”谈及中美企业级服务市场差异的时候,刘俊彦说道。在消费级市场上,中美前三名企业的市值总量均在千亿美金规模,处于同一数量级,而在企业级服务市场,中美之间则存在巨大的鸿沟。美国Oracle、微软、SAP等三家公司加起来市值大约3000亿美金,中国用友、金蝶加起来大约60亿美金。与此同时,中美市场的企业数量均在2000多万家,因此可见中国企业级服务市场的潜力巨大。
“目前在IT领域,中国存在重要的系统性机会,当一个国家两千万中小企业开始进行IT能力消费的时候,市场规模将实现从60亿美金到3000亿美金的跳跃。在系统性机会中,谁能继续淘金是很关键的问题。”
中国企业级服务市场的系统性机会不止于此。美国企业级服务市场发展已经趋于成熟,现在如果想进入客服软件等行业基本没有太多机会,相应的中国市场则尚是一片蓝海,甚至竞争者有些荒芜。若具备强执行力、先发优势和资本支持,创业公司在这个新兴产业脱颖而出相对较容易,并且有可能成为小巨头。到此还远远没有结束。美国市场已经出现微软、Oracle、SAP等千亿美金的综合性软件公司,中国则不然。基于中国市场的规模和前景,垂直行业的小巨头通过横向扩张完全有机会成长为综合性软件巨头,媲美微软、Oracle等。“中国在1~2年内会出现一批垂直领域百亿人民币的公司,3~5年内会出现第一批千亿人民币的公司,出现中国自己的综合性软件巨头。”刘俊彦期待道。
中国企业级SaaS服务已然呈现出爆发式发展
在基础服务市场格局成型的情况下,垂直领域的先发优势是创业公司的竞争力所在。
综观国内外,作为万亿级的超级大市场,云服务市场很难出现一家独大的局面,而是有着比较明显的划界和分化的趋势。最底层IaaS层是整个云计算的基础,为企业提供基础的计算能力和计算架构。国外最具代表性的有AWS、微软等,国内阿里云、UCloud、青云等竞相崛起。基于IaaS层,众多企业搭建云计算的能力才得以实现,它是整个云计算生态圈的支撑。第二层是PaaS层,目前发展已相对成熟,出现明显的巨头化和分化趋势。IaaS厂商做到一定程度以后,可能不满足只做IaaS,而是往核心PaaS层面延伸。不管是AWS、亚马逊,还是国内的互联网巨头,均积极布局核心PaaS能力,包括大数据、人工智能、通信能力等。第三层则是SaaS层,经过2015年SaaS元年,中国企业级SaaS服务已然呈现出爆发式发展,绝大多数的创业企业和创业方向涌向SaaS领域,可谓百花齐放。与美国数千家SaaS企业在垂直领域蓬勃发展相比,中国SaaS市场尚处于初级阶段。
“就目前的中国市场而言,如果不具备先发优势,在IaaS层和PaaS层将很难和巨头竞争,但SaaS层因为和行业、业务贴合紧密,有天然的壁垒和深度,非常适合创业公司介入。”刘俊彦表示,尽管如此“在IaaS和PaaS层创业公司也不是绝对没有机会,巨头之外仍有一些做得很好的创业公司。这个世界永远不可能一家独统天下。”
人工智能聊天正成为新的入口和蓝海
人工智能和大数据技术将彻底改变整个软件行业,这是一个技术足以改变世界的时代。
阿尔法Go和人类的围棋大战将人工智能市场的关注度推上了高潮,在云服务市场,人工智能发展尚未普及和成熟。值得注意的是App市场已经趋于饱和,人工智能聊天正成为新的入口和蓝海。
“人工智能和大数据技术将彻底改变整个软件行业。”当被问及人工智能将为云服务市场带来哪些革命性影响时,刘俊彦坚定地答道。
环信已推出全媒体智能客服,通过完全自主或人机混合模式的智能机器人技术极大降低人工客服工作量。在这个市场若想实现可持续发展,一定不是陷入同质化产品的无谓竞争,而是基础IT能力的实力较量。一针见血。这个技术出身、目前仍自诩程序员的创业者,洞察市场和未来的时候却不失犀利。
领导层的战略眼光和志向所在往往决定着一个企业的成长和命运,这一点在刘俊彦和环信上有着充分的体现。
一个成立仅两年左右的企业,能够在高手如林的市场迅速成长为驰骋的黑马,多少让人在敬佩之余想一探究竟。在这样一个互联网巨头虎视眈眈的市场,尽管现在看来一片蓝海,却丝毫不可松懈,而是要时刻保持敏锐的市场嗅觉和果断抉择的气魄。从交谈期间刘俊彦多次提及机会、风口和速度或许可见一斑,更显著地渗透在环信成长之路的点点滴滴。在发布即时通信产品仅4个月的时候,环信正式决定进军SaaS方向,作为当时在PaaS行业尚未做深做透的初创公司,其魄力已初步显现, “环信天然在早期接触到了用户的真实需求,结合PaaS方向的价值积累和战略思考,决定进军SaaS行业。”
这个决定被事实证明是正确的,并且得益于PaaS领域的积累和导流以及先进的技术实力,环信很快建立起强大的竞争壁垒,成为SaaS客服软件行业唯一一家同时拥有PaaS和SaaS产品的公司。在国内SaaS新兴增量市场,要做好全媒体智能客服软件,即时通信技术必不可少,从PaaS到SaaS的拓展水到渠成。选择做正确的事,而不仅仅是正确地做事,不仅仅是眼光,更是智慧。
技术可以改变世界的前提,是人文思想
不要被短期的利益迷惑,忘记自己的身份和目标。
鉴于企业级服务千差万别,如何平衡定制化和标准化成为SaaS行业不可忽视的问题。对此,刘俊彦表示任何SaaS企业都需要分三步走,第一要从小做起,为小企业提供优质的标准化服务,第二有一定积累后再开始接触中大型企业,带动一些可定制模块的开发,第三是做PaaS平台,团结生态圈的企业,平台与生态圈企业合作共赢,满足各种定制化需求。谈及此,刘俊彦语重心长:“到底是做外包公司,还是做SaaS的产品公司,这一点好多公司都迷失了。千万不要被钱迷惑了。”在这样的坚守下,环信目前已经开始进入PaaS平台化阶段。
谈及创业初衷,他说:“我们还是比较‘朴素’,我们想让好的技术造福更多的人,改造中国社会,帮助中国社会更好地进步。”环信的几个创始人曾在大型外企研发中心做了多年的技术,被刘俊彦形容为“跨国公司的螺丝钉”,当时他们有很多好技术和产品跟公司大方向不吻合就被湮没了。“中国有一批非常优秀的技术人才,不应该只做跨国公司的螺丝钉。适逢创业大潮,以技术为驱动的创业机会越来越多。”
“我们希望环信能以身作则,为开源世界贡献一些有价值的代码,并且我们还在做自己的社区,尽量以开源的形式将一些好的技术反馈给社区,通过分享带来更广泛的进步,这是我们一直的愿景。现在回过头来看,当6万个App用我们的技术和产品,有3亿部手机用我们的服务每天发几亿条消息的时候,某种程度上我真的觉得我们帮助了创业者、改造了中国社会,还是比较有成就感的。”
一个企业的核心价值在于通过朴素的技术壁垒坚守足以改变世界的独特价值,无可替代
优秀的SaaS公司一定是全球化公司,未来中国将向全球输出数字商品。
环信对标的对象是微软、甲骨文这类国际性公司。中国作为制造大国,曾一度沦为全球制造和外包中心,以向全球输出工业制造品为主。近年来随着科技的发展,中国在互联网领域逐步与国际接轨甚至在某些细分领域遥遥领先,这样的转变无疑将带来中国输出能力的质的提升。现在猎豹、大疆等公司已经向海外输出移动APP工具、智能硬件等产品,而未来十年中国将会向全球输出数字商品,SaaS作为天然适合全球化的产品,将成为中国向全球输出数字化商品的重要领域。
“一个优秀的SaaS公司一定是全球化的公司。”在刘俊彦看来,这是最基本的理念。 收起阅读 »
云中黑客松
报名时间
2016/04/15 00:00 — 2016/05/15 23:59
活动时间
2016/04/15 00:00 — 2016/05/31 00:00
活动地址
中华人民共和国
报名人数
无限 (27人已报名)
什么是云中黑客松?
2016微软“云中黑客松”即将启程!
此次在线黑客马拉松大赛身披“微软智能云”全新而来,时长一月,高能不断!微软智能云Azure,作为一个集混合云和SaaS服务的云平台,已成为企业发展、行业创新的利器。参加“云中黑客松”大赛,您将近距离感受微软智能云带给你的开源、开放体验,挖掘云上创业的智能、创新潜力!
云资源短缺 ?云技术匮乏?”云上黑客松“将为您一一扫清障碍。加入我们并秀出您的作品,微软助您迅速优化产品、精准定位市场、踏上干霄凌云之旅!
如果您拥有够硬的产品、够高潜质的团队,微软完善的生态服务系统和云生态、“微软创投加速器”将为您提供“人,财,策略,市场拓展”等全方位优质创投服务。
谁可以参加?
独立软件开发商ISV,包括:企业,中小微公司和创业团队
个人开发者只要您对云技术兴趣够浓、对产品信心够足,“云中黑客松”就是您的舞台!
黑客松日程
参赛流程
- 每位选手登录开放黑客松(hacking.kaiyuanshe.cn)平台, 进入“云中黑客松”,点击 “我要报名“;
- 每位选手报名之后,即刻就可在开放黑客松后台选择线上开发环境(Linux or Windows)进行体验或开发;
- 需要申请免费微软智能Azure云账号的团队,请在报名三日之内提交您的开发计划书(模板下载)。步骤:我的团队-> 下载开发计划书模板-> 填写并上传;
- 组委会将联系已提交开发计划书的团队,被通知的团队请进行实名认证,将实名认证的(姓名,联系电话,邮箱)发回给组委会邮箱(a-mali@microsoft.com),符合要求的团队将获得免费微软智能Azure云账号;
- 所有参赛团队或个人请上传你们的作品及其附属文档:项目计划书,功能描述文档, 幻灯片, 视频等, 作品展示部署链接。
奖项设置
所有参赛团队均有机会获得:免费微软智能云Azure试用账号(价值1,500人民币);
微软创投加速器八期现已开放申请,优胜团队将进入招募绿色通道,直接与微软创投加速器团队交流。
可以开发什么?
评审标准
基于微软智能Azure云开发或移植App,服务, SaaS应用等。
大赛不限云上开发主题或方向,尽情展现您的作品和创意。也可参考以下开发场景:
另外, 黑客松平台上将为每位参赛者提供一个云上的Linux或Windows虚拟开发环境, 让您秒级开启云上开发之旅。
- 认知服务(牛津计划 + 更多智能APIs)注册获取免费API
- 大数据
- 开放物联网 获得技术支持
- Docker容器
- 混合云
- 其他
[list=1]
您的项目必须是基于微软智能云Azure开发 评选标准会考虑以下方面:商业, 产品,创新,技术等方面综合考虑 提交物: 前期终期
- 报名完成提交《开发计划书》,初审通过之后会获得免费微软智能Azure云账号
- 必选: 3分钟作品介绍视频
- 必选: 幻灯片
- 必选: 作品部署展示的链接
- 可选: 文档说明
- 可选: 代码 (提供一个代码托管地址)
版权说明
作品版权属于作者,但主办方有权在文章或者PR宣传中使用您的作品。
联系我们
联系电话:021-61885153
联系邮箱:a-mali@microsoft.com
QQ交流群:522180538 (Azure云中黑客松)
收起阅读 »
2.x iOS SDK退出接口的isUnbind是什么意思?
当您的APP进程被杀死的时候,环信是通过APNs机制给您发消息提醒的。
所以当您APP启动的时候,我们会把您的deviceToken传到环信服务器,我们称之为绑定deviceToken。
当您退出登录的时候,不需要再接收APNs了,就需要解除绑定,这个时候,需要您在调用退出函数时,将isUnbind设置为YES。
什么情况可以设置为NO?
1、 如果您当前的账号在其他设备登陆了,在它登陆的时候,就会把它的deviceToken绑定。所以这个时候,您不需要解绑,可以传NO。
2、如果您是立刻要登陆新号的时候。如果您退出后立刻要登陆新的账号,可以传NO,因为你在登陆新账号的时候,环信会自动帮您结束之前的绑定关系。 收起阅读 »
环信直播课堂第九期--2.xSDK项目平滑升级3.xSDK
持续时间:半小时
描述:环信发布3.xSDK已经有一段时间了,之前使用的2.x,该不该升级3.X,又该如何升级,其中会遇到什么问题,有什么需要注意的地方?
本期环信直播课堂将由环信IOS工程师shenc给大家详细讲解2.xSDK项目平滑升级3.xSDK
直播观看地址: http://www.imgeek.org/video/15
视频回放稍后会上传到视频模板http://www.imgeek.org/video/
喜欢环信直播课堂,每周四下午三点老地方我们不见不散http://www.imgeek.org/video/15 收起阅读 »
iOS集成环信3.0总结
下面说正事 :
一般用这些SDK , 第一件事就是看文档 , 但是因为环信框架大 , 文档里面说的不会太详细, 我按照文档里面的集成方法试了几次都不行 , 所以我们可以直接参考Demo ,首先看看效果:我这里展示的只是会话和加好友,联系人功能哦
Demo运行
我下载的是IM2.x,里面包含2.0Demo , 3.0的Demo , 单独的EaseUI等等(还有红包功能哦)
IM2.x
第一步:导入所需文件(因为文件里面有的会包括依赖库,所以我们先导入文件)
1.导入SDK
我是参照IM2.x里面的3.0Demo来导文件的 , 所以我导入的SDK是EaseMobSDK(直接把整个SDK拉进去) ,而(IM3.x)的SDK是libHyphenateSDK
注意:3.0Demo里面的EaseMobSDK的lib这个文件夹有两个 , 一个是libEaseMobClientSDK.a(语音) 一个是libEaseMobClientSDKLite.a(无语音)这两个同时存在的话会有冲突的,将你不需要的一个删了就行了 ,
我用的是带语音的libEaseMobClientSDK.a
拉完SDK之后要在Building Settilngs 里面的 Otherlinking 里面设置一下,如果你选之前选择了libEaseMobClientSDK.a , 就输入-ObjC ,此 -ObjC是配合libEaseMobClientSDK.a使用的,如果你之前选择了libEaseMobClientSDKLite.a, 就输入-force_load ,-force_load加静态库路径是配合libEaseMobClientSDKLite.a使用的
设置
详细参考官方文档:
2.导入EaseUI文件
拖入EasyUI工程下的EaseUI文件夹、EaseUIResource里面的Resource文件夹、export文件夹里面的resources文件下的EaseUIResource.bundle
3.导入 CahtDemo-UI3.0下的文件
注意:Class里面包含有AppDelegate文件,可以选择删除自己的AppDelegate文件 ,亦可以把里面的你需要代码复制到你的AppDelegate中
第一步所有的文件已导入了 , 如果你手痒可以试着运行下 ,但是肯定会报各种各样的错
第二步: 我们需要一个PCH预编译头文件来全局引用某些文件
你可以新建一个pch文件,确保路径正确,在pch文件里面添加EaseUI-Prefix.pch(里面导入的文件如果和ChatDemo-UI3.0-Prefix.pch导入的重复了,可以直接删了这个文件)、ChatDemo-UI3.0-Prefix.pch这两个文件里面的代码, 如果你项目中不需要别的pch文件,你可以直接导入ChatDemo-UI3.0-Prefix.pch的路径就行了(因为这是一个Demo ,所以我是直接导入ChatDemo-UI3.0-Prefix.pch)
pch文件导入的路径最好加上$(SRCROOT) , 在Building Settilngs 里面的 prefix header 里面设置, 详细的自行百度
设置pch文件的路径
第三步,导入依赖库
现在开始导入依赖库 , 这是你会发现Linker Frameworks andLibraries里面已经有几个依赖库了 , 检查一下 , 把重复的先删了 ,如果里面同时发现了libEaseMobClientSDK.a和 EaseMobClientSDKLite.a ,就说明你第一步的步骤没完成 ,正常来说里面只有其中一个.a文件
一开始 , 我按照3.0文档的依赖库添加 , 后来报这个错误
报错
后来我核对了一下 , 发现跟文档中导入的依赖库一样啊 , 后来我就去骚扰他们的客服 ,他让我再加一个依赖库libiconv.tbd(但是官方文档根本没有提到要加入这个库,坑!)
我是直接参照Demo里面添加的依赖库添加的 , 这样比较安全一点 ,很多错误都是由于少导入某一两个依赖库造成的 , 再次检查需要耗时间 , 所以我直接全部添加
如果你遇到这个报错 , 里面带有parse twitter字眼的 ,大多数都是缺少导入parse的依赖库:
StoreKit.framework
Bolts.framework
Parse.framework
Accounts.framework
Social.framework
步骤已经说完了, 如果现在运行当然还是会遇到很多报错.
现在主要来总结一下报错原因:
如果报的错是 linkercommand failed with exit code 1 (use -v to seeinvocation)
不能只看红色报错的哪一行 , 要看看上面的内容有什么比较明显的字眼
1. 如果有报错说的是关于BackupViewController的 ,直接把文件删除就好,这个文件是没用的
1
2.如果看到报错上面开头是duplicate的 , 说明你重复导入了某些文件 ,再看看字眼里面如果有VoiceConvert, MBProgressHUD , 在全局搜索里面搜出来, 把其中的一个删掉就行了 , 一般来说我是删除EaseUI里面的VoiceConvert文件夹和三方里面的MBProgressHUD
2
3.如果你项目里面用到MJRefresh , SDWebImage什么的起冲突了 是因为环信里面也有用到这些三方 ,删掉环信的就行了
本篇集成文章由环信热心用户Scorpion_ZJ 发表在个人博客,博客地址Scorpion_ZJ 收起阅读 »
从产品上网到服务上网 ——传统商业转型电商的客服升级之路
从产品上网到服务上网
传统行业触网电子商务,要解决的第一个问题,便是平台搭建。对垂直类电商而言,贯通上下游的供应链管理是运营重点,而天猫、京东这类以流量运营为主的综合电商平台显然不适合。因此自己搭建独立网店或交易平台,成为垂直类电商的首选。目前,国内提供电商平台解决方案的服务商已为数不少。例如,APP端有有赞、微店、微盟萌店、微猫,PC端有Shopex、ECShop、Shop++、Javashop、千米、筑云等等,这些厂商的模块化解决方案帮助传统行业快速实现了产品上网和在线交易,完成了“触网”的第一步。
但我们看到,很多垂直电商虽然实现了“产品”上网,“服务”却迟迟没有上网。由于存在技术门槛,当前电商平台解决方案中,均没有集成客服功能,这导致很多垂直电商仍延用传统的客服方式。即使是规模较大的垂直电商,客户服务的渠道最多也就是营销QQ再加上400电话,这与电商的商业理念背道而驰。效率是电子商务对传统商业的重要革命,电商的本质即在于通过信息流动实现对供需关系的高效率匹配。电商的服务也应当像产品一样,进行效率革命。这也是“SaaS客服”、“全媒体服务”在互联网经济中蓬勃发展的重要原因。传统行业触网垂直电商,继产品上网之后,服务上网将是迈出的第二步。本文围绕服务效率和用户体验,为转型电子商务的传统行业商家介绍移动互联网下的客服形态——全媒体客服。
什么是全媒体客服
全媒体客服是指通过SaaS服务平台,客服人员可同时为来自各种渠道的用户提供实时和一致的服务,服务渠道包括APP、微信、微博、Web,以及传统呼叫中心等。用户和客服之间采用文字、图片、音视频等多媒体消息或是电话语音进行交互。通过覆盖多种渠道媒体,全媒体服务的理念是“用户在哪,服务在哪”。和传统客服相比,全媒体客服的核心价值在于能够大幅提高商家的服务效率,并且实现服务式营销,同时也为商家的客户带来了更好的体验。
从部署角度来看,基于云端SaaS平台的全媒体客服是一种极轻量部署模式。用户无需采购任何专用硬件设备,只需要普通PC,通过浏览器即可提供专业客户服务。实现的方式也极其简单,对于APP,只需要集成厂商提供的SDK,对于网页,微信或微博,更是一行代码即可开启全媒体客服。
环信是国内最早进军全媒体客服的厂商,根据易观智库的市场数据,在移动端SaaS客服市场,环信市场占有率高达77.4%(数据来源:易观智库《2015中国SaaS客服市场专题研究报告》)。本文即是基于市场发展趋势和环信的技术研究,介绍全媒体客服在提升效率和用户体验上的一些优势和关键技术。
1:1 vs 1:N,集约化客户服务
传统企业的客户服务通常以呼叫中心为主。用户需要服务时,拨打企业400/800电话,再由客服人员提供服务。由于一名客服同一时间只能服务于一名客户,因此这是一种1:1独占式服务方式。转型电商后,客户来的渠道多样化,有从网页端来的,有从APP端来的,或是从社交媒体,如微博、微信来的。这时候显然无法按呼叫中心1:1的配置方式,为不同渠道的客户提供单独服务。全媒体客服则是通过云端系统,打通不同渠道,对所有渠道来的用户进行统一排队和会话分配。每一位客服,都可同时服务于多个渠道来的客户,这是一种1:N式的集约化服务。在环信的案例——学而思的客户服务中心,每位客服人员最多时可同时服务20余名客户。在当前人口红利逐渐消失,人力成本急剧上升之时,任何对人力资源的高效率、集约化运用,都是在为企业创造效益。
从IVR到ITR,可触摸的服务
Gartner在3月份发布的《2020年前用户关系中心应关注的5项技术》中,把ITR(Interactive Touch Response,交互式触摸响应)技术列为关键技术之一。ITR是移动互联网时代,触摸化的IVR。客户不再需要听完所有的服务菜单,只需要在手机屏幕上,直接选择需要的服务内容,即可获得服务。例如,神州专车在给司机端提供的“微客服”服务,司机直接在手机屏幕上选择需要的服务内容,不需要打电话听完漫长的语音提示,再选择服务内容联系相应的客服。通过移动客服和ITR技术,整个服务过程时间节约了80%,既提升了用户体验,同时也降低了客服压力。
自助式服务,让用户自己解决问题
对于一些形式标准化,内容单一化的服务,可以通过自助式服务的方式,让用户自己解决问题。例如密码更改、订单管理、物流跟踪等等。自助式服务满足了用户碎片化的使用习惯。用户可以在车上闲暇时间,或是不方便电话沟通的场合即可完成服务,同时也降低了人工客服的需求量。
提供自助式服务,需要企业将IT系统前置,通过一些开放式接口,让用户在商城应用程序的客户端中,直接调用相应接口,完成服务过程。这是和传统呼叫中心封闭式系统部署的不同之处。
随着智能技术的成熟,在开放接口上还可进一步增加智能客服插件。客户通过自然语言,例如“我想修改订单送达地址”,经过自然语言处理技术进行语义分析后,客服机器人返回订单修改入口,用户完成自服务。这个过程,结合人机融合,可以提供无差错的高质量服务。
人机融合,用智能提高服务效率
虽然当前机器学习算法飞速发展,在客服领域也得到了广泛应用。但在自然语言处理,特别是中文语义的识别上,距离完全替代客服还有差距。用人机配合的方式,对于复杂问题,借助智能客服答复,辅以人工核验,既能提高效率,同时又保证了用户体验。
环信在坐席工作台界面中,增加了一个推荐答案窗口。当用户发出客服请求时,智能机器人在推荐窗口提供了若干个备选答案。坐席人员从中选择一个最合适的答案,只需要点击“发送”,用户立刻就可得到回复。如果需要修改,也只需要在推荐答案的基础上经过少量编辑即可。通过人机配合,在保证准确度的情况下,大幅提高了坐席的工作效率。
服务式营销,从成本中心到利润中心
在传统行业,客户服务中心通常是一个成本中心,以解决售后问题为主,难以和营销挂钩。以信息流动为基础的电子商务则为服务式营销提供了机会,客服人员可以接触到客户资料、历史购买商品以及消费习惯等关键信息。但要实现营销,关键之处在于如何利用这些信息。
服务式营销的关键技术之一是大数据分析能力,通过数据挖掘找到热门商品和潜在客户的最佳匹配关系。为此环信在全媒体客服产品提供了客户标签和热词分析功能。通过客户购买历史纪录、访问轨迹、基础资料等信息为客户打上标签,依靠热词分析及时发现热门商品,两者之间的匹配程度越高,商品销售的成功率越高。
服务式营销的另一关键技术是无干扰的消息推送技术。电话外呼之所以日渐淘汰,除了缺乏精准性外,对用户干扰大、体验差也是重要原因。环信全媒体客服采用基于长连接的消息通知方式。客服人员的营销信息,以后台通知的方式发送到用户端。用户可以在闲暇时间打开,这种方式给客户充分的主动权,不会对客户体验带来影响。此外,长连接技术的另一优势是不会丢失消息。通过大数据分析和无干扰的消息推送,客服能前置到销售链前端,实施主动的服务式营销,从成本中心转变为利润中心。
传统呼叫中心的升级之路
转型电商的传统行业商家升级至全媒体客服,根据不同情况有两种可行解决方案。对于当前没有大规模呼叫中心的中小企业,可以采用直接切换的方式,一步到位,用全媒体客服替代传统电话客服。这种方案成本低廉,简单易行。
对于已经建设了大规模呼叫中心的大型企业,以增量部署的方式,使全媒体客服嵌入呼叫中心,保护用户既有投资。这种方案通过开放式接口,可以实现“三统一”:
- 统一排队与路由分配。不论何种渠道来的客户,统一进行排队,共享客服资源;
- 统一话单记录。同一个客户,不论从哪种渠道访问,话单记录都只有一份,客服人员可查看完整历史会话信息;
- 统一后台数据。通过开放式接口,可以对接现有CRM、工单系统、知识库,打通后台数据,消除数据壁垒。
更加高效、更好体验、服务营销,这是全媒体客服对于传统呼叫中心的核心竞争力。转型全媒体客服,是传统呼叫中心的必然趋势。在这个过程中,既需要传统行业商家转变思路,拥抱移动互联网,更需要行业生态链的共建共赢。
收起阅读 »
【环信编程大赛优秀开源项目展示】公众号聊天助手--未认证的公众号与订阅用户之间的便捷联系
项目功能:
个人自媒体公众号越来越多,然而由于个人公众号目前尚无法认证,没有客服接口权限,公众号主难以及时回复订阅用户的消息。通过这款公众号聊天助手,可以绕过微信官方接口,实现未认证的公众号与订阅用户之间的便捷联系。
技术原理:
通过对微信网页后台进行抓包分析,获取并破解了微信网页后台进行回复的接口。后端采用Python+tornado+requests开发。
消息流程:
1.公众号收到用户消息
2.微信服务器通过回调通知聊天助手服务器,聊天助手服务器保存用户open_id
3.聊天助手将消息转发至环信IM云
4.公众号主人通过聊天助手收到消息
5.公众号主进行回复
6.助手服务器通过为订阅用户注册聊天账号并模拟登陆,通过轮询向环信拉取聊天信息(也可以通过收费的即时消息回调接口)
7.将拉取到的聊天信息通过抓包分析得到的网页接口进行回复。
心得:
1.环信sdk是业界较为成熟的IM解决方案
2.该工具在完善后会作为公益工具免费供外界使用,并注明Powered by EaseMob
该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
git源码下载:https://github.com/sunnylife/WechatMaster-backend
APK下载体验↓↓↓ 收起阅读 »
首届环信编程大赛颁奖典礼奖品
特别感谢以下企业的大力支持:
义创空间提供颁奖场地
萌岛从自有形象库中授权一套价值12000元的表情包
Emokit赞助Apple Watch一台
猿圈全程提供技术评测
本次环信编程大赛分现金和实物奖励,现金奖励共15000元,具体如下
一等奖8000元+Apple Watch/1+荣誉水晶杯/1+限量版瑞士军刀背包一个+价值12000元专属表情包
二等奖5000元+限量版瑞士军刀背包/1+荣誉水晶杯
三等奖2000+限量版瑞士军刀背包/1+荣誉水晶杯
决赛前十颁发荣誉水晶杯一支+限量版瑞士军刀背包一个
前五十可获得定制版精美T-shirt或卫衣
来到颁奖典礼现场均可获得环信定制文件袋+多功能便携工具卡,现场还会随机抽取赠送由环信CEO签名的编程书籍
颁奖典礼详情http://www.easemob.com/event/hackathon_party/ 收起阅读 »
【环信编程大赛优秀开源项目展示】Cloud Developer--程序猿之间互相交流学习的平台
项目简介
起初设想将此应用做成专门用于程序猿之间互相交流学习的一个平台,但是由 工作原因,没有充足的时间来投入到此次比赛中。只完成了部分功能。因为是个人开发, 没有美工和UI的配置,界面相对简陋,并且部分数据例如用户头像等采用随机数生成, 一些数据保存在本地UserDefault中,并且使用了环信内部的好友系统。工程主界面大部 分采用Storyboard完成,并且完成了界面适配,架构采用 MVVM 模式,结 合ReactiveCocoa来达到模块间的充分解耦。
0x02使用到的第三方类库
本工程没有使用到CocoaPod,所有用到的类库都位于工程中的Vendor文件夹
1. ReactiveCocoa 2. DZNEnptyDataSet 3. IQKeyBoardManager 4. SVProgressHUD
0x04其它扩展功能有
3D Touch (手机桌面) Apple Pay(开通会员)
该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
源码下载↓↓↓
git源码地址https://github.com/FinderTiwk/CloudDeveloper 收起阅读 »
【环信编程大赛优秀开源项目展示】宅男福利--美女直播应用
项目介绍
本项目属于个人娱乐项目,做项目时主要想用环信的视频直播聊天,就临时改成和美女聊天的功能。目前只想到这些功能
1、浏览各种类型美女,单击放大和玩逗。(目前只实现放大查看)
2、点击右下角美女的头像,进入和美女聊天玩逗,主要用了环信的小助手功能。
项目用到的技术:
1、用Kotlin和java混合编写。
2、用了安卓最新效果(Fab,Snake,Recycler,CardView等)
3、图片加载采用Glide
4、网络加载采用Retrofit
5、Activity和Fragment 的封装
总结:
由于最近只有晚上回来写项目,平时公司项目比较忙,还有很多想到还没有实现,大体框架实现了,具体功能只实现部分。后续完善。期待环信出很多有意思和好玩的功能。
该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
源码下载↓↓↓
源码git地址https://github.com/xusoku/EMDemo 收起阅读 »
【环信编程大赛优秀开源项目展示】薅羊毛技术社区--针对于技术人员的社交软件
项目简介
本app主要针对于技术人员的社交软件,技术开发者可以分享自己的文章,可以和其他技术人员聊天,平台也可以发一些文章。平时比较忙,偶尔有些功夫写一写,也对自己业余生活的一个补充,正好最近刚刚开发了一套新框架,顺便拿来使用。本app完全建立在服务端上。服务端提供数据和支持。
-技术点
客户端技术点
1.系统中所有图标均采用字体图标(dileber框架中写的一套字体图标)
2.图片采用.9图
3.集成环信sdk,可以和服务器上的用户交流
4.架构基于dileber(来源于DrCoSu工作室的开源mvp框架
https://github.com/dileber/dileber(我个人开发的一套框架)mvp架构,代码清晰,代码简洁,层次分明
5.json数据传输与解析
6.框架自动生成(整个项目是采用一套配置文件生成的一套架构)
7.本来想在项目资讯页面写一个瀑布流布局,貌似只有某些手机支持,不成功。
8.采用下拉刷新
9.自动登录
服务器端技术点
1.linux服务器,真实数据运行,api接口传输数据。
2.服务器采用java web 架构为 mybatis+spring mvc+nginx
3.数据均采用json包装
4.数据库采用mysql
该项目为环信编程大赛参赛项目。报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的技术大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
git源码下载https://github.com/dileber/technology_community 收起阅读 »
【环信编程大赛优秀开源项目展示】方圆十里--关注方圆十里内的人和事
项目简介:
基于IM(环信通信云)+LBS(百度地图SDK+GeoHash距离算法)的社交APP
一、功能列表:
1、登录、注册(采用后端授权注册的方式绑定环信id,更安全)
2、用户资料:头像、昵称、性别、生日、地区(本地arrays.xml存储地区数据库)、个性签名
3、Tab1.-“人”---- 百度地图中显示十公里内的人(如果用户位置集中或者人数过少会导致无法测试,因此“更远”选项,搜寻更多,并且本身的十公里概念也未进行筛选)
4、Tab2.“事”----十公里内的动态--文字、图片、位置的动态(发布、回帖、通知提醒)
5、Tab3.“聊”----IM会话
6、Tab4. “友”---通讯录及好友申请
7、我的动态
8、二维码---通过扫二维码加好友
二、特点备注:
1、本项目中采用geohash算法编码用户的位置坐标,达到位置的粗分区,而后进行精确精算实现十公里之内的人和事的概念。
2、创建自定义的百度地图标注。
3、一套将环信IM系统和开发者自身的用户体系融合的解决方案(当前的解决方案在多个项目中得到检验,成熟稳定)。
4、常见的发帖回复模块的处理解决。
5、利用环信的透传消息进行用户的帖子发布、回复等提醒通知。(待更新)
6、UI优化,如电话聊天背景的取自用户头像的毛玻璃特效--参考微信电话聊天背景(待更新)
该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
git源码下载https://github.com/huangfangyi/fangyuanshili 收起阅读 »
【环信编程大赛优秀开源项目展示】文播--一款文字直播平台性的APP
功能:
本项目是一款基于环信sdk进行个性化改造的文字直播平台性的安卓app。
在参赛报名的时候,曾想过这样一个问题:一款完全为IM而生的sdk,到底能有如何的潜力?因此,另辟蹊径将环信提供的IM群聊功能,通过重新设计,改造成了现在的文字直播的平台类型app。
每个直播间,其实就是一个“只有群创建者才能发言”的IM群组或讨论组,再进行一些界面上的改造,就可以实现一款类似于从早期非智能机时代流行至今的纯文字直播的app。
典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在《文播》里找到对应的频道。
提交的该版本目前为纯游客端,主播端另行实现。
技术:
·客户端使用DrCoSu工作室开源的dileber框架,MVP设计模式,整个项目冗余较低。
·融合环信SDK,并进行了个性化的改造。
·采用.9格式存储图片,ttf方式呈现界面与图标,各个机型兼容性较好。
·服务端采用Java(Spring),配合ngix和redis极大提升了访问响应速度。
·采用http通信和json、xml等数据格式,移植性和通用性好。
心得
重复造轮子虽然好,但是在实际开发中,往往可以使用更好的方式来加快你的节奏,从中获得更大的成就感。
环信SDK在即时通讯云领域是一款足够优秀的SDK。配合JPush和好的创意,能实现无限多的可能性。
创意是一款新型软件的核心竞争力。
介绍
文字的直播,一样精彩。
该项目为环信编程大赛参赛项目,报名参加颁奖典礼,这里有一群有时间,熟悉环信集成,开源项目的大牛,还有数十家环信企业级服务器小伙伴和金牌投资人,报名连接http://www.easemob.com/event/hackathon_party/
项目源码下载 ↓↓↓
收起阅读 »