🍪

文化祭入退場システムCrackerの再開発 【U22プロコン】

2023/12/05に公開

半年前ほどに所属する高校の文化祭で利用する入退場管理システム「Cracker (v1)」を開発しました。
https://zenn.dev/pocopota/articles/2d3d489272f724

そのシステムを色々機能を加えたりして「Cracker v2」を再開発しました。
そこでU-22プログラミングコンテストに応募してみると、経済産業省商務情報政策局長賞プロダクト部門を受賞することができました。ありがとうございます。

この記事ではCracker v2の機能や技術的な話やコンテストの感想とかを書いていこうと思います。Cracker v1の時の記事をまず読んでからこちらを読むことをおすすめします。

以下発表で使用したスライド。一部の写真は非表示済み。

前提事項

少し分かりにくいワードチョイスがあるので前提事項として書いておきます。
【用語の定義】

  • ユーザー : 管理システムを操作する学校教員やスタッフ生徒等
  • ゲスト:文化祭に来場する保護者や一般客

【バージョン情報】

  • v1 : 最初に作ったバージョン、実際に文化祭で使用
  • v2 : この記事で話すやつ、コンテストに提出したバージョン、v1から色々変更した

あとこの記事内で記載されているゲスト情報や諸々の情報はデモデータです。

システム構成

システム全体の構成としては以下のようになっています。
フロントエンド、バックエンドを分けて開発しています。ほぼ全てPHPです。(なんてレガシーな...)

フロントエンド

フロントエンドは役割別に複数サイトが存在し、サブドメインでわけられています。

  • cracker.xxx : メインサイト、管理者ページ
  • form.cracker.xxx : ゲスト用事前申し込みサイト
  • ticket.cracker.xxx : ゲスト用チケット表示サイト
  • reg.cracker.xxx : ユーザー登録サイト

ほぼ全てをPHPで作成しました。
また前回に追加してトースト通知用にNotyfを、ユーザーアイコン用にBoring avatarsを使用しています。

バックエンド

REST API

フロントエンドとバックエンドを明確に分けた開発を一度してみたかったのと、サイトが複数ドメインにまたがるという理由からAPIを通じてデータベース操作などをする方式に変更しました。

REST APIを作成し、それを用いることでユーザー情報やゲスト情報のやり取りを行うことができます。
フレームワークとかを使ってREST APIを作ればより楽に、安全に作れると思いますが、今回はPHPを用いて全てのコードを自分で書き、RESTっぽいルーティングとかを実装しました。

ルーティングなどの仕組みは良くわかりましたが、安全性等を考慮して次回からはフレームワークを使うと思います。

https://qiita.com/naga3/items/030f757ed413515551db
https://qiita.com/guchimina/items/9f351944ddaa33ba73b0


実装したAPIの量、めっちゃ多い...

REST APIの認証は事前に作成されたAPIキーをリクエストヘッダーに入れ込み、そのAPIキーが正しく、かつアクセス元のサーバーのIPアドレスがデータベースに保存されているものであればデータのやり取りができます。正直安全性が不安。

あとSQL文をめっちゃ書きました。プレゼンの練習に付き合ってくれた現役エンジニアの方からORMの存在を知り、調べてみるとめちゃくちゃ便利そうでした。

APIドキュメント

APIドキュメントは(自分しか使わないが)Docusaurusにまとめています。内容が雑な部分もあるためわざわざリンクは貼りませんが、GitHub pagesでホストしているので色々リンク辿ったりすると行き着くかもしれません。

ファイル生成系API

チケット(QRコード)の生成と後述するゲスト情報の記載されたPDFの生成の為にRESTとは別にAPIを作成しました。
こちらもPHPのGDを用いて作成しています。(どんだけPHP使うねん)

データベース

データベースはMySQLを用いています。DBに関する全ての操作はREST APIを介して行われます。

テーブル構成

一応データベースの中身を置いておきます。v1よりも機能の増加に伴いテーブル数も増えました。

user

key type description
user_id VARCHAR(20) primary key
user_name VARCHAR(40) ユーザー名
exhibit_id VARCHAR(20) 所属展示ID
password VARCHAR(256) 「セキュリティ/パスワード保管」に基づく

※パスワードの管理に関しては別ページでソルトとかハッシュ化とか書いてある。今回は省略

guest_group

key type description
group_id VARCHAR(128) primary key, uuid
guest_type VARCHAR(10) family/other
st_name VARCHAR(20) 生徒氏名, 姓名間空白なし
st_belong VARCHAR(30) 生徒所属
st_grade VARCHAR(10) 生徒学年, n年表記
st_class VARCHAR(10) 生徒クラス, n組表記(クラスなしの場合はなし)
state VARCHAR(20) not_entered/entered/disable
mail VARCHAR(400) メールアドレス

guest_typeについて

  • family : 生徒の同居家族
  • 家族以外の一般の人

stateについて

  • not_entered : 未入場
  • entered : 入場済み
  • disable : ゲスト情報無効化済み

not_entered/enteredの切り替えは文化祭入場口で処理した場合のみ行います。
各展示会場での処理では変更しません。
disableとなったゲストは入場が許可されません。

guest

key type description
guest_name VARCHAR(20) ゲスト名, 姓名間空白なし
relation VARCHAR(12) 母/父/兄/弟/姉/妹/他
group_id VARCHAR(128) グループID

activity

key type description
activity_id VARCHAR(128) primary key, uuid
guest_id VARCHAR(128) 対象ゲストID
user_id VARCHAR(20) 処理を行ったユーザーID
exhibit_id VARCHAR(20) 処理が行われた展示ID
activity_type VARCHAR(5) enter/exit
timestamp timestamp 処理時刻

activity_typeについて

  • enter : 入場処理が行われた際のタイプ
  • exit : 退場処理が行われた際のタイプ

exhibit

key type description
exhibit_id VARCHAR(20) primary key
exhibit_name VARCHAR(60) 展示名

user_invitation

key type description
invitation_code VARCHAR(8) primary key
usage_count INT(4) 使用回数
count_limit INT(4) 残登録可能数
time_limit timestamp 登録可能制限時間
creator VARCHAR(20) コード制作ユーザーID

role

key type description
role_id VARCHAR(128) primary key, uuid
role_name VARCHAR(60) ロール名
authority VARCHAR(60) ex)A000000000000000000000000000000

authorityについて
Aで始める。
各桁に権限の有無を記す。
0は権限なし、1は権限あり

usersrole

key type description
user_id VARCHAR(20)
role_id VARCHAR(128)

ホスティング

全てPHPで作られているので自分のレンタルサーバー上でホストしています。ちなみにConoHa。

機能たち

v2で新たに加わった新機能や変更点などを主に紹介します。あまりv1から変わっていない点に関しては書きません。

事前申し込み

事前申し込み周辺はv1でシステム面でも人的面でもエラーが多かったので根本から変更しました。

v1ではGoogle Formsを用いて、データはGoogle Apps Scriptを通じてデータベースへ登録がなされていました。運用段階でGASの制限による処理が実行されなかったり、メールアドレスの打ち間違えが多発しました。
そこでフォームを1から作成し、REST APIを通じることで常に正常な処理が行えるようになりました。また最初にメールアドレスを入力してもらい、確認コードを送信して認証を行う方式を取りました。

(左)メールアドレス入力, (中)確認コードチェック, (右)各種情報入力

現在のフォームでは申し込み完了画面に遷移した後、ブラウザのバックボタンを押すと再度ゲスト登録がなされてしまうので、改修が必要です。

チケットスキャン

チケットスキャンページのゲスト情報を表示する部分のUIをそれっぽく変更してみました。
チケット風のUIにしました。誰かが作ったCSSをコピペしようかと思いましたが、良さげなのが無く、結局自分でCSS書きました。大変だった...

チケットUI

チケットUI部分コード

HTML

<div class="ticket">
    <div class="rip"></div>
    <div class="ticket-header {family|other}">
        <div>文化祭チケット (家族枠)</div>
    </div>
    <div class="rip border"></div>
    <div class="ticket-middle {entered|not_entered|disable}">
        <div>未入場</div>
    </div>
    <div class="rip border"></div>
    <div class="ticket-body">
        <div class="first-guest">
            <div class="ticket-title">代表者</div>
            <div class="first-guest-info">
                <div>テスト太郎 <span>(父)</span></div>
            </div>
        </div>
        <div class="st_info">
            <div class="ticket-title">生徒情報</div>
            <div class="st_name">テスト花子</div>
            <div class="st_belong">普通科特進コース2年1組</div>
        </div>

        <div class="data-box">
            <div class="ticket-title">メールアドレス</div>
            <div class="data">guest-mail@example.com</div>
        </div>
        <div class="data-box">
            <div class="ticket-title">ゲストID</div>
            <div class="data">b355c172-c381-49f5-9b5d-4a63141be69a</div>
        </div>
    </div>
    <div class="rip"></div>
</div>

{a|b}などと書かれているclass部分はゲストの種類等によってプログラムでclassを書き換え、そのclassによってCSSで当てられるスタイル(色)が異なります。
もちろんゲスト情報もプログラムで書き換えられます。

CSS

ここに上げているスタイルとは別にBlumaだったりが効いてるからこれだけだと若干足りないかも

.ticket {

    width: 350px;
    margin-top: calc(34px - 15px/2 - 2px);

    .rip {
        position: relative;

        &.border {
            border: dashed 1px #aaa;
        }

        $circle-size: 15px;

        &::before,
        &::after {
            content: '';
            width: $circle-size;
            height: $circle-size;
            border-radius: 50%;
            display: block;
            position: absolute;
            background: #fff;
        }

        &:before {
            top: calc(-1*($circle-size/2));
            left: calc(-1*($circle-size/2));
        }

        &::after {
            top: calc(-1*($circle-size/2));
            right: calc(-1*($circle-size/2));
        }
    }

    .ticket-header {
        width: 100%;
        height: 70px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-weight: 800;
        font-size: 22px;

        &.family {
            background: #8a2be2;
            color: #fff;
        }

        &.other {
            background: #ff8c00;
            color: #fff;
        }
    }

    .ticket-middle {
        width: 100%;
        height: 40px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 800;
        font-size: 18px;

        &.entered {
            background: #FFDD57;
            color: $gray;
        }

        &.not_entered {
            background: #00d1b2;
            color: #fff;
        }

        &.disable {
            background: #FF3860;
            color: #fff;
        }
    }

    .ticket-body {
        padding: 20px;
        display: flex;
        flex-direction: column;
        gap: 6px;
        background: #e9e8e8;

        .ticket-title {
            font-size: 14px;
        }

        .first-guest {
            .first-guest-info {
                div {
                    font-size: 23px;
                    font-weight: 800;

                    span {
                        font-size: 16px;
                    }
                }
            }
        }

        .guests {
            ul {
                display: flex;
                flex-wrap: wrap;

                li {
                    width: calc(100%/2);
                    font-size: 19px;
                    font-weight: 600;

                    span {
                        font-size: 15px;
                    }
                }
            }
        }

        .st_info {
            .st_name {
                font-size: 19px;
                font-weight: 600;
            }

            .st_belong {
                font-size: 15px;
                font-weight: 600;
            }
        }

        .data-box {
            .data {
                font-size: 15px;
                font-weight: 600;
            }
        }
    }
}

ゲスト情報検索

文化祭で運用した際にチケットが表示できないゲストが複数発生したので、管理画面から名前でゲスト検索→入場処理ができるように実装しました。

ゲスト検索画面

名前でゲスト検索を行い、そのページから入退場処理のページへ遷移することができるようになりました。ゲスト名での検索は部分一致で調べられます。
検索対象はゲスト名以外にもメールアドレス(部分一致)やゲストID(完全一致)でも調べることができます。

部分一致のゲスト名検索はただのゲスト取得APIでは対応できず、ゲスト検索用で別にエンドポイントを用意しました。
エンドポイント/guests/search?type={search type}&query={search word}でAPIが叩けるようになっています。

チケットPDF生成機能

文化祭の運用では、メールが確認できずチケットを表示できないと事前に連絡を受けたゲストに対してチケット画像を貼り付けたPDFを印刷し、渡していました。
しかしその作業が怠すぎるということでこの機能を実装しました。

ゲスト検索画面からPDFをダウンロードすることができます。チケット画像以外にも氏名等も表示しています。


生成されるPDF

このPDF生成にはtcpdfを用いています。

ユーザーロール機能

ユーザーの管理にはロール機能を実装し、より細かな権限管理を可能にしました。

ロールってなんぞや

Discordのサーバーなどで見られる権限管理の仕組みのことです。
所有者や管理者は任意のロールを作成し、そのロールに権限を付与します。
次にロールをユーザーに付与することでユーザーは与えられたロールに許可されている権限が解放されます。
また1ユーザーに複数のロールを付与することもできます。

文化祭では教員や実行委員会、模擬店など様々なユーザーが想定され、固定化された権限管理では難しい場面が想定されます。そこでロール機能を用いると自由自在な権限管理ができるようになります。

以下が設定可能な権限一覧です。

  • 入退場処理可能の有無
  • ゲスト情報確認可能の有無
  • ゲスト情報検索可能の有無
  • アナリティクス閲覧可能の有無
  • ユーザーのロールの編集可能の有無
  • ユーザー削除可能の有無
  • ロール情報変更可能の有無
  • ユーザー招待可能の有無
  • ユーザー招待コード削除可能の有無
  • 展示情報変更可能の有無
  • ゲスト情報編集可能の有無

ロール自体の作成は設定のロール管理ページから、ロールの付与は設定のユーザー管理ページから行うことができます。

設定->ロール管理

設定->ユーザー管理

招待制ユーザー登録

イベントの規模が大きくなると登録するべきユーザー数が多くなっていきます。誰でも勝手にユーザー登録ができてしまうのもダメ、管理者が一括で設定するにはユーザー情報の柔軟性がなくなります。
ここで管理者が招待コードを発行し、その招待コードを用いてユーザー登録を行う方式をとりました。
また登録直後はほとんど権限がない状態で、管理者がロールを付与することで権限が解放されます。


設定->ユーザー招待

ユーザー新規登録サイト

アナリティクス

v1では申し訳程度の統計機能しか設けていませんでしたが、v2ではある程度しっかりと整備しました。各種統計を見ることができます。
(以下の画像は開発段階のものだが、何故か 現在入場者数<累計入場者数 となっている、謎すぎ(現在は正常に表示される))



アナリティクス

コンテスト感想

プログラミング関係のコンテストやイベントに参加するのは初めてで2割程度の緊張と8割の楽しみさで東京へ行きました。
あの一つの会場に入選者や審査員含めエンジニアばかりだと思うとなかなかすごい空間に居たのだなのこの記事を書きながら実感しています。
他の入選者の発表はどれも興味深く、自分が知らない分野もあったので新たな視点が得られました。あと小学生×2人が凄すぎて(いい意味で)絶句でした。
あとあまり話せませんでしたが、ネット上でお互い知っている人が居たりして世間は広いのか狭いのか不思議な感じでした。

当日まともに写真を撮っていなかったのでリハ時の写真を

最後に

Cracker v2ではv1の使用者側の問題点はかなり改善できたと思います。また新機能であるロールによる権限管理についてはそれっぽい機能で個人的に満足しています。
しかし開発者側から見た時の問題(セキュリティやDB関連など)はまだまだ存在します。

Crackerの開発はこれをもって一時休止するかと思いますが、今後はもっとフレームワークや新しい技術にも挑戦し、色々な開発を続けていこうと思います。

【謝辞】
U22プロコン受賞のきっかけとなったシステム開発を誘ってくれたK先生を始め、発表練習に付き合ってもらったツヨツヨエンジニアたち、応援してくれた先生やクラスメイト、その他僕がプログラミングをここまでできるようになった過程でお世話になった友人らや大人の人にこの場を借りて感謝したいと思います。本当にありがとうございます。
また文化祭システムの先駆者たち、インターネットの集合知を築き上げてきた先人達にもとても感謝しています。

Discussion