如何找到好的产品?(下)


如何找到好的产品?(下)
周鸿祎

“如何找到好的产品?它必须满足三个条件:刚需、痛点、高频。”

无论是行业大佬,还是一个小的创业者,你听完了我讲的需要做的只有一件事,就是怎么找到一个产品,利用这个产品去吸引用户。

如何让你的产品可以积累用户,可以黏住用户和用户连接,其中最重要的就是体验,但是用户体验是第二步,最重要的就是你要找到一个用户的刚需和痛点,这是我反复强调的。

就像我刚才说,我们内部有人做了电子捕鼠器,配置都很高端,唯一需要的就是把老鼠抓了放进去,还有你们很多人看到的现在各种智能硬件,摸起来光滑,但是忽略了一个前提,我不想买这个东西,因为没有需求。没有需求解决的不是痛点,就是伪需求。做产品最大的限制不是体验不好,体验不好可以改,最大的问题就是伪需求。

还有一些产品有没有需求呢?有,但是没有它,用户也可以过,所以我管它叫痒点。眼中钉、肉中刺,你们按照这个标准对,任何伟大的战略都是从这么一个小点开始切入。有人老吹牛说想战略,我告诉你战略就是找到这个用户的痛点和刚需,然后做一个产品解决它。至于解决的好和更好,这是体验的问题。

理论上如果说用今天我们手机座机行业的文案标准,那个捕鼠器绝对是温文尔雅,豪迈大方。用了403不锈钢做的铁门,杀老鼠的刀片和吉利刀片利用一样的大马士革钢,上了六遍漆,让老鼠很容易摔倒,我们可以做一个PPT,我专门开一场发布会,为一个捕鼠器讲两个小时。而且我还告诉你,我可以对付美国大仓鼠,欧洲的老鼠,而且支持蓝牙。我做了一个手机App,如果说抓住了老虎会把视频发给你。但是问题还不是你需要抓个老鼠放进去,而是,大哥还是买猫吧。

以前做产品我老忽略了本质,所以我谈了很多如何改善体验的方法,今天我要回归本原,想出一个主意以后要问自己是不是刚需,是不是痛点。还有一些人自己想象了一个场景,这个场景在生活中很少发生。上门修锁是不是刚需?是刚需,是不是痛点?绝对痛,但是它的频度太低。这种情况下,你这种产品就很难替你凝聚用户。

其实对很多的大企业,我都说你要忘掉原来的商业模式,你要忘掉你原来丰富的产品线。所有的战略都要归结成从用户角度出发寻找一个需求,一定是中等以上的频度,痛点、刚需。这个服务可以非常不起眼,但是它一定是对用户有价值的。

我再举两个例子,今天小米手机做的相当成功,小米现在什么都卖了。很多人谈做手机必谈生态链,这个错误在哪里?你们买手机是因为这个吗,不是,你还是冲着配置、颜值、性价比。所以小米最早做起来的原因才是它真正破局的点,一旦它破了这个局,它后续的东西都是一种很自然延续的发展。

什么是痛点?想用双核,所有的双核手机苹果卖六千,三星卖四千五,国产的卖三千,小米1999,这就是痛点。第一代的小米手机有设计吗?雷总说没有设计就是最好的设计,所以人家拼的不是颜值和工艺。你们分析很多公司的时候,不要分析它走到今天成功是因为什么,一定要看他们第一个产品是怎么做的,这种产品的分析才有价值。

微信今天是一个特别伟大的产品,但是你们看看张小龙曾经的一篇自述,他讲了微信最早做出来,当时也找了很多的点但是都没有突破,它的突破点是什么呢?摇一摇,那是他们快速获得第一批忠诚用户的点,这是刚需,高频。一个婚介公司是世纪佳缘,但是不是太大,因为你只能找一个,找到了就不能再去了,低频。

咱们接着讲facebook,facebook做到后来产品经过不断的演化。刚开始的时候,你的产品不要试图解决所有人的问题,你可能只解决一部分人,你就解决一个最大的问题。消费者选择你的产品是特别简单的,我拿你干什么,给我创造什么价值。回答这个问题,你的公司就成功一半,剩下的就是执行力的问题,持续改进的问题。

很多人写商业计划书的时候容易犯一个错误,老喜欢重论革命形势,或者就是重论各种概念,计划书里说了半天O2O,SNS,闭环,流程,这些话如果你的用户听不懂,投资人也不会投资你。你把他当成一个用户,这个产品面对什么人解决他们什么问题。

比如说我们很多的产品经理,他在日常生活中是一个优秀的用户,到一个餐馆吃饭不满意一定会拍桌子,下载了一个App五秒钟不会用,你一定会卸载,但是自己做产品的时候,突然就改变了,觉得要教育用户,觉得我的产品就是这么设计,就是这个流程。我说什么年代,当年的PC年代软件难用,电脑是专业活,如何21天学会用XXX软件的书卖出好多本。现在,还希望用户到中关村创业大街买一本叫21天学会利用周鸿祎做的智能捕鼠器?笑话。好的产品要有一个特征,要特别的简单。

不要小看这个,很多人发短信说老周,我做了一个可以颠覆腾讯的东西,我一般礼节性的回一下,谢谢。第二条短信说你想知道吗,我说在这儿发两句话吧,他说两句话说不清楚,我得见你,给我两个小时。我一般不再回了,很简单的,再牛掰的产品,你如果说利用两三句话说不明白,你的用户不会为他买单。

今年BAT再难用的产品媒体都关注,你没有这样的机会,你在面对第一批用户的时候,实际上只有两三句话的机会,让用户听了你的话就觉得我想用这个东西。如何让人对你的产品一见钟情,再谈我怎么在产品里做细节和交互体验的提升。超出用户的预期,让用户尖叫,让用户觉得很惊讶,让用户疯狂,让用户变成你的粉丝,这是日久生情。

很多人学互联网模式学歪了,他们学到了很多表象,上门给你送一只鸡,利用大奔找几个美少女上门给你送。他们觉得这就是体验,是惊喜。但是有一个前提,我是不是每天都要吃鸡,这是不是我的痛点和刚需以及高频。体验没有产品重要。

我刚才说的捕鼠器,大家嘲笑需要把老鼠塞进去,这都不是笑话,那是用户参与感好不好。真正的问题是因为现在大家家里都没有老鼠,为什么你要买捕鼠器呢?需求就不存在。你们想想有多少智能硬件的发布会都是跳过伪需求,假定说用户热爱我的产品,用户会每天都用,然后谈你的材质和工艺,谈你的外形,这都是错的。

不是免费,是倒贴钱

刚才讲了半天产品,我顺道多讲一点商业模式问题,免费不是一个忽悠人的模式,免费是互联网里非常重要的一个建立用户手段。免费商业模式里面最重要的一句话就是羊毛出在猪身上,我非常相信这句话。今天免费不仅大行其道,现在大家倒贴钱。

原来我们产品跟市场传播是分开的,经常有人做了一个产品再给市场,市场设计卖点然后炒作,这是不对的。我认为今天做市场的人自己至少要变成半个产品经理,要和产品经理一起想。今天好的做传播的人,他一定了解用户的想法,所以一定可以成为优秀的产品经理。

反过来,今天任何一个产品经理都得学会在朋友圈里卖面膜,都得学会发微信,发微博,直接跟用户对话。如果说他不能把他产品的想法让用户简单的理解和传播共鸣,他做出来的产品一定很难用。所以以后我觉得公司的架构可能会有一个产品小组,产品小组里像踢球一样,会有分工,有前场,有后卫、有中锋,但是每一个人的使命都可以上前踢球射门,也就是说每个人都可以成为产品经理。

好的产品的最大心得――小白模式

做好的产品,我给大家分享我自己最大的心得“小白模式”,心理学的话来说就是同理心,你可以设身处地从用户的角度想,这是最重要的。我们每个人都很自大,我们只能顺应用户的习惯。一般刚出来的产品很难强求用户,文案是这样,产品也是这样。看过很多产品的交互,我觉得所有错误在内部骂产品,都是它们不能从用户角度出发。

我的路由器做的很成功,为什么?我终于领悟到做硬件和做软件有一个差别。软件基本都免费,下载无成本,软件难看好看都是图标文字。如果用户用起来觉得功能不错,又是他的刚需,有的时候体验差一点用户可以容忍。你反正软件可以升级,实在不行了用户可以卸载,所以很多人做软件的人第一次做硬件,他们都忽略了这一点。

因为硬件你用户买回家就基本上很难再改动,所以用户要真金白银的买一个硬件,颜值特别重要。我应该找四种人,一种人就是设计师,就是在做硬件的产业设计师特别重要。你的颜值,你的工艺完全决定了用户是不是会一见钟情。我举一个例子,最近观察到用苹果笔记本的人越来越多,苹果笔记本哪一点做的好?很多人买回家也是装了一个WindowsXP在用,那个颜值就是比较好。

当时我们做路由器的时候,他们第一版的设计做了像鹅卵石,设计者本人觉得像鹅卵石,我看起来像肥皂盒。这里分享一个经验,我的经验是跟小米学的,在拿不定设计意见的时候,我们还是看看苹果怎么做的。

但是要跨界,比如说你可以照着苹果笔记本设计一个路由器,你可以照着苹果路由器设计一个手机。世界上有一些产品人家有很好的设计,刚开始还是要学习的,所以我新版的路由器照着苹果笔记本的样子,利用同样的铝合金外壳,感觉颜值很高。因为用户的心理是如果说一个卖一百块钱的东西看着一定要像五百块钱,注意啊,这也是很多互联网的硬件前辈的真谛。看着要逼格要高。

做路由器时,我和他们讲,要尽可能把价格做低。你觉得路由器超过一百你会买吗?大部分人不会买,我们按照89元的定价,花相应的成本在外观和用户感受上。

所以刚才我们谈到了体验为王,硬件给别人的体验是需要让用户可以感知到,感知不到的体验你说完了用户就忘掉。你给用户讲双频,为什么很多用户感受不到双频。特别简单,很多人的设备不支持双频,5G的信号好,但是遇到墙就弱了。所以尽管你不断的宣传5G是未来的标准,但是实际上很多人没有感知。

而且这里我都犯了一个错误,我原来特别想强调路由器的辐射问题,有没有辐射?我真的不知道,因为感知不到。但是我们打安全健康牌,做了一个低辐射模式,把信号调到最低。这个功能受到少数孕妇欢迎,结果我拿了一台路由器回家,怎么没有信号呢?这时候我非常的愤怒,这个时候已经不是理性的周鸿祎,作为一个感性小白用户的周鸿祎,我的路由器怎么没有信号,信号怎么这么弱呢?我早就把辐射问题扔在九霄云外。所以第二版路由器我说信号一定要强。

我们把这个路由器定名字是大户型路由器,这个也是一个体验的问题,今天起名字一定要直观,为什么叫大户型路由器。第一,什么是大户型,大家觉得可以定义吗?在香港40平米叫400尺也是豪宅。所以你做几十平米的屋子都觉得是大户型可以买。第二,大户型是每个人都向往的目标,所以你买不起大户型,可以买得起大户型的路由器。有一个公司东施效颦,他们给路由器起了一个名字别墅型路由器,这个名字过了。因为大部分的人肯定明确的知道自己住的不是别墅,别墅不是今天的幻想。

所以起名字也好,做产品功能定义也好,所有的依据我和我底下的产品经理争论,我都是站在用户的角度说,用户会怎么着。

儿童手表做到第三代,终于找到了痛点

我们在硬件上的争论比软件多。特别是涉及到工艺、材质、外形,有没有屏幕,有没有键盘。最后所有的选择都是来自于用户怎么看,而不是你做出一个产品想创造一个新的门类,妄图教育用户,教育用户不是不可以的,需要有一个漫长的过程,需要足够的广告投入,需要足够强大的市场行销。但是对今天很多的初创公司条件不具备,如果想做一个爆款单品,你想一炮而红,一定不要挑战用户的常识。

我们做儿童手表第三代,前两代我们的产品经理犯很多错误。最早设计的时候,他们希望满足0岁-10岁小孩的需求,大家觉得这个想法对吗?0岁小孩跟10岁小孩胳膊的大小都不一样的,你为了满足所有这些人的需求,最后这个表可以做的很大吗?这个表做的很小,就意味着电池很小,我们最早的电池只有200毫安。意味着你的待机时间只有一天,每天都要充电。

所以这就变成了最致命的问题,为什么今天很多手环大家也戴不下去了。有一次忘了充电,觉得不戴也无所谓,所以我们在儿童手表上电的问题非常困扰,内部争论的非常厉害。你说把电池做到500毫安手表必然就要大,手表大有人说3岁小孩戴不了,我们做到第三版的时候,我们决定手小的孩子我们不管了,我们必须把电池做到500毫安以上,必须待机超过三天。我们通过大电池和软件的优化才解决待机问题。

很多的时候我们发现用户反馈里面每天要充电是一个问题,真的用户很反感每天充电吗?其实不是,为什么?而且手表原来最大的问题就是产品定位的问题,我们当时主打的卖点是什么,孩子防丢防诱拐防走失,这个点对不对?非常好,非常对,在座只要是父母会买,但是问题就是买的人是我的用户吗,不是,用户是孩子。所以孩子的角度这个手表给了他什么价值,有价值吗?对孩子来说是他被迫戴上的,他没有乐趣,就看时间,所以他觉得没什么用,他就没有动力充电,他又不是用户,父母可能又经常忘了。

这就是前两代产品非常尴尬的地方,结果到第三代我们终于解决了这个问题,因为我们原来也是内部争论,所以很多争论我认为都是脱离用户的两方人很自我的争论,得不出好的结论。

其实我们的手表就是一个手机,我们的手表是假定很多的学校不让小孩带手机,太小的孩子不能给他手机,所以说它有电话的功能。但是很多人争论说小孩没有打电话的需求,他们就把这个打电话的功能给屏蔽掉了。我们做到第三版的时候突然发现,安全是一个基础需求,但是安全不是一个体验点。

因为毕竟中国有再多走失小孩的案例,但是对每个人来说遇到的概率是比较低的,很多人至少今天没有遇见过。没有遇见的事情没有发生,他没有体验,父母一周最多定位孩子两次,我们发现很多父母在孩子上学的时候反而不用,因为他知道孩子在学校。这一版我们把打电话的功能恢复了,就是父母可以随时拨一个电话到手表,就像一个真正的电话小孩可以跟父母回答,小孩遇到任何的情况,哪怕就是想爸爸了,想妈妈了,他按一个键可以直接的给爸爸妈妈打电话。

这个功能变成今天第三代儿童手表最主要的核心功能,不仅仅是一个安全的工具,成了一个沟通的工具。所以我们发现才真正把体验为王做出来,因为小孩子有体验,小孩子从我们做的实验来看使用频度和黏性产生了指数级的上升。

即使在新加坡等不会出现小孩诱拐的安全地方,很多的父母也会希望每天能够关心孩子,打电话问情况,因为沟通是基本需求。我们产品经理历经三代,最后绕了一个很大的弯子,重新回到了我说的痛点、刚需、高频和简单。丢孩子是痛点,防丢也绝对是刚需,但是频度太低,通信是刚需,是痛点,又是高频度的应用。

所以今天我们的手表就是三个功能,跟孩子随时保持通信,加上小孩一键呼救,再加上随时定位小孩防走丢。过去带防丢手表和同学在一起没有办法秀的,所以你的产品就不能再产生推荐。

有人置疑,当时为什么不做打电话的功能。这是一个很重要的经验分享,理由就是因为电池小,小孩老打电话会把电耗完了,这个逻辑听着是对的。但是是误入歧途。

现在小学生的电话沟通需求也很强烈。怎么解决待机问题,继续把电池做大吗?很多小孩学会了自己充电,我们给他做一个小的充电宝随身携带。

为什么?因为他有了刚需,他为了这个刚需就开始容忍你的缺点。今天苹果在原来4寸屏幕的时候,它的电永远是不够的,为了美观乔布斯牺牲了电池,所有用苹果的人都必然带着一个充电宝,方便吗?不方便,但是今天我们所有人容忍了不方便都带了一万毫安的充电宝,都养成了习惯。还是因为手机满足了你的刚需和痛点,这种强大的拉力使你可以容忍产品的缺点。

没有完美的产品,你每天做产品经理都是在做不断的选择,其实很多时候选择一定要在用户最痛的点上进行突破,如果用户接受了最痛的点,其它的点都可以容忍,我们不再增加500毫安大电池,再重的话戴在手上就不舒服。用户喜欢这个功能我们会做一个儿童的充电宝,或者说儿童的充电书包,书包里放一块电池,手表和书包一接就可以充电,这不是完美的方案,但是用户可以接受。我们忽略了用户最本质的需求,所以我们做了很多功能都没有打动用户。

如何找到好的产品?(上)


如何找到好的产品?(上)
周鸿祎

我不知道大家想听什么,比如说有两大部分可以讲,高大上的我们讲转型互联网。今天的主题是产品创新,我就讲讲微观的。我在我们的行业里,战略比不上两位马总,我从骨子里本质上还是一个产品经理,可以分享一些做产品的心得。

最近互联网有一个很不好的风气,猫三狗四开始装教主,说我是如何成功的。我经常讲成功是偶然,失败是必然。大家的愚蠢上是有共性的,很多人在失败上会犯同一个错误。当你了解别人的产品是如何失败的,你才能在自己做产品的过程中避开这些暗礁。

+互联网还是互联网+?

大家最近都在谈互联网+,我理解大概有两种用互联网的方法,一种是+互联网,一种是互联网+。这两个有什么不一样?

+互联网:很多人希望把传统行业跟互联网结合,这是一种术,比如说我在互联网开一个店卖东西,在互联网上打广告,甚至是你在互联网上雇水军黑老周,云计算,大数据,这些做法都叫+互联网。因为没有改变某一个行业或者说产品的本质,你只是利用互联网把它改的更加有效率,不会产生爆炸性的指数级的变化。这种做法是传统企业转型互联网最简单的,所以说今天这不在我们的话题之内。

今天热门的互联网+有什么不一样呢,我的理解就是利用互联网的思维,去指导一个产品或者一个行业去改变它的产品体验,看待用户的方式,它和用户的连接方式,改变它的商业模式,从而产生真正的资源重新配置,产生化学反应甚至是核反映的效果。

连接的力量

过去,很多传统大咖看不上互联网,认为是一群毛孩子忽悠国外VC的钱在国内乱烧。行业里有一位我比较尊重的老朋友丁磊,2000年互联网泡沫破碎,他为了给自己和大家打气弄了一个广告,当年我没有看懂。过了10年我终于理解,那个广告道出了互联网的真谛――网聚人的力量。网络之所以牛,因为网络把很多东西连在一起。

大家今天谈一个词“连接”,你要考虑你的产品如何能够真正把很多东西连接在一起。这个东西可以是人,可以是企业,也可以是信息。只有理解了连接,你才会理解为什么很多行业会被颠覆。UBER为什么这么热,因为它改变了连接关系。为什么微信会比QQ牛,因为他真正把人连在一起。

下一个趋势是什么?

有两种思路创新和做产品,一种是站在过去看现在,一种是站在现在看未来。当你创业,当你要创新,你一定要做未来的事情。未来的事情有两种,要不就是别人没有做过的事情,要不就是把别人做过的事情换一种别人想不到的方式去干。所以我认为只有做一件今天大家可能都不看好,但是明天后天有可能起来的事情,你才可能获得巨大的成功。

未来我觉得有两个趋势,一个趋势IOE(Internet of Everything)或者O2O,我觉得更多的就是面对服务业讲的。利用互联网把很多服务业来进行改造,无论今天的上门服务还是打车、定餐等等所有东西这是一大块。

还有一个趋势IOT(Internet of Things),把今天很多的物理器件变成智能设备,把它们和云端连接在一起,意味着今天你所有看到的东西都可以被智能化,无线化,移动化、云端化。我特别不喜欢一个词“物联网”,被很多人庸俗化成一个传感器的网络。

如果说仅仅是一个传感器没有价值,重要的就是每一个设备都是智能的,它采集数据,做出一些智能的判断,再把数据反馈到云端,然后云端汇总成大数据,大数据再产生一些结果再反馈给各个智能设备。所以说今天在我的眼里,在IOT的世界里面所有的东西都是手机。你们想想未来手表是不是手机,眼镜是不是手机。

前几天我看了一个行业大老,我说智能汽车就是四个轮子的苹果,他一听特别的激动,他说我早就这么认为,但是大家不认同。所有的东西你可以认为都是手机,所以说今天做手机也不一定就要造放在耳朵边上的东西,谁说五年以后手机还是这样呢?手机有可能变成这样的。

今天所谓的车联网,当你坐车的时候,你真的需要再把手机打开吗?可能车本身就变成了智能的系统,车有屏幕,车也可以跟你对话。车子里有全套的通信系统,回到家里把手机一放,家里到处都是智能设备,有摄象头,有电视,有音响各种的家电设备。包括你身上穿戴的各种东西,我一直在想能不能做一个可充电的皮带,在皮带里装满了电池。

我们公司的员工创造性特别高,有一个员工做了一个智能捕鼠器,有特别多的智能功能,比如手机可以遥控,可以把老鼠电死,掐死噪音把它震的口鼻出血。但是有一个缺点,就是需要抓老鼠放在捕鼠器,但是对智能设备来说这不是缺点。

前一段时间GE所有的航空发动机里装上一个智能的设备记录发动机的运转数据。同时,会把数据汇总到GE总部,通过大数据告诉航空公司,你的这个发动机有一点问题,跟其它的数据曲线不一样。

这里不仅意味着很多的设备可以智能化,最重要的就是很多硬件产品的用户体验将会被重新改变,商业模式会被改变。换句话说,以后大部分产品,特别是3C产品,除了苹果以外,卖硬件赚钱的机会都会没有。以后的很多设备只是会变成连接。

用户还是客户?

很多企业转型互联网,他们恨不得在一夜之间引刀自宫,结果最后就流血而死。很多人说我们知道,马总给我们讲了连接,我说连你个头,你连接谁?他们就无语了。还有很多的企业说老周我有大数据,我说您那一个硬盘的数据,不叫大数据。最重要的就是转换一个概念,你们原来心中只知道有客户,而不知道有用户。在座的诸位知道用户和客户的差别吗?其实不是简单付不付钱作为代表。

传统行业我和大家讲,产品业务很复杂,商业模式特简单,谁掏钱就是它的客户。它们心中如果只有客户的概念转型不了互联网,为什么?你要建立用户的概念,用户有几个特征。第一,用户不见得向你掏钱。更重要的就是用户要经常性的用你一个什么服务或者产品,连接,交互,这些才是用户的条件。所以做客户容易,做用户难。

如果你有了用户,后续的牌就胡打胡有理。所以用户至上不是一句空话,如果说互联网总结四个字的话我就是用户至上,没有用户的概念怎么连接,所有的连接、大数据等等都是空谈,你就无法建立你的商业模式。

所以很多人一上来就老想说我在互联网里怎么赚钱,我不是不爱钱,我和你们一样的爱钱,但是如果你在互联网上刚开始想怎么赚钱,就想着弄客户,你可能就没有用户。各位想想到底有客户关系还是有用户关系。

很多人讲如何和用户交朋友,如何让用户参与,怎么搞社群,这些都没错。这些方法都很好,但是前提是你得有一个产品或者服务,把用户吸引过来。我举几个例子大家就可以理解用户的价值为什么要高于客户。

滴滴和快的的例子,最早他们做的是打车生意,在打车过程中出租车公司会向滴滴付钱吗?打车的人会向它付钱吗?没有一个人是它的客户,但是它解决了两个问题。打车是不是刚需?打不到车是不是痛点?打车还是比较高频次的业务,解决了一部分用户或者80%的用户高频、刚需和痛点。

跟这些用户建立连接,这些用户和出租车司机原来有连接吗?没有连接。但是现在和打车软件建立连接以后,有了这么多用户,你发现它再下一步往专车走,天下所有小租车公司,或者说有车愿意出租的人,最后都会成为它的客户。前提是因为它连接了很多用户,所以你去看互联网里很多模式到今天的颠覆,都是用户战胜客户。

我们再说互联网电视。今天买了一台电视,买回家以后在历史上和电视厂商还有关系吗?你们家再不换电视,五年之内见不到它。客户价值就是一次性挣了你一笔钱,仅此而已。所以每年对这些企业来说,因为没有连接,所以说当它做了一个新产品以后怎么办?它又得从零做起,又得再打一轮的广告,兄弟们你们的40寸电视过时,我们推出了41寸的电视。周而复始。

所以互联网对电视业的冲击不仅仅是说把电视机加了一个智能设备,最重要的就是电视机以后的销售没有硬件利润。卖电视不再是一个生意,你把电视买回家,服务才刚刚开始。你买电视的决策,取决于里面有没有好玩的游戏,有没有好看的动作片。对传统的电视机厂商是多么大的挑战。你想一下用户和客户的变化。跟传统的行业老板来说,这是一个巨大的变化,不挣钱了。

你可以看到很多企业没有领会到这个本质,还是在做客户关系。只不过在客户关系里加了一些互动,召开客户见面会,这些其实没有改变连接的本质,客户还是觉得你没有给他提供什么持续而有价值的服务。所以我们为什么要从客户转成用户?

道理很简单,因为你只有拥有的用户你才能真正在用户的基础之上,你才能往后走建立粉丝。有了粉丝用户的参与感和用户的社群才能做起来,而且以后的生意,我们说每一个企业都会互联网化,这意味着什么,不仅意味着客户用户化,还意味着会成为一个服务业的企业。

特斯拉最本质的革命不是它前面的那个大Pad电脑,那个操纵特别的不方便。特斯拉改变了车厂和消费者的关系,特斯拉每一个买车的人都是它的客户,这是没有问题。传统车从4S店提走以后,你和车厂还有关系吗?没有。所以你就是客户,但是以后所有的智能汽车就是4个轮子的手机,这个手机时刻和造车公司的服务器连接。

不光有OTA的升级,可能还有各种信息的推送,各种互联网服务。所以将来大家想象一下,每一个造汽车的公司都要变成互联网的导航服务商,可能是一个生活服务指南提供商,或者一个开车时候的音乐电台服务提供商,你会觉得意外吗?一点都不意外,所以这就是连接。

原来我觉得最牛的行业是运营商,为什么说运营商最牛掰呢?因为它又是客户,又是用户,你每一个月交话费买套餐,是不是客户?但是你每天都在打电话发短信、上网,你离不开它的服务,运营商把服务断一分钟你都受不了。你每天都在利用运营商的服务,但是为什么我经常说到微信干掉了运营商呢,你每天在手机里,你大量用的都是微信服务,你和运营商之间的距离越来越远,大家跟运营商之间真的就没有用户关系,还只剩下客户关系。如果以后都像我们设想的那样免费wifi无处不在,你连那个SIM卡都不需要。

所以要通过现象看本质,你就可以理解为什么微信干掉了运营商。将来在这个价值链里面,离用户越近时间越长年度越高的厂商是最有价值的,不然永远是拿利润最微薄的部分。

今天的运营商为什么会出现如此大的变化,因为他们没有搞清楚用户和客户的区别。这两个词一字之差理念非常不一样,所以曾经记得有一段运营商提了一个问题,你的微信收费吗?这是传统的思路,传统思路就是你提供了某一个服务,你就一定要收费,你要收费你就是要把它变成客户。其实马化腾需要收费吗?马化腾现在每年微信投入几十亿,给大家提供免费的通信服务,让你们每个人每天花5个小时在上面,你们都离不开。我想不用都不能,因为你们都用了。有了用户以后互联网的规律是什么呢?胡打胡有理,插根扁担都开花,在上面做什么不好。我几年前演讲的时候就说老马比你老婆还了解你。

Windows搭建Testlink环境

现在团队测试用例是excel管理的,bug追踪用的是mantis,最近准备管理下团队的测试用例,于是打算搭建一下Testlink。

整个搭建过程中,还是遇到几个坑的,最大的原因在于,我以前有一套apache2.2+mysql5.5+php5的环境,结果,下载testlink-1.9.16后发现,这几个环境都太陈旧了,于是全部更新为较新的32位版本。

apache升级到了2.4.27
mysql升级到了5.7.19
php升级到了5.6.31
然后为了这三个程序可以运行,又安装了vc_redist2012、vc_redist2013、dot_net_framework4.0

把testlink-1.9.16解压后,放到apache的www目录下。

1、修改apache配置
httpd.conf

#调整SRVROOT 
Define SRVROOT "C:/Testlink/Apache2.4"

<IfModule dir_module>
    #新增index.php
    DirectoryIndex index.html index.php
</IfModule>

<IfModule mime_module>
    #新增下面一行
    AddType application/x-httpd-php .php .phtml .php3 .php4
</IfModule>

<IfModule alias_module>
    #新增下面一行
    Alias /testlink C:/Testlink/Apache2.4/www/testlink
</IfModule>

#新增一段
<Directory "C:/Testlink/Apache2.4/www/testlink">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

#文件最后新增两行
PHPIniDir "C:/Testlink/PHP"
LoadModule php5_module "C:/Testlink/PHP/php5apache2_4.dll"

2、修改php配置
php.ini

;增加超时时间
session.gc_maxlifetime = 3600
session.cache_expire = 180

;开启以下几个plugin
extension=php_curl.dll
extension=php_gd2.dll
extension=php_mysql.dll
extension=php_mysqli.dll
extension=php_openssl.dll
extension=php_soap.dll

3、新增数据库,字符集utf8

4、新增数据库用户,并设置好权限

5、修改testlink配置
config.inc.php

//默认为中文
$tlCfg->default_language = 'zh_CN';

//日志路径
$tlCfg->log_path = 'C:/Testlink/Apache2.4/logs/';

//上传路径
$g_repositoryPath = 'C:/Testlink/Apache2.4/uploads/';

//SMTP设置
$g_smtp_host        = 'smtp服务器';
$g_tl_admin_email     = '邮箱地址';
$g_from_email         = '邮箱地址'; 
$g_return_path_email  = '邮箱地址';
$g_mail_priority = 5;
$g_phpMailer_method = PHPMAILER_METHOD_SMTP;
$g_smtp_username    = 'smtp用户名';
$g_smtp_password    = 'smtp密码';
$g_smtp_connection_mode = '';
$g_smtp_port = 25;                        
$g_SMTPAutoTLS = false;

6、访问localhost/testlink
选择新安装
查看是否有错误的配置
如果没有则填入数据库相关配置
确认数据库脚本执行成功
安装完毕

7、用admin/admin登录系统

8、删除testlink下的install目录

9、重启PHP

OpenKM6中PDF2SWF中文支持

在使用过程中,发现部分PDF文件是可以正常转换为flash,但部分却只能转换英文,更有甚者直接转换失败。

直接命令行进行转换,发现提示如下:

pdf2swf.exe -f -T 9 -t -s storeallcharacters 1.pdf -o 1.swf 

Error: Unknown character collection 'Adobe-GB1'
Error: Couldn't find 'UniGB-UTF16-H' CMap file for 'Adobe-GB1' collection
Error: Unknown CMap 'UniGB-UTF16-H' for character collection 'Adobe-GB1'
Error: Unknown font tag 'SimSun'
Error: Unknown character collection 'Adobe-GB1'
Error: Couldn't find 'UniGB-UTF16-H' CMap file for 'Adobe-GB1' collection
Error: Unknown CMap 'UniGB-UTF16-H' for character collection 'Adobe-GB1'
Error: Unknown font tag 'SimSun'
...

主要原因是,PDF2SWF中一些字体文件是需要授权的,所以OpenKM6默认是不会带着些文件的。我们配置好就可以使用了:

1、大家可以到网上找这个包xpdf-chinese-simplified。

2、解压到一个路径后,修改包中的add-to-xpdfrc文件,将路径改为正确的路径。

3、用命令行试下,转换是否正常

pdf2swf.exe -f -T 9 -t -s storeallcharacters -s languagedir=X:/OpenKM/extras/xpdf-chinese-simplified/ 1.pdf -o 1.swf 

4、修改OpenKM根目录下openkm.cfg文件中pdf2swf的配置信息

#system.swftools.pdf2swf=X:/OpenKM/Tomcat7/bin/pdf2swf.exe -f -T 9 -t -s storeallcharacters ${fileIn} -o ${fileOut}
system.swftools.pdf2swf=X:/OpenKM/Tomcat7/bin/pdf2swf.exe -f -T 9 -t -s storeallcharacters -s languagedir=X:/OpenKM/extras/xpdf-chinese-simplified/ ${fileIn} -o ${fileOut} 

5、删除flash缓存
X:\OpenKM\tomcat7\repository\cache\swf\*.swf

6、重启OpenKM

看下,是不是已经好了:)

OpenKM6检索功能中文支持

根据OpenKM官方的网站的提示,尝试IK分词的各个版本,都失败了。
没办法,退而求其次,用Lucene自带分词吧。

1、修改OpenKM根目录下openkm.cfg文件,新增下面一行

hibernate.search.analyzer=org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer

2、重启OpenKM
3、到后台,重建Lucene索引
4、现在就可以查询中文了,进行中文搜索时,在前面增加一个”*”号就可以了,比如搜“规范”,就要写“*规范”。

OpenKM6工作流功能浅析

前几天同事和我沟通,希望用工作流类似的功能,对现有的任务进行管理。
说实话,我实在不看好openkm的工作流管理功能(jbpm),但周末还是花了一些时间看了一下。
不看还不知到,看了以后才发现,OpenKM6已经有较长一段时间没有进行框架升级了,还都停留在JDK7早期时代,并且用到了GWt技术。
OpenKM6的工作流,用的是jbpm-3.3.1.GA。看了一下jboss上的jbpm项目,已经进化到jbpm6,好歹开始拥抱drools了吧。
额,扯远了。回到正题。

一、首先是开发环境的搭建,要到这里来下载哦
https://sourceforge.net/projects/openkmportabledev/?source=directory
如果你嫌麻烦,建议就直接解压到指定文件夹就可以用了。

二、打开eclipse,编译jbpm项目
是基于mvn的,有一些jar包,在openkm的仓库里,但下载不到,啥意思吗。自己google咯,还好不算多。

三、编译的时候,建议看下openkm的相关教程,超级简单
https://docs.openkm.com/kcenter/view/wfg/sample-workflow-execution.html

四、介绍一下几个常用节点:
start:工作流的起点,一个流程只允许一个起点
end:工作流的终点,可以有多个,但只要有一个到达终点,流程就结束了
fork、join:就是任务分支与状态同步啦
decision:选择
node:自动处理的任务节点
task node:需要人工干预的任务节点,数据的输入是通过表单form完成的
mail node:自动进行邮件处理的几点
transition:就是两个节点之间的连线

五、构建一个自己的工作流
1、新建一个工作流项目
2、会自动新建一个工作流,打开XXX.jpdl.xml文件,就可以进行编辑了
通过编辑器,自己画一个工作流,会自动生成两个文件,加上刚才的文件一共有三个文件:
XXX.jpdl.xml 节点定义及流程描述
.XXX.gpd.xml 元素的位置信息
XXX.jpg 工作流截图
在发布工作流的时候,这三个文件都要带上
JBPM01

六、写Handler
编辑器上,可以直接处理的操作,其实很有限,也没有太多功能,主要还是靠编码实现。
编码的话,主要是实现几类接口:
org.jbpm.graph.def.ActionHandler:
多数的Handler,都用这个即可,包括transition、mail node、task node、node等
org.jbpm.taskmgmt.exe.Assignable:
主要用于任务执行者的分配
org.jbpm.graph.node.DecisionHandler:
主要用于选择节点

Handeler都只需要实现对于的方法即可。最后,通过编辑器进行配置。
JBPM02

六、发布工作流
发布也很简单,就是直接用eclipse上的jbpm菜单就可以发布了,但要把参数配置好才行
JBPM03

七、执行
1、首先要到openkm的后台,将工作流启用
2、然后找到任意一个文件、文件夹,执行工作流即可

八、填坑
遇到的最大的坑,莫过于字符集问题,我这边的方法,只修正了.XXX.gpd.xml的字符集问题,form的字符集问题,并没有修正
找到文件org.jbpm.gd.jpdl_3.4.1.v20120717-1252-H7-GA-SOA.jar,替换掉文件AbstractContentProvider.class即可
AbstractContentProvider.java源码如下:

package org.jbpm.gd.common.editor;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.part.FileEditorInput;
import org.jbpm.gd.common.model.NamedElement;
import org.jbpm.gd.common.model.SemanticElement;
import org.jbpm.gd.common.notation.AbstractNodeContainer;
import org.jbpm.gd.common.notation.BendPoint;
import org.jbpm.gd.common.notation.Edge;
import org.jbpm.gd.common.notation.Label;
import org.jbpm.gd.common.notation.Node;
import org.jbpm.gd.common.notation.NodeContainer;
import org.jbpm.gd.common.notation.NotationElement;
import org.jbpm.gd.common.notation.NotationElementFactory;
import org.jbpm.gd.common.notation.NotationMapping;
import org.jbpm.gd.common.notation.RootContainer;
import org.jbpm.gd.jpdl.Logger;

public abstract class AbstractContentProvider implements ContentProvider {
	protected abstract SemanticElement getEdgeSemanticElement(Node paramNode,
			Element paramElement, int paramInt);

	protected abstract SemanticElement getNodeSemanticElement(
			NodeContainer paramNodeContainer, Element paramElement, int paramInt);

	protected abstract void addNodes(NodeContainer paramNodeContainer,
			Element paramElement);

	protected abstract void addEdges(Node paramNode, Element paramElement);

	protected abstract SemanticElement findDestination(Edge paramEdge,
			Node paramNode);

	protected String getRootNotationInfoElement() {
		return "<root-container/>";
	}

	protected String createInitialNotationInfo() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		buffer.append("\n\n");
		buffer.append(getRootNotationInfoElement());
		return buffer.toString();
	}

	public String getNotationInfoFileName(String semanticInfoFileName) {
		return ".gpd." + semanticInfoFileName;
	}

	public String getDiagramImageFileName(String semanticInfoFileName) {
		int index = semanticInfoFileName.indexOf(".xml");
		String result = index > -1 ? semanticInfoFileName.substring(0, index)
				: semanticInfoFileName;
		return result + ".jpg";
	}

	protected void processRootContainer(RootContainer rootContainer,
			Element notationInfo) {
		addDimension(rootContainer, notationInfo);
		addNodes(rootContainer, notationInfo);
		postProcess(rootContainer);
	}

	protected void addNodes(NodeContainer nodeContainer,
			SemanticElement[] semanticElements, Element notationInfo) {
		List notationInfoElements = notationInfo == null ? new ArrayList()
				: notationInfo.elements();
		for (int i = 0; i < semanticElements.length; i++) {
			Element notationInfoElement = null;
			String nodeName = ((NamedElement) semanticElements[i]).getName();
			for (int j = 0; j < notationInfoElements.size(); j++) {
				Element element = (Element) notationInfoElements.get(j);
				String elementName = element.attributeValue("name");
				if (((elementName != null) && (elementName.equals(nodeName)))
						|| ((elementName == null) && (nodeName == null))) {
					notationInfoElement = element;
				}
			}

			addNode(nodeContainer, semanticElements[i], notationInfoElement);
		}
	}

	protected void addEdges(Node node, SemanticElement[] semanticElements,
			Element notationInfo) {
		List notationInfoElements = notationInfo == null ? new ArrayList()
				: notationInfo.elements();
		for (int i = 0; i < semanticElements.length; i++) {
			Element notationInfoElement = null;
			if (notationInfoElements.size() >= i + 1) {
				notationInfoElement = (Element) notationInfoElements.get(i);
			}
			addEdge(node, semanticElements[i], notationInfoElement);
		}
	}

	protected void addNode(NodeContainer nodeContainer,
			SemanticElement semanticElement, Element notationInfoElement) {
		String notationElementId = NotationMapping
				.getNotationElementId(semanticElement.getElementId());
		Node notationElement = (Node) nodeContainer.getFactory().create(
				notationElementId);
		notationElement.setSemanticElement(semanticElement);
		notationElement.register();
		nodeContainer.addNode(notationElement);
		semanticElement.addPropertyChangeListener(notationElement);
		processNode(notationElement, notationInfoElement);
		if ((notationElement instanceof NodeContainer)) {
			addNodes((NodeContainer) notationElement, notationInfoElement);
		}
	}

	protected void addEdge(Node node, SemanticElement semanticElement,
			Element notationInfoElement) {
		NotationElement notationElement = node
				.getRegisteredNotationElementFor(semanticElement);
		if (notationElement == null) {
			String notationElementId = NotationMapping
					.getNotationElementId(semanticElement.getElementId());
			notationElement = node.getFactory().create(notationElementId);
			notationElement.setSemanticElement(semanticElement);
			notationElement.register();
			node.addLeavingEdge((Edge) notationElement);
			semanticElement.addPropertyChangeListener(notationElement);
		}
		processEdge((Edge) notationElement, notationInfoElement);
	}

	protected void addDimension(RootContainer processDefinitionNotationElement,
			Element processDiagramInfo) {
		String width = processDiagramInfo.attributeValue("width");
		String height = processDiagramInfo.attributeValue("height");
		Dimension dimension = new Dimension(width == null ? 0 : Integer
				.valueOf(width).intValue(), height == null ? 0 : Integer
				.valueOf(height).intValue());
		processDefinitionNotationElement.setDimension(dimension);
	}

	protected void processNode(Node node, Element notationInfoElement) {
		addConstraint(node, notationInfoElement);
		addEdges(node, notationInfoElement);
	}

	protected void processEdge(Edge edge, Element edgeInfo) {
		processLabel(edge, edgeInfo);
		addBendpoints(edge, edgeInfo);
	}

	protected void addBendpoints(Edge edge, Element edgeInfo) {
		if (edgeInfo != null) {
			List list = edgeInfo.elements("bendpoint");
			for (int i = 0; i < list.size(); i++) {
				addBendpoint(edge, (Element) list.get(i), i);
			}
		}
	}

	protected BendPoint addBendpoint(Edge edge, Element bendpointInfo, int index) {
		BendPoint result = new BendPoint();
		processBendpoint(result, bendpointInfo);
		edge.addBendPoint(result);
		return result;
	}

	protected void processBendpoint(BendPoint bendPoint, Element bendpointInfo) {
		int w1 = Integer.valueOf(bendpointInfo.attributeValue("w1")).intValue();
		int h1 = Integer.valueOf(bendpointInfo.attributeValue("h1")).intValue();
		int w2 = Integer.valueOf(bendpointInfo.attributeValue("w2")).intValue();
		int h2 = Integer.valueOf(bendpointInfo.attributeValue("h2")).intValue();
		Dimension d1 = new Dimension(w1, h1);
		Dimension d2 = new Dimension(w2, h2);
		bendPoint.setRelativeDimensions(d1, d2);
	}

	private void processLabel(Edge edge, Element edgeInfo) {
		Element label = null;
		if (edgeInfo != null) {
			label = edgeInfo.element("label");
		}
		if (label != null) {
			Point offset = new Point();
			offset.x = Integer.valueOf(label.attributeValue("x")).intValue();
			offset.y = Integer.valueOf(label.attributeValue("y")).intValue();
			edge.getLabel().setOffset(offset);
		}
	}

	private void addConstraint(Node node, Element nodeInfo) {
		Rectangle constraint = node.getConstraint().getCopy();
		Dimension initialDimension = NotationMapping.getInitialDimension(node
				.getSemanticElement().getElementId());
		if (initialDimension != null) {
			constraint.setSize(initialDimension);
		}
		if (nodeInfo != null) {
			constraint.x = Integer.valueOf(nodeInfo.attributeValue("x"))
					.intValue();
			constraint.y = Integer.valueOf(nodeInfo.attributeValue("y"))
					.intValue();
			constraint.width = Integer
					.valueOf(nodeInfo.attributeValue("width")).intValue();
			constraint.height = Integer.valueOf(
					nodeInfo.attributeValue("height")).intValue();
		}
		node.setConstraint(constraint);
	}

	protected void postProcess(NodeContainer nodeContainer) {
		List nodes = nodeContainer.getNodes();
		for (int i = 0; i < nodes.size(); i++) {
			Node node = (Node) nodes.get(i);
			List edges = node.getLeavingEdges();
			for (int j = 0; j < edges.size(); j++) {
				Edge edge = (Edge) edges.get(j);
				SemanticElement destination = findDestination(edge, node);
				Node target = (Node) edge.getFactory()
						.getRegisteredNotationElementFor(destination);
				if ((target != null) && (edge.getTarget() == null)) {
					target.addArrivingEdge(edge);
				}
			}
			if ((node instanceof NodeContainer)) {
				postProcess((NodeContainer) node);
			}
		}
	}

	public boolean saveToInput(IEditorInput input, RootContainer rootContainer) {
		boolean result = true;
		try {
			IFile file = getNotationInfoFile(((IFileEditorInput) input)
					.getFile());
			InputStreamReader reader = new InputStreamReader(file.getContents(), "UTF-8");
			Element notationInfo = new SAXReader().read(reader)
					.getRootElement();
			if (upToDateCheck(notationInfo)) {
				getNotationInfoFile(((IFileEditorInput) input).getFile())
						.setContents(
								new ByteArrayInputStream(toNotationInfoXml(
										rootContainer).getBytes("UTF-8")),
								true, true, null);
			} else {
				result = false;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	protected String toNotationInfoXml(RootContainer rootContainer) {
		StringWriter writer = new StringWriter();
		write(rootContainer, writer);
		return writer.toString();
	}

	protected void write(RootContainer rootContainer, Writer writer) {
		try {
			Document document = DocumentHelper.createDocument();
			Element root = document.addElement("root-container");
			write(rootContainer, root);
			XMLWriter xmlWriter = new XMLWriter(writer,
					OutputFormat.createPrettyPrint());
			xmlWriter.write(document);
		} catch (IOException e) {
			e.printStackTrace(new PrintWriter(writer));
		}
	}

	protected void write(RootContainer rootContainer, Element element) {
		addAttribute(element, "name",
				((NamedElement) rootContainer.getSemanticElement()).getName());
		addAttribute(element, "width",
				Integer.toString(rootContainer.getDimension().width));
		addAttribute(element, "height",
				Integer.toString(rootContainer.getDimension().height));
		Iterator iter = rootContainer.getNodes().iterator();
		while (iter.hasNext()) {
			write((Node) iter.next(), element);
		}
	}

	protected void write(Node node, Element element) {
		Element newElement = null;
		if ((node instanceof AbstractNodeContainer)) {
			newElement = addElement(element, "node-container");
		} else {
			newElement = addElement(element, "node");
		}
		addAttribute(newElement, "name",
				((NamedElement) node.getSemanticElement()).getName());
		addAttribute(newElement, "x", String.valueOf(node.getConstraint().x));
		addAttribute(newElement, "y", String.valueOf(node.getConstraint().y));
		addAttribute(newElement, "width",
				String.valueOf(node.getConstraint().width));
		addAttribute(newElement, "height",
				String.valueOf(node.getConstraint().height));
		if ((node instanceof AbstractNodeContainer)) {
			Iterator nodes = ((AbstractNodeContainer) node).getNodes()
					.iterator();
			while (nodes.hasNext()) {
				write((Node) nodes.next(), newElement);
			}
		}
		Iterator edges = node.getLeavingEdges().iterator();
		while (edges.hasNext()) {
			Edge edge = (Edge) edges.next();
			write(edge, addElement(newElement, "edge"));
		}
	}

	protected void write(Edge edge, Element element) {
		Point offset = edge.getLabel().getOffset();
		if (offset != null) {
			Element label = addElement(element, "label");
			addAttribute(label, "x", String.valueOf(offset.x));
			addAttribute(label, "y", String.valueOf(offset.y));
		}
		Iterator bendpoints = edge.getBendPoints().iterator();
		while (bendpoints.hasNext()) {
			write((BendPoint) bendpoints.next(),
					addElement(element, "bendpoint"));
		}
	}

	protected void write(BendPoint bendpoint, Element bendpointElement) {
		addAttribute(bendpointElement, "w1",
				String.valueOf(bendpoint.getFirstRelativeDimension().width));
		addAttribute(bendpointElement, "h1",
				String.valueOf(bendpoint.getFirstRelativeDimension().height));
		addAttribute(bendpointElement, "w2",
				String.valueOf(bendpoint.getSecondRelativeDimension().width));
		addAttribute(bendpointElement, "h2",
				String.valueOf(bendpoint.getSecondRelativeDimension().height));
	}

	protected Element addElement(Element element, String elementName) {
		Element newElement = element.addElement(elementName);
		return newElement;
	}

	protected void addAttribute(Element e, String attributeName, String value) {
		if (value != null) {
			e.addAttribute(attributeName, value);
		}
	}

	private void createNotationInfoFile(IFile notationInfoFile) {
		try {
			notationInfoFile.create(new ByteArrayInputStream(
					createInitialNotationInfo().toString().getBytes("UTF-8")),
					true, null);
		} catch (CoreException e) {
			Logger.logError(e);
		} catch (UnsupportedEncodingException e) {
			Logger.logError(e);
		}
	}

	protected IFile getNotationInfoFile(IFile semanticInfoFile) {
		IProject project = semanticInfoFile.getProject();
		IPath semanticInfoPath = semanticInfoFile.getProjectRelativePath();
		IPath notationInfoPath = semanticInfoPath.removeLastSegments(1).append(
				getNotationInfoFileName(semanticInfoFile.getName()));
		IFile notationInfoFile = project.getFile(notationInfoPath);
		if (!notationInfoFile.exists()) {
			createNotationInfoFile(notationInfoFile);
		}
		return notationInfoFile;
	}

	public void addNotationInfo(RootContainer rootContainer, IEditorInput input) {
		try {
			IFile file = getNotationInfoFile(((FileEditorInput) input)
					.getFile());
			if (file.exists()) {
				InputStreamReader reader = new InputStreamReader(
						file.getContents(),"UTF-8");
				Element notationInfo = new SAXReader().read(reader)
						.getRootElement();
				boolean changed = convertCheck(notationInfo);
				processRootContainer(rootContainer, notationInfo);
				if (changed) {
					file.setContents(
							new ByteArrayInputStream(toNotationInfoXml(
									rootContainer).getBytes("UTF-8")), true,
							true, null);
				}
			} else {
				file.create(new ByteArrayInputStream(
						createInitialNotationInfo().toString()
								.getBytes("UTF-8")), true, null);
			}
		} catch (DocumentException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} catch (CoreException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private boolean convertCheck(Element notationInfo) {
		boolean changed = false;
		if (("process-diagram".equals(notationInfo.getName()))
				|| ("pageflow-diagram".equals(notationInfo.getName()))) {
			MessageDialog dialog = new MessageDialog(
					null,
					"Convert To 3.1.x Format",
					null,
					"A file created with an older GPD version was detected. If you open this file it will be converted to the 3.1.x format and overwritten.\nDo you want to continue?",
					3, new String[] { "Convert And Open",
							"Continue Without Converting" }, 0);
			if (dialog.open() == 0) {
				convertToRootContainer(notationInfo);
				changed = true;
			}
		}
		return changed;
	}

	private boolean upToDateCheck(Element notationInfo) {
		if (("process-diagram".equals(notationInfo.getName()))
				|| ("pageflow-diagram".equals(notationInfo.getName()))) {
			MessageDialog dialog = new MessageDialog(
					null,
					"GPD 3.0.x Format Detected",
					null,
					"The file you are trying to save contains GPD 3.0.x information.Saving the file will result in an automatic conversion into the 3.1.x format.It will be impossible to open it with the old GPD.\nDo you want to continue?",
					3, new String[] { "Save And Convert", "Cancel" }, 0);
			return dialog.open() == 0;
		}
		return true;
	}

	private void convertToRootContainer(Element notationInfo) {
		notationInfo.setName("root-container");
		convertChildrenToEdge(notationInfo);
	}

	private void convertChildrenToEdge(Element element) {
		List list = element.elements();
		for (int i = 0; i < list.size(); i++) {
			convertToEdge((Element) list.get(i));
		}
	}

	private void convertToEdge(Element element) {
		if ("transition".equals(element.getName())) {
			element.setName("edge");
		}
		convertChildrenToEdge(element);
	}
}

结语:
1、jbpm如果要用起来,还是要有很大的工作量的,而且早期版本对中文支持并不友好
2、openkm的工作流,整体来说太弱了,不够假单和直观
3、这种流程式管理方式,并不符合我们的管理方式
4、先用openkm的文档管理功能就好了
5、如果openkm再不升级框架,估计后面我们要找其他文档管理工具了。

OpenKM6搭建说明

近期发现,公司的文档管理十分的混乱,于是准备找一个文档管理工具来。

主要找了几类工具,第一类是Wiki,第二类是内容管理系统CMS,第三类是文档管理系统DMS,第四类是企业文档管理系统EDMS,第五类为在线协作软件。

我们的要求有:
1、可以兼容大量的历史文档
2、文档要在本地,不能在云端
3、最好可以直接版本管理和检索功能
4、最好免费

查看了这些软件的主流厂商,发现只有DMS文档比较适合我们:
1、wiki、多数的CMS希望大家用在线工具进行写作,我们有大量的word和execel文档,很难直接导入
2、在线协作软件要把文档托管在云端,也不适合我们
3、EDMS系统看了几个,发现功能太多,也不适合我们

最后选用了DMS,找了Alfresco免费版、LogicalDOC免费版和OpenKM免费版。周末自己进行了搭建测试,发现我的思路和Alfresco格格不入,设计理念太逆天了。LogicalDOC和OpenKM都不错,最后感觉OpenKM操作更顺畅一些,选用了OpenKM。

OpenKM有这几种搭建方式:
1、通过OKMInstaller.jar
https://sourceforge.net/projects/openkm/files/common/
2、通过打好的安装包,包括linux和windows版本,推荐
https://sourceforge.net/projects/openkm/files/6.3.2/
3、通过bundle
https://sourceforge.net/projects/openkm/files/6.3.2/
4、纯手工,从war包开始
https://sourceforge.net/projects/openkm/files/6.3.4/

本文介绍第3种方式。因为第1、2种方式相对简单,第4种方式和第3种方式基本一样(一定要到上面提到的common目录下下载tomcat)。

1、首先,下载最新的bundle,选择openkm-6.3.2-community-tomcat-bundle.zip,并解压
https://sourceforge.net/projects/openkm/files/6.3.2/

2、下载extra,并解压
https://sourceforge.net/projects/openkm/files/common/

3、安装mysql,新建数据库,新建用户,授权

4、修改解压后bundle文件夹中的OpenKM.cfg文件

# OpenKM Hibernate configuration values
#把方言改为MySQL
hibernate.dialect=org.hibernate.dialect.MySQLDialect
#第一次运行,一定要改为create
#运行后,系统会自动改为null
hibernate.hbm2ddl=none

# Initial configuration - Linux
#system.imagemagick.convert=/usr/bin/convert
#system.openoffice.path=/usr/lib/libreoffice
#system.swftools.pdf2swf=/opt/openkm/bin/pdf2swf -f -T 9 -t -s storeallcharacters ${fileIn} -o ${fileOut}

# Initial configuration - Windows
# 按extra文件的位置,改一下路径
system.imagemagick.convert=C:/NeoECM/OpenKM/Tomcat7/bin/convert.exe
system.openoffice.path=C:/NeoECM/OpenKM/extras/ApacheOpenOffice_4.1.1/Bin/OpenOffice 4
system.swftools.pdf2swf=C:/NeoECM/OpenKM/Tomcat7/bin/pdf2swf.exe -f -T 9 -t -s storeallcharacters ${fileIn} -o ${fileOut}

5、修改解压后bundle文件夹中conf/server.xml,修改数据库连接方式为mysql

    <Resource name="jdbc/OpenKMDS" auth="Container" type="javax.sql.DataSource"
            maxActive="100" maxIdle="30" maxWait="10000" validationQuery="select 1"
            username="openkm" password="openkm" driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost:3306/openkm?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF8"/>
                
    <!--Resource name="jdbc/OpenKMDS" auth="Container" type="javax.sql.DataSource"
            maxActive="100" maxIdle="30" maxWait="10000" validationQuery="select 1 from INFORMATION_SCHEMA.SYSTEM_USERS"
            username="sa" password="" driverClassName="org.hsqldb.jdbcDriver"
            url="jdbc:hsqldb:${catalina.base}/repository/okmdb"/-->

6、运行startup.bat

7、如果有错误,请查看日志。如果没有错误,就可以用okmadmin/admin进行登录了。

8、关掉控制台

9、将bin/win-x64下两个文件,拷贝到bin下面

10、命令行注册为服务

service install OpenVM

11、启动服务编辑界面

tomcat7w //ES//OpenVM

12、点击启动服务即可

13、如果遇到问题,请排查一下内容
A、JVM版本和tomcat的tomcat7版本是否同为32或同为64
B、tomcat7w界面中jvm.dll选择是否正确
C、如果还报错,可以把jdk/jre下的msvc*.dll拷贝到bin目录下面

Webmin搭建说明

1、下载安装包

wget http://prdownloads.sourceforge.net/webadmin/webmin_1.840_all.deb

2、安装依赖包

apt-get install perl libnet-ssleay-perl openssl libauthen-pam-perl libpam-runtime libio-pty-perl apt-show-versions python libapt-pkg-perl

3、安装webmin

dpkg --install webmin_1.840_all.deb

4、访问地址http://localhost:10000/
用户名:root,密码:系统root密码

Spring Boot各模块作用

1、spring-boot
web容器整合,快速开发,上下文、外部配置、日志的统一管理

2、spring-boot-autoconfigure
自动配置,自动判断需要的jar包

3、spring-boot-actuator
生产环境管理,用rest方式给出多种接口:
mappings/autoconfig/configprops/beans
env/info/health/heapdump/metrics
loggers/logfile/dump/trace
shutdown/auditevents

4、spring-boot-starters
各种配置好的功能模块,用于快速拼装各种需要的功能

5、spring-boot-loader
用于加载jar包中的jar,可以实现单个jar/war的运行
注意:这种jar包放到jar包时,不要再次压缩

6、spring-boot-cli
快速开发groovy

7、spring-boot-devtools
方便调试,远程调试