3月23日,团队收到指导老师的微信消息,学校希望在封校期间通过提前预约的方式开放图书馆,老师要求团队在“小风筝”小程序或 App 中添加图书馆预约功能。具体要求是:图书馆仅开放2层共274个座位,除周一、周二闭馆外,每天有两个时段(后改为上午、下午、晚上三个时段)开放。

老师向团队提出需求的聊天记录

经过沟通团队了解到,2层所有座位将会被编号,四人桌只允许坐一人,六人桌只允许坐两人,入口处由志愿者负责入场检查,图书馆内部由志愿者巡查。

有部分同学提出了方案:

  1. 图书馆多开放一些座位,例如同时开放三楼、四楼,还是很容易做到不拥挤的。但可能由于人手有限,图书馆老师并不同意。

  2. 安排同学将每张桌子所配多余的椅子撤走。但是没有得到采纳。

  3. 直接使用图书馆闸机系统控制进馆人数,每桌座位限制可以通过安排工作人员巡视的方式进行。但此方法由于门禁系统过旧,不大可行。

  4. “第二课堂” 刷卡机可以记录入馆人员信息,但是不方便记录人数。出入时可能存在多次刷卡。

  5. 微信公众号的抢票平台。该方法需要每天发布,较麻烦。

但这些方案并未得到采纳,因而接下来要通过技术方法解决问题。

本项目的项目地址:

功能项目
用户侧kite-app
后端kite-server
管理侧kite-admin-app

可行性分析

由于小程序暂不维护,团队决定在 App 中添加预约功能。由于 App 不能强制用户更新,对于苹果平台在更新前需要等待 1 天的审核时间,功能和限制须尽量完善,方便向后兼容。

需求分析

团队为防止图书馆临时变更开放教室、开放时间等信息,大部分限制需要由服务器完成,且需要一个单独的公告模块将临时性的公告发送用户。预约操作听起来很简单,但列出需求之后可不少。团队安排同学将每张桌子安排同学将每张桌子列出了如下基本功能:

  1. 管理员预先设置好教室座位数,同时标记该房间是否可用。 教室 (校区,房间ID,名称,最大人数,可用标记)

  2. 支持显示公告,应对临时性通知。

  3. 管理员需要提前设置好可申请的日期段,如3月1日~3月31日,以及上午下午。

  4. 管理员可以获取某一个人或某一天预约记录

  5. 用户可以查看某一日可用座位数

  6. 用户可以查看自己的预约记录,可以预约和取消预约。

    • 用户只能申请当天及次日座位;
    • 用户只能取消自己的预约记录,且该场次未开始。
  7. 用户可以展示自己的预约状态(二维码)并可以保存本地,二维码中包含用户信息、预约 ID、预约日期。

  8. 管理员可以扫码获取用户预约状态;放行并标记该用户已到馆。 因此每一个申请application)应该包含(日期, 用户编号, 房间编号, 入馆标记),前两项须唯一。

  9. 对前一天爽约的同学,禁止预约第二天

但仍存在一些疑问。后续通过和老师的沟通,对需求描述做了修改:

  1. 不需要支持徐汇校区的图书馆。

  2. 不需要处理图书馆大厅公共区域的桌椅。

  3. 爽约的同学禁止预约次日座位。

  4. 入馆二维码不允许截图,程序通过算法保证二维码信息不被修改,防止在座位不够的情况下“借用截图”入馆的行为。

同时团队发现一个漏洞:如果某用户在1日预约了1、2日的座位,但1日全天没去,如何处理次日座位?讨论无果,团队决定先编写程序,看一看实际预约情况再说。

沟通时是3月23日周三,预计系统将于3月30日使用,扣除两天苹果应用商店的审核时间、一天提前宣传的时间,开发团队只有不到四天的时间。

设计

基本概念

表结构

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天时间,最直接的感受就是——很难预估项目的时间成本。当然很大原因是需求分析没有做好,在字段、功能限制上修修补补花了不少时间。在写功能时,常常写着写着——“要不再加个功能吧!”也侧面反应出需求分析的问题。

正值上海疫情爆发,每日感染人数增长,希望能尽自己绵薄之力、助力抗疫。毕设拖了好久,也该写了。

以上。