微信抢票
大作业报告
产品目标
相较于原来的项目,我会想到了以下可能的功能性需求
- 邮箱验证:部分用户很担忧密码被泄露(见知乎);
- 微信扫码检票:活动发布者能够更加方便的检票;
- 脱离于微信的完整Web前端:使得用户可以在没有手机的情况下使用产品。
此外还有一些更加面向程序员的非功能性需求:
- RBAC:基于Role的访问权限控制(因而也就有了独立的认证机制)
- 前后端分离:无HTML模板,后端只负责提供动态内容而非表现;
- RESTful:AJAX部分的API设计符合RESTful语义;
- 实时消息:借助 Socket.IO,任何数据库的更新可以及时反馈到终端;
- Scalable:状态管理及消息分发依托数据库(除缓存),支持多种分布式解决方案;
- 安全:使用jwt,微信认证借助OAuth,用一次性token防回放攻击;
- 日志:良好的日志信息,易于定位错误;
- PWA:前端为单页应用,同时拥有Service Worker,像本地应用那样的前端体验。
总体来说,我更侧重于独立于微信的部分的开发,而不是依附于微信的那部分。
设计实现
后端架构
后端主要使用了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单线程的不足,我做了多进程的支持。
前端架构
前端贡献了代码的绝大部分。还是使用了Vue全家桶包括vuex、vue-router。UI使用的Vuetify。
前端部分并不比后端部分简单。实际上页面数目众多、交互复杂。
活动列表和电子票详情页面是支持类似知乎的infinity scroll。用户可以不停地往下滑,讲侦测用户滚动来实现按需加载。
抢票的时候需要与服务器同步时间,我这里采用的方法是用WebSocket向服务器发10条请求,取中位数分别作为ping和时间差。实际的抢票按钮再稍早于服务器时间一整个ping的时间开放允许点击(半个ping是数学期望50%刚好时间okay,改为一个ping为了避免偏差)。
扫码和分享的部分调用了微信JS SDK。用户分享能配个图啥的,感觉会好一点。
测试方案
使用了Mocha进行自动化测试,严格意义上可能这算是功能测试而非单元测试。此外还有手动的功能测试,主要是使用了Postman之类的RESTful API工具。
由于项目设计之初所有的API调用都嵌死在了RESTful API里,所以单元测试可能并不容易实现。这个我以后的项目可能会改进。
不过诚然,项目对于我而言是极其庞大的(不包括测试代码,只是运行的那部分前后端代码就已经达到了1万行),因此测试真的很重要。之后的大作业会尝试测试驱动。
性能方面测试的不多。只是先前随意测试,基本500到600每秒并发应该是okay的,带宽才是最重要的。
系统部署
考虑到助教提供的服务器出口带宽有限,我使用了自己的一台VPS szp15.cn。该域名尚在备案中。
Nginx方面主要负责静态文件(包括前端和上传的文件),开启了静态gzip查件,使得后缀为gz的静态文件能被直接压缩地发往用户。此外用Nginx作为反向代理还解决了SSL证书的问题。目前证书来源于Let’s Encrypt。
Mongodb开启了权限验证,不过由于本身不监听公网端口,倒也无所谓。
用户指南
删除旧用户 & 用户注册
用户是软删除,但其实除了数据库应该是对其他地方透明的。也可以禁掉用户,之后可以解禁。此外用户创建是可以借助邮箱的。
添加活动 & 修改微信菜单
上述视频出现了个小bug,在于修改活动的时候,后台因为前端请求多了个字段反回了400错误。该bug原因在于后端这里活动的“短名称”是创建之后不能修改的,旧版可修改。前端还没及时更新。
真的懒得fix这个bug了。反正大作业是写后端,不过变相说明我后端对于请求检查是很严的,anyway不是500而是400。
绑定微信 & 抢票 & 退票
微信绑定中的一闪而过的“登录中”其实是微信弹出的OAuth验证进度条。
演示了微信抢票和Web抢票,Web抢票其实有同步时间的过程,但已经完成。
整个系统都是实时通信的,唯独余票数目需要手动刷新,这是因为本来计划余票数目借由redis广播debounce之后再推送给所有客户端,属于单独的一条推送线路,然而比较复杂所以没写完。
微信扫码检票
这里借助手机的前置摄像头看了一下电脑的情况,检票成功后客户端是同步收到电子票检票成功的推送的。电子票的二维码是临时生成、每次都不同、只有2分钟有效期且一次性使用的。(可以等个2分钟看手动刷新界面)
用户管理
演示了RBAC和用户屏蔽。