微信抢票

这篇博客主要是自己的微信抢票大作业的一些心得。

# 1 产品目标

相较于原来的项目,我会想到了以下可能的功能性需求

  • 邮箱验证:部分用户很担忧密码被泄露(见知乎);
  • 微信扫码检票:活动发布者能够更加方便的检票;
  • 脱离于微信的完整Web前端:使得用户可以在没有手机的情况下使用产品。

此外还有一些更加面向程序员的非功能性需求:

  • RBAC:基于Role的访问权限控制(因而也就有了独立的认证机制)
  • 前后端分离:无HTML模板,后端只负责提供动态内容而非表现;
  • RESTful:AJAX部分的API设计符合RESTful语义;
  • 实时消息:借助 Socket.IO,任何数据库的更新可以及时反馈到终端;
  • Scalable:状态管理及消息分发依托数据库(除缓存),支持多种分布式解决方案;
  • 安全:使用jwt,微信认证借助OAuth,用一次性token防回放攻击;
  • 日志:良好的日志信息,易于定位错误;
  • PWA:前端为单页应用,同时拥有Service Worker,像本地应用那样的前端体验。

总体来说,我更侧重于独立于微信的部分的开发,而不是依附于微信的那部分。

# 2 设计实现

# 2.1 后端架构

后端主要使用了Koa。RestFul部分的API直接调用Mongoose手写。微信部分API调用的是co-wechat系列的支持Promise的。Socket.IO负责消息实时推送,由于需要跨多个节点进行消息分发,因而也就引入了Redis作为消息的一个载体。此外Redis还用于存放一些一次性的token和一些临时信息。

细致来说,目前Mongodb数据库里有四张表

  • 用户:存储了用户的权限,密码加盐存储及其他信息。
  • 微信用户:之所以将这个表独立于用户表,是为了更加灵活地绑定解除绑定等等的操作。
  • 活动表:
  • 电子票表:

权限的主线在于JWT。用户可以通过用户名密码登录,或是续尚未过期的JWT或是使用微信OAuth登录等方式获得JWT。JWT中间存放了很关键的权限信息,是每一次需要特殊权限的借口调用的凭证。

所有的和验证相关API都是返回一个一次性使用的token,这些token存于redis中,主要有以下几种:

  • 用户注册信息:如邮箱验证(这样一份激活邮件只能使用一次)
  • 用户回话信息:有些回话可能因为跳转的缘故不那么容易保持,比如微信的OAuth认证,我们不信任前端发来的openId(原紫荆之声的实现)所以有了OAuth,但把不怎么能撤回的无状态的JWT放进URL真的是个非常危险的行为。因而我们会给用户一个临时的token用以兑换一个JWT。
  • 电子票临时检票信息:当用户打开电子票的时候,其上的二维码也是一个一次性的token(2分钟有效期),如果在这期间未能检票需要重新刷新。

其他所有的RESTful部分的API都是符合语义的,这里不再细说。每个API都是使用了符合JSON SCHEMA标准的ajv库验证输入的。此外查询部分的API还做了分页处理。涉及到文件上传的Multipart部分的文件检查是在上传过程中进行的,任何出错(如文件类型不对、文件过大)都会直接终止上传。

Socket.IO 的实时消息是做在Mongoose hook那一层的,因而基本上任何数据库更改都会实时反馈给相关的用户。

为了弥补NodeJS单线程的不足,我做了多进程的支持。

# 2.2 前端架构

前端贡献了代码的绝大部分。还是使用了Vue全家桶包括vuex、vue-router。UI使用的Vuetify。

前端部分并不比后端部分简单。实际上页面数目众多、交互复杂。

活动列表和电子票详情页面是支持类似知乎的infinity scroll。用户可以不停地往下滑,讲侦测用户滚动来实现按需加载。

抢票的时候需要与服务器同步时间,我这里采用的方法是用WebSocket向服务器发10条请求,取中位数分别作为ping和时间差。实际的抢票按钮再稍早于服务器时间一整个ping的时间开放允许点击(半个ping是数学期望50%刚好时间okay,改为一个ping为了避免偏差)。

扫码和分享的部分调用了微信JS SDK。用户分享能配个图啥的,感觉会好一点。

# 3 测试方案

使用了Mocha进行自动化测试,严格意义上可能这算是功能测试而非单元测试。此外还有手动的功能测试,主要是使用了Postman之类的RESTful API工具。

由于项目设计之初所有的API调用都嵌死在了RESTful API里,所以单元测试可能并不容易实现。这个我以后的项目可能会改进。

不过诚然,项目对于我而言是极其庞大的(不包括测试代码,只是运行的那部分前后端代码就已经达到了1万行),因此测试真的很重要。之后的大作业会尝试测试驱动。

性能方面测试的不多。只是先前随意测试,基本500到600每秒并发应该是okay的,带宽才是最重要的。

# 4 系统部署

考虑到助教提供的服务器出口带宽有限,我使用了自己的一台VPS szp15.cn。该域名尚在备案中。

Nginx方面主要负责静态文件(包括前端和上传的文件),开启了静态gzip查件,使得后缀为gz的静态文件能被直接压缩地发往用户。此外用Nginx作为反向代理还解决了SSL证书的问题。目前证书来源于Let’s Encrypt。

Mongodb开启了权限验证,不过由于本身不监听公网端口,倒也无所谓。

2016-2020 Ziping Sun
京ICP备 17062397号