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”表示上午。 -
预约记录
指某一用户在某一场次的申请记录,包括座位号和入馆状态。
表结构
create table library.application
(
id serial
constraint application_pk
primary key,
period integer not null,
uid integer not null
constraint application_account_uid_fk
references "user".account,
status integer default 0 not null,
index integer default 0 not null,
ts timestamp with time zone default now() not null,
constraint application_pk_2
unique (period, uid)
);
SQL 函数
create function apply(_uid integer, _period integer, max_seat integer)
returns TABLE(id integer, index integer, is_exist boolean)
language plpgsql
as
DECLARE apply_id integer;
DECLARE seat_index integer;
BEGIN
SELECT FALSE INTO is_exist;
-- 查询用户申请记录
SELECT a.id, a.index, TRUE INTO apply_id, seat_index, is_exist FROM library.application a WHERE period = _period AND uid = _uid LIMIT 1;
-- 如果该用户未申请过
IF seat_index IS NULL THEN
LOCK TABLE library.application IN ROW EXCLUSIVE MODE;
-- 取一个座位
SELECT coalesce(library.get_next_seat(max_seat, _period), 0) INTO seat_index;
IF seat_index != 0 THEN
-- 插入用户预约记录
INSERT INTO library.application (period, uid, index) VALUES (_period, _uid, seat_index)
ON CONFLICT (period, uid) DO NOTHING
RETURNING application.id INTO apply_id;
END IF;
END IF;
-- 如果该用户已经申请过了,返回上次申请的座位
-- 返回申请到的座位号,自动释放锁
RETURN QUERY SELECT apply_id, seat_index, COALESCE(is_exist, FALSE);
END
$$;
create function get_next_seat(max_seat integer, _period integer) returns integer
language plpgsql
as
DECLARE seat integer;
BEGIN
SELECT generate_series(1, max_seat) AS index
INTO seat
EXCEPT (SELECT index FROM library.application WHERE period = _period)
ORDER BY index
LIMIT 1;
RETURN seat;
END
ZZZDANDMATH1ZZZSZZZDANDMATH2ZZZ1, ZZZDANDMATH3ZZZ1, SET name name = 2
RETURNING uid, account, name, create_time, role, is_block;
看上去是不知何时按到了鼠标中键,在操作系统中被解释为粘贴,因而多了一个 name,由于该代码是最近修改的,没有经过充分测试。这里不得不吐嘈一下中键粘贴这个设定……修改后用户注册功能即恢复正常。
后记
从最开始接到这一需求,心里想当天就能做出来,到实际完成总共花了5天时间,最直接的感受就是——很难预估项目的时间成本。当然很大原因是需求分析没有做好,在字段、功能限制上修修补补花了不少时间。在写功能时,常常写着写着——“要不再加个功能吧!”也侧面反应出需求分析的问题。
正值上海疫情爆发,每日感染人数增长,希望能尽自己绵薄之力、助力抗疫。毕设拖了好久,也该写了。
以上。