3月23日,团队收到指导老师的微信消息,学校希望在封校期间通过提前预约的方式开放图书馆,老师要求团队在“小风筝”小程序或 App 中添加图书馆预约功能。具体要求是:图书馆仅开放2层共274个座位,除周一、周二闭馆外,每天有两个时段(后改为上午、下午、晚上三个时段)开放。
经过沟通团队了解到,2层所有座位将会被编号,四人桌只允许坐一人,六人桌只允许坐两人,入口处由志愿者负责入场检查,图书馆内部由志愿者巡查。
有部分同学提出了方案:
图书馆多开放一些座位,例如同时开放三楼、四楼,还是很容易做到不拥挤的。但可能由于人手有限,图书馆老师并不同意。
安排同学将每张桌子所配多余的椅子撤走。但是没有得到采纳。
直接使用图书馆闸机系统控制进馆人数,每桌座位限制可以通过安排工作人员巡视的方式进行。但此方法由于门禁系统过旧,不大可行。
“第二课堂” 刷卡机可以记录入馆人员信息,但是不方便记录人数。出入时可能存在多次刷卡。
微信公众号的抢票平台。该方法需要每天发布,较麻烦。
但这些方案并未得到采纳,因而接下来要通过技术方法解决问题。
本项目的项目地址:
功能 | 项目 |
---|---|
用户侧 | kite-app |
后端 | kite-server |
管理侧 | kite-admin-app |
可行性分析
由于小程序暂不维护,团队决定在 App 中添加预约功能。由于 App 不能强制用户更新,对于苹果平台在更新前需要等待 1 天的审核时间,功能和限制须尽量完善,方便向后兼容。
需求分析
团队为防止图书馆临时变更开放教室、开放时间等信息,大部分限制需要由服务器完成,且需要一个单独的公告模块将临时性的公告发送用户。预约操作听起来很简单,但列出需求之后可不少。团队安排同学将每张桌子安排同学将每张桌子列出了如下基本功能:
-
管理员预先设置好教室和座位数,同时标记该房间是否可用。
教室 (校区,房间ID,名称,最大人数,可用标记)
-
支持显示公告,应对临时性通知。
-
管理员需要提前设置好可申请的日期段,如3月1日~3月31日,以及上午下午。
-
管理员可以获取某一个人或某一天预约记录。
-
用户可以查看某一日可用座位数。
-
用户可以查看自己的预约记录,可以预约和取消预约。
- 用户只能申请当天及次日座位;
- 用户只能取消自己的预约记录,且该场次未开始。
-
用户可以展示自己的预约状态(二维码)并可以保存本地,二维码中包含用户信息、预约 ID、预约日期。
-
管理员可以扫码获取用户预约状态;放行并标记该用户已到馆。
因此每一个申请(application)应该包含(日期, 用户编号, 房间编号, 入馆标记)
,前两项须唯一。 -
对前一天爽约的同学,禁止预约第二天
但仍存在一些疑问。后续通过和老师的沟通,对需求描述做了修改:
-
不需要支持徐汇校区的图书馆。
-
不需要处理图书馆大厅公共区域的桌椅。
-
爽约的同学禁止预约次日座位。
-
入馆二维码不允许截图,程序通过算法保证二维码信息不被修改,防止在座位不够的情况下“借用截图”入馆的行为。
同时团队发现一个漏洞:如果某用户在1日预约了1、2日的座位,但1日全天没去,如何处理次日座位?讨论无果,团队决定先编写程序,看一看实际预约情况再说。
沟通时是3月23日周三,预计系统将于3月30日使用,扣除两天苹果应用商店的审核时间、一天提前宣传的时间,开发团队只有不到四天的时间。
设计
基本概念
-
场次
一个7位整数,标记了日期和上午、下午、晚上。如
2203251
,前6位表示22年3月25日,最后1位“1”表示上午。 -
预约记录
指某一用户在某一场次的申请记录,包括座位号和入馆状态。
表结构
1 | create table library.application |
SQL 函数
1 | create function apply(_uid integer, _period integer, max_seat integer) |
关键问题
并发
需求分析一旦结束,后面的设计和开发工作就容易了。但很快,团队也遇到了一个计算机中的经典问题:
在多人(近似)同时预约座位时,数据库可能会出现并发问题。假设程序流程是:先求出当前空闲座位的集合 ,再将当前同学的座位安排到其中。那么两个用户近似同时访问时就会出现:
-
A 查询空闲座位,得到
[1, 2, 3]
-
B 查询空闲座位,得到
[1, 2, 3]
-
A 申请座位 1
-
B 申请座位 1
会导致两个用户的座位申请冲突。
团队提出用一个方法侧面解决这个问题:即,每个人在申请预约时,只获得一个入场的“许可”。待到入场时决定该同学实际的座位号。但这个方案被老师否决。
解决方案
通常数据库的并发问题,可以通过加锁解决。在数据库事务中加入以下行即可:
1 | LOCK TABLE library.application IN ROW EXCLUSIVE MODE; |
PostgreSQL 中的锁会等到事务结束后自动释放,其他锁类型可以查阅相关资料。
安全问题
对于用户侧出示的二维码,若允许截图,在座位不够的情况下,可能存在未预约用户通过截图的方式骗取门口进入图书馆,或者使用其本人曾经的二维码截图。因此在管理端要主动提示不符合入馆条件的入馆码。同时,为防止二维码篡改,项目中使用了公私钥加密算法,在后端返回二维码数据时,使用私钥对数据签名,返回格式如:
1 | { |
签名的明文是 id
、index
、period
、user
、timestamp
(二维码生成日期)等几个字段组成的,使用 RSA 2048 签名后,再通过 base64 编码得到签名字段 sign
。管理端 App 初始化时从服务端获取公钥后校验签名合法性。
效果
截图(左为用户侧视图,右为管理侧视图):
需求变更
就在团队交付图书馆和团委志愿者队使用时,有老师提出,希望在导出的名单中添加姓名字段。修改之余,团队感受到在最初商议需求时对细节把握不够,没有认真、详细地和老师沟通。今后应当引以为戒。
“你永远也不知道用户会怎样使用你的产品”——同样在与用户交接时,有老师提出:要求程序每天晚上十点自动停止预约,并由志愿者导出第二天的预约名单。入馆时,学生在纸质版名单上签名。后经解释,老师才同意使用我们的系统记录登记信息。
突发状况
对于这一项目来说,可靠性是最重要的,绝不能出现用户卡在图书馆门口的情况。这里的可靠性一是指在特定用户量(并发)下的可靠性,一是指程序不出现 Bug 的可靠性。由于经费限制,前者无法解决,只要没有人闲着去攻击服务器倒也问题不大,而 Bug 的问题,却还是发生了。
——项目第一天上线的时候,去图书馆现场围观,看到和朋友写的项目实际落地还是很开心的,突然有同学反映无法预约。周围还站着很多老师呢,但只得打开电脑,ssh 连上服务器看日志,发现是用户注册时数据库返回了错误,找到报错的 SQL 语句如下:
1 | INSERT INTO \"user\".account (account, name) VALUES($1, $2) |
看上去是不知何时按到了鼠标中键,在操作系统中被解释为粘贴,因而多了一个 name
,由于该代码是最近修改的,没有经过充分测试。这里不得不吐嘈一下中键粘贴这个设定……修改后用户注册功能即恢复正常。
后记
从最开始接到这一需求,心里想当天就能做出来,到实际完成总共花了5天时间,最直接的感受就是——很难预估项目的时间成本。当然很大原因是需求分析没有做好,在字段、功能限制上修修补补花了不少时间。在写功能时,常常写着写着——“要不再加个功能吧!”也侧面反应出需求分析的问题。
正值上海疫情爆发,每日感染人数增长,希望能尽自己绵薄之力、助力抗疫。毕设拖了好久,也该写了。
以上。