从钉钉一个忽略了近亿人的产品细节谈谈产品思维

本来我一个软件工程师,是很不屑提产品的,但时不时总见到一些产品人吹牛皮,也忍不住凑个热闹。 钉钉(DingTalk)是一款由阿里巴巴集团开发的用于商务沟通和工作协同的IM,其和企业版微信占据了中国的大部分企业IM市场。阿里并不是一个擅长做社交的公司,钉钉也是一款命途多舛的产品。2014年左右阿里在内部强行推广来往,一款承载了阿里社交梦的产品,花了巨额的研发和营销费用后,依然是折戟沉沙。后来,来往的团队保留了部分下来,做起了钉钉,针对办公社交,居然做成功了。 在办公社交上,钉钉的崛起甚至早于以社交闻名的腾讯,在社交上扳回了一局,甚至可以说是唯一的一局。微信和QQ在钉钉后也快速推出了办公社交QQ和企业微信等功能,但是在它们推出后,钉钉在很多城市使用的频率已经很高了。 我用的是linux操作系统,钉钉并没有官方linux版本,所以有时候我会使用手机钉钉和网页版钉钉凑合。但是某一次打开钉钉设置的时候,发现了一个问题。 不知大家注意到没有,网页版钉钉的设置使用了流行的switch开关,但是用了红色和绿色的搭配。可能钉钉的设计和开发者觉得“红灯停,绿灯行”的概念已经深入人心,但是他们是否想到了一个事实:中国存在近亿的色盲和色弱用户,这其中又以红绿色盲色弱最多! 红色和绿色,是两个对比度比较接近的颜色,也是最难辨识的两个颜色,别说对色盲和色弱用户来说,即使是对于普通人来说,在某些光线条件下,红绿色也是很难于辨识的。实际上在打开这个页面的时候,我也愣了一会,才辨清了这两个颜色状态。 正因为红绿色是很难于辨识的两种颜色,现在城市的红绿灯,大部分都是掺了蓝色的,所以大家看到的绿灯,都是泛蓝的,而不是单纯的绿色。还有的城市,绿灯不仅掺了蓝色,还会使用动画或声音提示行人车辆,目的就是为了减少了特殊用户甚至是正常用户的困扰。 阿里一直宣传产品的人文关怀,比如雇佣残疾人客服,支付宝支持语音支付等,但是可曾想到,他们另外一款最流行的企业IM软件,却忽略了上亿人! 产品的设计开发中,有许多细节,只有真正用心的人才会注意到,并设计出用户友好的软件,减少用户的困扰。 作者使用了钉钉很久了,最早的APP版设置页面就是使用红色和绿色来作为swith开关的,作者曾经在微博等多个渠道向阿里反馈,可惜一直没有收到阿里的回复,直到一年后的某天,钉钉悄悄地改了这个细节,也不知道是无意中修改还是真的收到了用户的反馈。遗憾的是,网页版钉钉至今没有修改这个细节。

使用Java自动登录需要动态密码的堡垒机

公司的生产服务器买了QiZhi Technologie的堡垒机,每次登录都得输入密码+空格+OTOP验证码,都得打开手机APP操作一把,烦不胜烦。 不可忍,想了想,还是借助Java在每次调用时自动生成验证码,然后搞个ssh自动登录(别问我问啥不用公钥,哪有权限啊)得了。 结合之前写的博客 TOTP算法Java版本,很容易就写出计算验证码的代码: public long getCode(String secret, long timeMsec) throws Exception { Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret); long t = (timeMsec / 1000L) / 30L; for (int i = -window_size; i <= window_size; ++i) { long hash; try { hash = verify_code(decodedKey, t + i); return hash; } catch (Exception e) { e.printStackTrace(); } } return 0L; } 写一个类,专门调用这个方法生成验证码,获取程序执行结果 java -Dfile.encoding=UTF-8 -classpath /soft/tool/authcode/ GoogleAuthTest ,接下来,要实现自动登录就简单多了,先写一个shell

Linux 下获取文件创建时间

在Linux里,用户层面并没有文件创建时间的概念,无论是用ls还是stat 指令,都无法获取到文件的创建时间 [tudou@tudou-pc statx]$ stat test-statx.c 文件:test-statx.c 大小:6656 块:16 IO 块:4096 普通文件 设备:805h/2053d Inode:6684737 硬链接:1 权限:(0644/-rw-r--r--) Uid:( 1000/ tudou) Gid:( 1001/ tudou) 最近访问:2018-10-07 13:16:29.000000000 +0800 最近更改:2018-10-07 13:21:09.855461986 +0800 最近改动:2018-10-07 13:21:09.855461986 +0800 创建时间:- 可以看到「创建时间」一行总是「-」。 如果我们使用百度的话,会看到很多文章说,最近改动时间就是创建时间。的确,我们拿很多文件试验了下,这个最近改动时间(Change Time)确实和创建时间很相近,然而Change time并不是Create time,它实际是文件属性修改时间。 试一下即知: [tudou@tudou-pc 下载]$ ./statx ~/.face statx(/home/tudou/.face) = 0 results=fff Size: 7589 Blocks: 16 IO Block: 4096 regular file Device: 08:05 Inode: 5505043 Links: 1 Access: (0644/-rw-r--r--) Uid: 1000 Gid: 1001 Access: 2018-09-16 01:15:52.

linux下解压bin文件

现在的一些Linux软件很流行使用bin这种安装包格式,只需要下载个安装包就能自动安装解压,比tar.gz省事,比.deb,.rpm的安装包兼容性强,适应范围广。但也有一个问题,bin安装包让人无法知道里面的细节,还是有所顾虑的。比如我前几天需要下载一个JRE6,但Oracle官方在JDK7之前都没有tar.gz包,只有bin包。我肯定不能直接安装bin文件啊,这会破坏我本机已有的JDK8开发环境。 怎么从bin文件里提取出原始安装包呢?其实很简单。用vi打开一个bin文件就知道了,bin文件其实就是一个sh文件和二进制文件的合并文件,前面一段是sh命令,负责实际的安装,它会提取后半部分的二进制数据,后半部分一般是个压缩文件包或者自解压文件的二进制流。 vi jre-for-linux.bin 可以看到,第一行是 #!/bin/bash 接下来就是一堆安装和设置环境变量,提取解压部分了,最关键的部分在这几行 outname=install.sfx.$$ tail ${tail_args} +162 "$0">$outname chmod +x $outname 继续往下看,267行是exit 0,从第268行开始,就是一堆看似乱码的二进制了,到这里那就清晰多了 # 从268行起提取二进制文件 tail -n +268 jre-for-linux.bin >install.sfx # 因为是sfx格式,就用7z解压 7z x install.sfx 到此解压成功。手动安装,使用export设置临时变量,就用上了JRE6了。

使用内嵌undertow开发调试jfinal项目

今天在修一个老项目,使用的是jfinal框架,这个框架算是一个比较传统的框架,只支持打包成war运行放入容器中运行,但是在开发过程中可以使用jetty快速启动和调试。个人不是很喜欢jetty,遂换成了undertow。 引入如下依赖 <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-core</artifactId> <version>2.0.1.Final</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-servlet</artifactId> <version>2.0.1.Final</version> <scope>provided</scope> </dependency> 再写一个启动类就好了 public class Main { public static void main(String[] args) throws Exception { DeploymentInfo servletBuilder = Servlets.deployment() .setContextPath("/") .setClassLoader(Main.class.getClassLoader()) .setDeploymentName("zooadmin.war") ; FilterInfo jfinalFilter=new FilterInfo("jfinal",JFinalFilter.class); jfinalFilter.addInitParam("configClass","com.baicai.core.Config"); servletBuilder.addFilter(jfinalFilter); servletBuilder.addFilterUrlMapping("jfinal","/*", DispatcherType.REQUEST); servletBuilder.addFilterUrlMapping("jfinal","/*", DispatcherType.FORWARD); servletBuilder.setResourceManager(new FileResourceManager(new File("src/main/webapp"), 1024)); DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder); manager.deploy(); PathHandler path = Handlers.path(Handlers.redirect("/")) .addPrefixPath("/", manager.start()); Undertow server = Undertow.builder() .addHttpListener(1080, "localhost") .setHandler(path) .build(); // start server server.

导出freeOTP中的配置

之前公司的一个网站使用了OTP来做二次验证,然后我就在手机上安装了freeotp这款软件来管理OTP密码,等到换手机了,才发现没法导出原手机的配置,这就尴尬了。FreeOTP is sponsored and officially published by Red Hat,也算是大家闺秀出品的软件,居然不支持这么重要的功能。 试了很多方法,在手机的文件管理器中到处搜索,都没有找到这个配置,基本可以确定freeotp把密钥存放在了系统目录,没有root的话,是没法查看和处理系统目录下的文件,即使用备份工具也备份不出来。 当初网站的OTP二维码也找不到了,网站也没找到重新设置OTP的入口,本着万事不求人的想法,暂时还不想最后求助运维。看来唯一的办法就是root手机了,试了很多工具,没想到kingroot居然支持root魅蓝手机了。 root成功后,马上去freeotp的配置存储目录找到配置文件,找到 /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml 文件,得到如下的配置,配置中的引号被转义了 <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="bbcx@qq.com:chen">{"algo":"SHA256","counter":0,"digits":6,"issuerExt":"bbcx@qq.com","label":"chen","period":30,"secret":[17,-56,-42,-70,-48,-79,53],"type":"TOTP"}</string> <string name="bbc">{"algo":"SHA1","counter":0,"digits":6,"issuerExt":"","label":"bbc","period":30,"secret":[0,1,2,3],"type":"TOTP"}</string> <string name="tokenOrder">["bbcx@qq.com:bbcx","bbc"]</string> </map> 可以看出,这里面是就是关于otp的全部配置了,最关键的就是secret字段,这里做了加密,反复试验了半天,没找到解决方案,最终想到Google,找到了这个解决方案: https://github.com/viljoviitanen/freeotp-export/blob/master/README.md ,只需要把tokens.xml贴到这里,https://rawgit.com/viljoviitanen/freeotp-export/master/export-xml.html,就能还原出二维码来,用新手机扫描就好了。 事情还没完,最后想去freeotp的官方那里反应下,没想到官方的态度让我大跌眼镜,https://github.com/freeotp/freeotp-android/issues/20,“出门右转买收费软件去,老子就是不增加备份功能,你能咋地”。 "'''Can I create backupcodes'''? ''No, but if you're using an Android smartphone you can replace the Google Authenticator app with Authenticator Plus. It's a really nice app that can import your existing settings, sync between devices and backup/restore using your sd-card.

Java里常见的几个语法小坑

很久没更新博客了,想到几个小坑,虽然没啥技术含量,但或许有人不知道呢。 1.删除sublist的元素导致原对象元素被删除 看下面这段代码 List<Integer> students=new ArrayList<Integer>(); for (int i = 0; i <5 ; i++) { students.add(i); } List<Integer> subList=new ArrayList<Integer>(); subList=students.subList(0,5); subList.remove(0); subList.remove(1); for (int i = 0; i <5 ; i++) { System.out.println(i+"="+students.get(i)); } students是个list,然后我们新建立了一个subList对象,这个对象截取了students的一部分,我们删除了subList对象里的一些元素,看下运行结果。 0=1 1=3 2=4 Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.get(ArrayList.java:433) at bai.ListDo.main(ListDo.java:17) 难道说,删除subList对象里的元素也会导致students里的元素被删除?我明明是新建了一个对象啊。然而,事实确实是这样的。 我们要理解一个事情,使用new新建一个对象,只是开辟了一块空间,用来存放这个对象的地址指针,但是这个新建的对象地址,指向的却是原有对象,也就是说,使用subList这个方法的时候,并没有从students里把内容拷贝了一份,仅仅是纪录了一个指针的移动,这样从某种角度来说,是提高了性能节省内存的做法。 看一下subList这个方法的JavaDoc我们就更清楚了。 Returns a view of the portion of this list between the specified * <tt>fromIndex</tt>, inclusive, and <tt>toIndex</tt>, exclusive.

折腾阿里云OSS的API

这两天想给博客做个插件,利用阿里云的OSS来存储文件.但阿里的文档和代码都烂的超乎想象,要么代码老旧不堪,要么跟小脚老太一样引入一坨依赖,想必这块是外包团队做的吧,或者阿里非核心业务员的技术水平也就这样吧.  所以想绕开阿里云官方提供的代码自己整一套OSS的API,先跑一个上传文件的demo,能在客户端跑通后再用代码去实现.最简单的方法就是用REST client来模拟.折腾了一下,还挺费劲,记录下折腾过程  先来试试上传文件,选择PUT方法,要请求的URL为http://baicaidoc.oss-cn-shenzhen.aliyuncs.com/image/small/mm1.jpg ,添加以下header,header头需要包含哪些内容可以看这里 Authorization:OSS LTAIxkX6Qj2OuMZ6:tLZ7nYYP/hkCJbG/6gkOJ7Mi4E= Date:Thu, 25 Jan 2018 15:20:39 GMT Content-Disposition:attachment;filename=ivy.jpg Host:baicaidoc.oss-cn-shenzhen.aliyuncs.com Content-Encoding:utf-8 然后在body里添加file body. 至于header头怎么写和Authorization字段计算的方法,文档里说的比较清晰了https://help.aliyun.com/document_detail/31951.html. 尤其需要注意的是Date必须是GMT格式,这个对Java来说也好办,不过要注意时区的问题,GMT时间比东八区慢了8个小时.还有Host需要带上bucket,这在早期是不需要的(早期带上反而会报错SignatureDoesNotMatch) 另外就是这个Authorization字段的签名需要注意,base64需要处理byte[]数组,而不是字符串.所以用网上的在线验证工具是验证不了的. Java版的签名代码如下: import bai.tool.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; /** * Hello world! * */ public class App { public static byte[] hamcsha1(byte[] data, byte[] key) { try { SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return mac.doFinal(data); } catch (NoSuchAlgorithmException e) { e.

PHP的mb_check_encoding函数的存在是鸡肋吗

前不久,有人问到我一个问题,就是使用mb_check_encoding来侦测一段字符的编码,预期是GBK编码,但是PHP给出来UTF-8编码的错误判断。那么,mb_check_encoding的正确姿势是什么呢? 我们来看一段代码, <?php $utf8Str = '别abc扯淡'; var_dump(mb_check_encoding($utf8Str, 'UTF-8')); //输出true var_dump(mb_check_encoding($utf8Str, 'gbk')); //输出true  这段代码的输出是啥呢?按理,我们的PHP文件保存为什么编码,那它输出的就应该是啥编码,然而以上输出的都是true。再换个例子,这样呢? <?php $utf8Str = '别abc扯淡啊'; var_dump(mb_check_encoding($utf8Str, 'UTF-8')); var_dump(mb_check_encoding($utf8Str, 'gbk'));  后面多加了一个汉字,这次PHP做出了正确的判断,给出了是UTF-8的判断。那么mb_check_encoding到底有没有用?是这个函数有bug还是我自己不懂姿势?  难道是,只要汉字是3的整数倍就会判断失灵?试验后确实是的,当然这只是表面现象,但无疑说明这个函数是不可靠的。为什么呢?其实原理说起来也不难理解,计算机并不懂什么叫乱码。一段文字,解释成UTF8或GBK其实都是可以的,我们用肉眼看到有了乱码,根据我们的经验,觉得解释成这种编码是错误的,而解释成另外一种编码才算正确。可是计算机不懂啊,你觉得有个字符很奇怪,你不认识所以认定是乱码,可计算机认识啊,它不觉得奇怪。除非字节数解释成另外一种编码,会多出一个字节,并且ASCII码也不是常见范围,计算机才能大胆判定解释成这种编码不对。所以这样去检测编码是无法完全可靠地.  那既然mb_check_encoding这个函数不可靠,那么用正则可靠么?或许吧。 但是我们更应该关注的是PHP为什么会有这么一个功能?为什么其他语言没有这个方法,或者根本不会遇到这个问题?  问题还是出在PHP本身。因为客户端可能会有多种编码输入,PHP为了解决这个问题就引入这么一个贴心的函数给使用者。可是PHP不应该是遇到问题就去动歪脑筋解决问题啊,而且规范问题。为什么其他语言不需要在SDK里引入这个方法呢?或者说是PHP程序员的使用姿势不正确?  最后,其实PHP给出这个函数也不算错,但是一定要参照其他语言里的惯行做法,在文档里说清楚,这个函数的判断的是一种“可信度”,而不是给出一个非此即彼的“权威”结果。但是遗憾的是,这个函数的文档里没说很好的说清楚,而是这么写的, > “Checks if the specified byte stream is valid for the specified encoding. It is useful to prevent so-called “Invalid Encoding Attack Returns TRUE on success or FALSE on failure.”  其实加上这样一句话“This function only give the confidence level of the result”就好了,也就不会平白引起那么多的疑虑。

FireJava输出Java服务器端调试日志到控制台

针对最新火狐浏览器50+以上版本的firebug协议,类似FirePHP,但是FirePHP已经很久不更新,并且对最新的浏览器也已失效。 > 这个在Firebug之上运行的扩展,结合一个服务器端的库,就可以让你的PHP代码向浏览器发送调试信息,该信息以HTTP响应头(HTTP headers)的方式编码。经过设置,你可以像在Firebug控制台调试JavaScript代码一样得到PHP脚本的警告和错误提示。下面我们来看看具体步骤。 直接上代码 import com.alibaba.fastjson.JSON; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @version V1.0 * @Description:直接输出服务器端调试日志到控制台,简易版本。 * @date 2017/6/13 16:51 */ public class DebugTool { public final String VERSION = "2.0.j1"; public final String HEADER_NAME = "X-ChromeLogger-Data"; protected Map<String, Object> console = new HashMap<>(); private String response=""; public DebugTool() { console.put("version", VERSION); console.put("columns", new String[]{"log", "backtrace", "type"}); console.put("rows", new ArrayList<Objects>()); console.put("request_uri", this.getClass().getName()); } public DebugTool(Class cls) { this(); console.