What a May Day

周四晚上跟生日的父母吃过晚饭后,傻乎乎的带着小梦梦在孩子王逛,一点没有意识到已经是五月的最后一天。有时候,记录的习惯更多的就是提醒自己,时间的昼夜不舍。

这个月有意识的接触了挺多人,对于鄙人这种社交贫瘠的人来说,这个月花在这方面的时间算是奢侈的了。有老友也有新朋友。一次跟新朋友印象深刻的夜谈。已经很久没有跟新认识的人如此没有负担的沟通和交流了。对了,上一次有意思的聊天也是去年的这个时候。初夏,真是一个神奇的时节,一切都开始要变得明亮而耀眼。

五月在做和要做的事情越来越多,一种此情无计可消除,才下眉头,却上心头之感。一部分算是甜蜜的负担,而立之年,一些事情逐渐跟自己是否准备好已经没有必然关系,而是直接去解决它就对了。一部分是多过去时光的辜负,亡羊补牢,希望犹未晚矣。前天看韩老师的5X兴趣社区,看到李伟龙一个视频的幕后花絮,对话挺走心的:时间只会让你老去,其他什么都不会带来;只有你想改变的时候,你才能改变。

神奇的五月,居然达成了跑渣的第一次5公里(一个都不好意思提的配速)。从去年4月份参加跑团开始跑步,到现在已经一年多了,跑步成绩上没有任何提升,也是我预料之中的。对于这件事,我其实想得很明白:我一点都不喜欢运动,但是要支撑我的情怀和要做的事情,我必须要有这个练习和准备。显然,如果保持当前的做事的节奏,也许一周一次的跑步很快就无法支撑自己在做的事情,但是只要保持这件事情的惯性,我相信这股力量不会让自己失望。

这个月最喜欢的书是吴军老师的《智能时代》。因为一直在订阅吴军老师在得到的专栏,因此书中的很多内容其实都在专栏中听过了(如此说来,维护专栏及时高产如母猪,也是需要有存货当备份的?)。有两点体会最深:

  1. 人类文明发展是一个不短加速的过程,每一次加速都会让已有产业与新结束结合形成形成新的产业,赶上这个浪潮的会以数量级的优势领先,赶不上或者不愿拥抱变革的则会被无情的淘汰。
  2. 大数据和AI是当前最有可能成为下一个时代的蒸汽机和电。超越时代是困难的,但是从思维方式上则是可以刻意练习大数据和AI思维的。对于程序员而言,这尤为重要——有很大可能性,这决定了当前的你是成为为工业时代的码农,还是智能时代的工程师。

六月会迎来自己在两个月前设定的一个deadline, 从目前看来,不容乐观。可能当时在设定这个目标的时候,其实内心的真实独白就已经是法乎其中则得其下,法乎其上则得其中。但是,总的来说,过去的两个月无论是还在发生还是已经发生的事,多少带来了一丝丝改变。

期待六月,不负好时光。

微信小程序文件上传二三事

这段时间陆陆续续上了好几个微信小程序,功能上都会用到文件上传功能(头像上传、证件照上传等)。在APP上传文件到云端的正确姿势中,我们介绍了我们认为安全的上传流程:

即将密钥保存在服务器,客户端每次向服务器申请一个一次性的signature,然后使用该signature作为凭证来上传文件。一般情况下,向阿里云OSS上传内容,又拍云作为灾备。

随着大家安全意识的增强,这种上传流程几乎已经成为标准姿势。但是,把这个流程在应用到微信小程序却有很多细节需要调整。这里把踩过的坑记录一下,希望能让有需要的同学少走弯路。

微信小程序无法直接读取文件内容进行上传

在我们第一版的上传流程方案中,我们的cds 签名发放服务只实现了阿里云 PutObject 接口的signature发放. PutObject 上传是直接将需要上传的内容以二进流的方式 PUT 到云储存。

但是,微信小程序提供的文件上传API wx.uploadFile 要求文件通过 filePath 提供:

另一方面,微信小程序的 JS API 当前还比较封闭,无法根据 filePath 读取到文件内容,因此也无法通过 wx.request 直接发起网络请求的方式来实现文件上传。

考虑到 wx.uploadFile 本质上是一个 multipart/form-data 网络请求的封装,因此我们只需要实现一个与之对应的签名发放方式接口。阿里云OSS对应的上传接口是 PostObject, 又拍云对应的是其 FORM API. 以阿里云OSS为例,cds 服务生成signature 代码如下:

func GetDefaultOSSPolicyBase64Str(bucket, key string) string {
    policy := map[string]interface{}{
        "expiration": time.Now().AddDate(3, 0, 0).Format("2006-01-02T15:04:05.999Z"),
        "conditions": []interface{}{
            map[string]string{
                "bucket": bucket,
            },
            []string{"starts-with", "$key", key},
        },
    }
    data, _ := json.Marshal(&policy)
    return base64.StdEncoding.EncodeToString(data)
}

func GetOSSPostSignature(secret string, policyBase64 string) string {
    h := hmac.New(sha1.New, []byte(secret))
    io.WriteString(h, policyBase64)
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

小程序端代码如下:

//使用说明
/**
 * 1、引入该文件:const uploadFile = require('../../common/uploadAliyun.js');
 * 2、调用如下:
 * uploadImg: function () {
        const params = {
            _success: this._success
        }
        uploadFile.chooseImg(params);
    },
    _success: function(imgUrl){
        this.setData({
            cover_url: imgUrl,
        })
    },
*/

const uploadFile = {
    _fail: function(desc) {
        wx.showToast({
            icon: "none",
            title: desc
        })
    },
    _success: function() {},
    chooseImg: function(sendData) {
        //先存储传递过来的回调函数
        this._success = sendData._success;
        var that = this;
        wx.chooseImage({
            count: 1,
            sizeType: ['original', 'compressed'],
            sourceType: ["album", "carmera"],
            success: function (res) {
                that.getSign(res.tempFilePaths[0]);
            },
            fail: function (err) {
                wx.showToast({
                    icon: "none",
                    title: "选择图片失败" + err
                })
            }
        })
    },
    //获取阿里上传图片签名
    getSign: function (path) {
        var that = this;
        wx.request({
            url: 'https://somewhere/v2/cds/apply_upload_signature',
            method: 'POST',
            data: {
                "content_type": "image/jpeg",
                "signature_type": "oss_post",
                "business": "xiaochengxu",
                "file_ext": '.jpeg',
                "count": '1'
            },
            success: function (res) {
                let getData = res.data.data[0];
                that.startUpload(getData, path);
            },
            fail: function (err) {
                that._fail("获取签名失败" + JSON.stringify(err))
            }
        })
    },
    //拿到签名后开始上传
    startUpload: function (getData, path) {
        var that = this;
        this.uploadAliYun({
            filePath: path,
            dir: 'wxImg/',
            access_key_id: getData.oss_ext_param.access_key_id,
            policy_base64: getData.oss_ext_param.policy_base64,
            signature: getData.signature,
            upload_url: getData.upload_url,
            object_key: getData.oss_ext_param.object_key,
            content_url: getData.content_url.origin 
        })
    },
    uploadAliYun: function(params) {
        var that = this;
        // if (!params.filePath || params.filePath.length < 9) {
        if (!params.filePath) {
            wx.showModal({
                title: '图片错误',
                content: '请重试',
                showCancel: false,
            })
            return;
        }
        const aliyunFileKey = params.dir + params.filePath.replace('wxfile://', '');

        const aliyunServerURL = params.upload_url;
        const accessid = params.access_key_id;
        const policyBase64 = params.policy_base64;
        const signature = params.signature;
        wx.uploadFile({
            url: aliyunServerURL,
            filePath: params.filePath,
            name: 'file',
            formData: {
                'key': params.object_key,
                'policy': policyBase64,
                'OSSAccessKeyId': accessid,
                'Signature': signature
            },
            success: function (res) {
                if (res.statusCode != 204) {
                    that._fail("上传图片失败");
                    return;
                }
                that._success(params.content_url);
            },
            fail: function (err) {
                that._fail(JSON.stringify(err));
            },
        })
    }
}


module.exports = uploadFile;

使用阿里云OSS域名上传失败

解决签名问题后,发现使用阿里云OSS提供的上传域名无法上传成功,在微信后台尝试添加合法域名的时候,惊奇的发现阿里云OSS的域名直接被微信小程序封禁了:

显然是两个神仙在打架,作为草民只能见招拆招。解决办法就是在阿里云OSS -> bucket -> 域名管理 绑定用户域名:

此外,由于微信小程序已经升级为uploadFile的链接必须是https, 因此还需要在绑定用户域名后设置 证书托管

他山之石,可以攻玉

既然微信能够封禁用阿里云OSS的上传域名,那么微信也可以封禁你自定义的域名。根据以往经验(对天发誓,我们不是有意为之,我们也是受害者……),微信封禁域名一般都是一锅端,即发现一个子域名存在违规内容,那么整个域名都会被封禁。因此,一方面要从技术角度对上传的内容及时检查是否合规(如黄图扫描),另一方面提前做好域名规划,将业务接口域名与自定义的文件上传域名分开,这样即使上传域名被一锅端了,不至于是业务完全不可用。

热眼旁观:老罗与锤子

第一次对老罗的全面了解是本科时候的那趟南戴河毕业旅行:出发的时候随便抓了一本书塞书包里——《我的奋斗》。一群毕业屌丝自然坐的是最便宜,但如今想来却最有意思的绿皮火车去的南戴河。在路上和车站上看完了这本书。

与其说这是一本书,不如说这是老罗在新东方几年上课和演讲的语录。即使按照当时的认知,也不会把它归为推荐一类的书。但这并不重要,重要的是,对这个彪悍且还在继续活跃的胖子有了一个相对完整的了解。如今依然不知道那本书是从哪里来,后来遗失到了哪里去。很多事情,奇妙的偶然性让我知道了这个世界上有个有趣并值得持续关注的灵魂。

一共参加过两次老罗的现场演讲。

第一次是研究生一年级,老罗来学校做一个理想主义者的创业故事II的演讲。那时候老罗还没有开始做手机,演讲中用来插科打诨的公司主体是他的英语培训机构。那时候的自己对商业也没有清晰认识和完整理解。只是自己当时也在参与创业,对「理想主义」和「创业者」这两个词是没有任何抵抗力的。仿佛找到了一起参与一场改变世界革命的战友,惺惺相惜,感同身受。当然,也可见当时万众创业对在校学生荼毒之深?。

那次分享的很多故事依然记忆犹新,但是要说最深刻的应该是:学会站着赚钱。理所当然的道理遇到浮躁的年代,往往知难行易。感谢老罗的那次演讲,深刻地影响了自己后来创业、工作时候处理棘手事情的底限和原则。

第二次是锤子科技在成都举行的 2017 秋季新品发布会。刚得知消息的时候,还是非常意外的,因为的确没有想到在从帝都回到成都工作的第四个年头,能够在成都听一场老罗的现场相声。

也许近两年成都的热度太高了吧,偌大的成都大魔方演艺厅票务放出即售罄。最后还是因为前东家是锤子科技的股东,因此老同事给置办了一张内部票才得以入场,也算是听相声路上的一段小插曲。那次的发布会发布了两个产品,一个是坚果 Pro 2——目的非常明确,坚果 Pro 是锤子迄今为止最畅销的手机,Pro 2 意图百尺竿头,更进一步; 一个是与成都雾霾非常应景的空气净化器——畅呼吸,算是在产品品类上横向扩充的尝试。

两个产品都很棒,但是即使凭借门票有优惠和优先购买权、手上的 iPhone 6 Plus 廉颇老矣的情况下,自己也没有购买这次发布会上的任何一款产品。很多人说,锤子的产品历来都是叫好不叫座,也许是有原因的?

回到几天前的鸟巢发布会。有人说坚果 R1 中规中矩,毫无亮点;与之结合的 TNT 工作站就是一个笑话。我不想参与这个结论的辩论,一来自己还没有体验过这两个产品,讨论结论为时过早,毫无意义;二来我想我可能是一群人的典型代表:关注老罗和锤子,只因为这是一家老罗牵头的公司。

R1 是创新吗?当然不是。在手机如此同质化的今天,无论是小厂锤子科技还是巨厂苹果,这几年出的产品整体上都达无法满足大家的心理预期。R1 不过是众多改进型产品中的一员。至于老罗说的全世界第一,哎哟喂,都是成年人了,大家平时都在吹牛X,你不能自己吹完不让别人吹不是?

TNT 是创新吗?当然是。我会购买吗?现在当然不会。

Touch and Talk 的交互方式会是次时代的主流人机沟通方式吗?我不确定,这里没有和稀泥的意思。这个问题可以反过来看:如今嘴里含着触屏手机出生的一代,他们对于 touch 的交互几乎是与生俱来的一种操作直觉,如今的键鼠操作一定不是这一代人所有场景下的最终交互归宿,在很多场景下,一定会有新的交互方式来代替当前的这种操作交互。那么它会是 Touch and Talk 吗?这个真的不知道。超越时代是困难的,而在超越时代的产品真正出现的时候,在大众眼中,这种超越往往又是荒诞的。

我知道很多程序员都会调侃说 TNT 写代码可能会是这样的:

关于这一点,我想说,也许应用在现实的生产工具场景中,也许它……真的就是这样的。但是,这其实杠精了。回想一下老罗介绍的几个 TNT 应用场景:表格数据自动处理、keynote 语音辅助编辑、闪念胶囊自动找图和幻灯片生成…… 没有一个是要取代如今的密集生产工具场景。虽然现场 demo 状况频出,老罗也是汗如雨下,但是,我想你不会否认这么几点:老罗的敏感使其非常善于发现问题,他提到的问题和场景你一定不会陌生,他给出的解决方案也许不是你会选择的方法,但是一定是解决了特定用户群的痛点。

有人说,TNT 工作站这种手机 + Dock 的方式根本不是老罗首创,因为好几年前的摩托罗拉和当前的三星都有对应的产品。单从产品形态来说,TNT 的确没有重新发明轮子。甚至加上 Touch 和语音,整个解决方案也必然不是业界首创。

在这一点上,我比较同意吴军老师的第三眼美女以及发明是以最后一个发明家的名字命名的理论。老罗和他的 TNT 会是这个垂直品类的最后一个发明者以及第三眼美女吗?说实话,我并不确定,但是我愿意将一半的可能性投给老罗。

因为,对于老罗,我想我可能是关注老罗若干群体中的某个群体的典型代表:每次老罗的相声都不会错过,但是却几乎未曾真正触动自己的钱包;表面是老罗的粉丝,但是却自诩为独立思考,不是任何人的拥趸;关注老罗,更多的是他所言所行符合了某一群理想主义者的价值观,视其位精神的知己,行动的马前卒;将老罗及其挂帅的锤子科技看做浮躁创业环境下的一股清流,并由衷乐见其成;经常感怀自己,如果老罗失败了,是这个社会对价值的多元筛选和宽容失败了。

TNT 最终会怎样 boom 我们交给时间评判。但是,老罗一直都是那个埋头做事的人,同时更是平地吹B三丈起的奇才。我依然会是一个旁观者,如同观看一场带有完全主观倾向的比赛,向他投与这场比赛所有的希冀和祝福。

:吴军老师第三眼美女理论

第三眼美女当然是相对第一眼美女和第二眼的美女。第一眼美女有什么特点呢?首先,一眼看上去就很漂亮,但是不属于大众。这里面有很多原因,或许是因为她们本身就认为自己是精英人群而非主流人群,抑或是这些人光芒太扎眼,一般人想接近也接近不了她们。总之结果是,大众只能在远距离去欣赏她们了。其次,人有时会看走眼,乍一看很漂亮,接近以后如果发现没有内涵,看到第二眼、第三眼时,未必还能有最初的好印象了。

第二眼美女未必有第一眼的那么天生丽质,因此她们常常需要更懂得时尚细节才能引来周围人欣赏的眼光,但是这样一来,和一个第二眼美女交往的成本就比较高,大众即使心里痒痒的,未必能得到。即使得到了,第二眼美女的脾气未必好,因此双方的蜜月期一过,可能也就形同陌路了。

第三眼美女是属于大众的,她们未必那么显眼,但是如果仔细观察,她们还是不错的。更重要的是,正是因为她们可能没有光鲜的外表,如果依然能够吸引人,那么必定有某种美德或者价值。而对于欣赏这种美德,或者看重这种价值的人来讲,他们对第三眼美女的喜欢会持续很久,除非这种美德和价值不再存在或者过时了。