下午收到封邮件,标题是沉痛哀悼xxxx,差点吓尿… 原来是哀悼之前的项目下线。引发了很多的回忆…
两年前加入雪球时写了篇十个字博客,今天就来完成那篇文章吧
Snowball 是雪球的「核心」项目(从名字就能看出来),就像歌手出道喜欢发行同名专辑一样。也是因为这个项目,雪球在「互联网金融」领域崭露头角。
email

第一次上线事故

记得刚进公司时第一次提交上线代码,是一个技术改造,某个接口虽然已经使用了缓存(redis),但因为业务较为复杂导致接口速度还是比较慢,最后考虑使用 local-cache 方案替换原有方案。但因为没有仔细了解透业务,导致遗漏一个接口没有替换。上线后,匿名首页出现异常,那时候雪球上线管理非常随意,一天可能上线多次。那次时间正好是下班时后七点左右,我正高兴的和妹纸吃着饭… leader 来电说出问题了,代码回滚,导致饭也没有吃好,以至于至今还记忆犹新。

之后业务发展越来越好,用户越来越多,单一项目带来的坑也越来越多。随后开始做项目拆分、RPC 改造。别扭着从零开始写 scala ,写到后面越来越顺手。之后换到组合业务、做雪球钱包,全都是 Scala 的项目。

公司搬家

15 年初公司顺利搬迁望京 SOHO, 也从一个小公司转变为100人左右的「中小型企业」,从一两个小破屋变成了有茶水间,健身房,餐厅,多个会议室的让人羡慕的工作环境,终于像一个土豪互联网企业了。

公司是大了,但并没有找到自己独有的商业模式。15年开始各种的尝试,当时组合售卖做的风生水起,大家仿佛看到了希望之光,直到某天一位中年秃顶男子来到我司。之前一直都是只闻 「紫金花」其名,不知其厉害。现在亲身体会了…此处省略八百字。

此路不通后开始转向全力做交易,此前组合业务同事大部分都转到目前我们这个基金业务当中了。小半年后我们的产品终于也即将上市了…尽请期待吧

雪球目前一百多人,我的工号 3x 也算是个老油条,现在已经有很多新员工不认识了~ 经常吃饭感觉到了一个陌生的公司…虽然人越来越多,但雪球还是那个雪球。

总结

雪球是个好公司,扁平化的管理,宽松的工作环境,管午饭,有年度旅游和各种小福利。产品有自己的逼格,有特有的创新,「组合」上线后已经被多个竞品模仿抄袭。几乎没有办公室斗争,虽然人数过百,还是保持着创业公司的风范。

当然也有缺点,雪球员工多数个性低调含蓄,从 leader 到员工都如此。整个团队环境是比较内敛的,新员工不容易融入,一些入职不久的新员工因为这个原因也很快就离职了。或许这就是雪球团队特有的一种风格。

当然以上所有都是我个人作为一个普通员工的看法,嘿

之前在团队内做了一次 Api 的规范化,决定采用 RESTful Api 的架构。 并统一了 JSON 结构,约定常用属性、字段的命名方式、URL/HTTP Method 的使用规则。
这样的好处不用多说,一个方便易懂,易于使用的 Api 能大大的提高开发效率(做开发的有谁没被第三方 Api 坑过?)

设计过程

之前我们有一些设计比较不错的 api 可能是这样的:

/user/(add/get/cancel)?id=123
/topic/(create/destroy/destroy_batch)?id=123 

也算是简单易懂了,那为什么要换成 RESTful 架构?(关于 REST 的特性,设计思想什么的就不说了,网上自取)这样带来最直接的问题就是不统一(规范)。我的 CRUD 叫 add/get/cancel,换一个人可能叫 create/modify/destroy, 甚至可能我的是 adduser 你的是 useradd (瞬间想到了 ubuntu 有木有!)。

规范这东西小范围没什么大问题, 一但队伍庞大了就是个大坑,所以大公司在规范上都是下了大功夫。

HTTP 协议就是一个标准的 REST 实现,RESTful Api 是以 HTTP 协议为强烈依托的,最终我们规范后是这样的:

/user/{uid} HTTP.GET/POST/PUT/DELETE
/topic/{tid} HTTP.GET/POST/PUT/DELETE

暂时只使用这四个方法,使用 PUT 替换 PATCH。很多人对部分情景下 PUT / POST 的使用会有疑问:

解答:根据 HTTP 文档介绍,GET/HEAD/PUT/DELETE 是 Idempotent(重复执行多遍,結果一致)。  
POST 每次执行肯定会新增一条记录,大多数情况下可以根据此原则从 POST/PUT 中选择合适的动作。  

/模块/{id} HTTP.Mehod 这种统一的规范能节省出不少的沟通时间~ 刚开始感觉很美好,当场景多起来之后又出现一些新的问题:
有一种功能叫「撤单」,使用 /order/{oid} HTTP.DELETE吗? 那删除订单用什么呢?
所以有人开始用 /order/undo/{order_id} HTTP.PUT 类似的方式。按 REST 规范每个 URL 都是一个独立资源,其中不应该包含任何动作。
也许应该改成 /order/{oid}?action=undo HTTP.PUT,但这样可能会在同一个接口中引入过多的操作。

类似这样的模糊的场景可能有许多,在 Api 数量随业务庞大的过程中需要反复的对当前 Api 做迭代重构,使它更适合当前的业务场景

那满足以上设计就是 RESTful Api 吗?
eg: /topic/fav/{tid} HTTP.POST 收藏某个话题

这个接口需要从 token 中获取登陆用户信息,且所有的用户都是访问同一个地址。
这与 REST 架构中的「无状态」和「唯一资源」的概念冲突。
按要求可能是这样的:

/topic/{uid}/fav/{tid} 

当然这些都是 REST 的建议,如何做还是要根据具体的业务情况而定

总结下来就是以下几点:
  1. 规范很重要
  2. 保持 Api 的优雅是一件持续的重复迭代的工作
  3. 不要局限于各种规范中,要在设计规范的时候充分考虑业务上的灵活性

起因:最近兴起一个 App 叫好近,专做 SOHO 附近小食,每日推出 1 分钱抢购或一些特价下午茶之类的活动,很难抢,不是错过就是被秒抢,遂想出个办法来提高抢购率!

最直接的需求是相对即时通知,让你快人一步。此类工具用 Python 来实现再合适不过了

提醒方式

工作期间不能老盯着手机,考虑通过 OSX 的 Notification 实现,让它带着音乐蹦出来~
调用 OSX 接口需要依赖 PyObjC 模块,通过 pip 装之(时间比较长,装一堆 OC 相关的包):

pip install PyObjC

调用的代码示例(python2.7+适用):

import objc
import Foundation

//py2.5可以通过 from Foundation import NSUserNotification 方式引入 
NSUserNotification = objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter')

def notify(title, subtitle, info_text, delay=0, sound=False, userInfo={}):
    notification = NSUserNotification.alloc().init()
    notification.setTitle_(title)
    notification.setSubtitle_(subtitle)
    notification.setInformativeText_(info_text)
    notification.setUserInfo_(userInfo)
    if sound:
        notification.setSoundName_("NSUserNotificationDefaultSoundName")
    notification.setDeliveryDate_(Foundation.NSDate.dateWithTimeInterval_sinceDate_(delay, Foundation.NSDate.date()))
    NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notification)

截获相关 Api

利用 Charles 可以很方便的抓取,经观察一些优惠信息只有移动端才有,构造相应的 headers,使用 requests 模块(pip 装之):

def queryNotification():
    url = "http://api.haojin.in/takeout_item_list?atag_id=0&offset=0&pagesize=10&region_id=55b9c9d4c69575999049b2b4"
    headers = {'content-type': 'application/json', 'User-Agent':'User-Agent: QMMWD/1.3.6 iPhone/9.1 AFNetwork/1.1'}
    req = requests.get(url, headers=headers)
    content = json.loads(req.content)
    items = content['data']['sale_items']
    for item in items:
        price = item['price']
        title = item['title']
        origin_price = item['origin_price']
        end_time = item['end_time']
        quantity = item['quantity']
        e = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
        if float(price) < 2 and e > datetime.datetime.now() and quantity > 0:
            notify(u"特价提醒!!", title, u"原价"+origin_price+u",现价:"+price, sound=True)

if __name__=='__main__':
    while True:
        queryHomeNotification()
        time.sleep(30)

每 30 秒查询一次,没结束的活动中价格低于 2 元且还有库存的优惠信息,一共不到 50 行代码,运行成功。但看起来使用方式并不友好,没办法分享给小伙伴们呢。

打包

想到把代码包装成一个 OSX 的应用,这样就方便任何人用了。使用 py2app 打包(pip 装之):

  1. 新建 setup.py,可以自定义 icon(icns 格式)

    from setuptools import setup
    
    APP = ['nearRemind.py']
    APP_NAME = "NearRemind"
    DATA_FILES = []
    OPTIONS = {'argv_emulation': True, 'iconfile': 'ico.icns'}
    
    setup(
        name=APP_NAME,
        app=APP,
        data_files=DATA_FILES,
        options={'py2app': OPTIONS},
        setup_requires=['py2app'],
    
  2. 打包,默认当前路径下 dist 目录:

    Python setup.py py2app
    

可以分享给小伙伴啦,启动就好,效果如下:
notification

其他

虽然有了提示但还是未必能抢到,所以后面还给自己做了自动下单版本,因为需要 session_id 和 sid 及收货地址之类的个人信息,做公用版比较麻烦些,就当是自己的私人福利了~
有兴趣自己玩的话代码在 Github

如此简易的 App 缺陷有很多,例如启动后没有合理的关闭方式,只能右键强制退出;无限重复通知等。

顺便发现「好近」的一些问题:

  • 接口访问频率似乎没有限制
  • 重复下单(一秒十单没问题~再快也没试了)
  • 活动状态好像是根据手机当前时间与接口里 end_time 对比来判断活动是否结束,且所有时间格式如下 “end_time”: “2015-12-21 23:59:00”,这种实现貌似不够好

Notification 刚移植到 OSX 上时就觉得这东西可以有很多种玩法,比如自己实现类似番茄工作法这样的小工具,这次也算试水成功了