2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flask】入退管理システムを作ってみた①

2
Last updated at Posted at 2026-06-26

やりたいこと

  • 入退管理システムの開発
    • フォームに会員IDを入力
    • データベースで照合
    • 会員情報を記録

導入

Ubuntu-22.04
$ git colone https://github.com/masaki130/GymSystem.git
$ cd gym_system
$ python app2.py

フォルダ構成

Ubuntu-22.04
├── gym_system
    ├── __pycache__
       └── db.cpython-310.pyc
    ├── app2.py          # Flaskサーバ
    ├── db.py            # DB接続関数の定義
    ├── gym.db           # DB本体
    ├── init_db.py       # DB作成
    ├── schema.sql       # DBテーブル作成
    ├── seed.py          # DBに情報を登録
    └── templates
        └── index2.html  # UI

テーブルの作成

schema.sql
-- SQLiteに3つのテーブルを作成

-- ==========================
-- 会員テーブル
-- ==========================
CREATE TABLE IF NOT EXISTS members (                -- 無かったら作る
    member_id TEXT PRIMARY KEY,                     -- 文字列型, 主キー(一意のID)
    name TEXT NOT NULL,                             -- NOT NULL; 必須
    email TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP   -- 現在時刻
);

-- ==========================
-- 入退室履歴
-- ==========================
CREATE TABLE IF NOT EXISTS entry_log (
    log_id INTEGER PRIMARY KEY AUTOINCREMENT,               -- 自動で増える履歴番号
    member_id TEXT NOT NULL,
    action TEXT NOT NULL CHECK(action IN ('IN','OUT')),     -- 中と外を判別
    access_time DATETIME DEFAULT CURRENT_TIMESTAMP,         -- 入退室した時間
    FOREIGN KEY(member_id)                                  -- 外部キー, membersテーブルに存在するmember_idしか登録できない
        REFERENCES members(member_id)
);

-- ==========================
-- 現在の状態
-- ==========================
CREATE TABLE IF NOT EXISTS current_status (         -- 現在館内にいるか
    member_id TEXT PRIMARY KEY,
    is_inside INTEGER NOT NULL DEFAULT 0,           -- 初期値として0
    last_updated DATETIME,                          -- 現在の時刻
    FOREIGN KEY(member_id)
        REFERENCES members(member_id)
);

DBに接続するための関数

db.py
# gym.dbに接続する処理
import sqlite3
import os

DATABASE = "gym.db"

# 絶対パス
print(os.path.abspath(DATABASE))

# データベースに接続する処理
def get_connection():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row      # SQLiteから取得したデータを,列名でアクセスできるようにする設定

    return conn

DBの作成

init_db.py
# schema.sqlに書かれたSQLを実行して,gym.dbを初期化
# db.pyで作ったget_connection()関数を読み込む
from db import get_connection

def init_database():
    # gym.dbに接続(db.pyに定義済み)
    conn = get_connection()

    # schema.sqlを開く
    with open("schema.sql", "r", encoding="utf-8") as f:
        # 読み込んだ複数のSQLを一度に実行
        conn.executescript(f.read())

    conn.commit()   # gym.dbに変更を保存
    conn.close()    # gym.dbの接続を終了

    print("データベースを作成しました。")

# 直接実行した時のみ動く
if __name__ == "__main__":
    init_database()

DBに会員情報を登録

seed.py
# gym.dbに,テスト用の初期データを登録する処理
# db.pyで定義したget_connectionを読み込む
from db import get_connection

# 初期データを登録する処理
def seed_members():
    conn = get_connection()     # gym.dbに接続
    cursor = conn.cursor()      # pythonとsqlの仲介役

    # 会員データ
    members = [
        ("1001", "山田 太郎", "yamada@example.com"),
        ("1002", "佐藤 花子", "sato@example.com"),
        ("1003", "田中 次郎", "tanaka@example.com")
    ]

    # executemany;各会員分,同じSQLを複数回実行
    # ?;プレースホルダー, 取得した値が入る
    cursor.executemany("""
        INSERT OR IGNORE INTO members
        (member_id,name,email)
        VALUES (?,?,?)
    """, members)

    # 現在の状態(全員外にいる)
    current = [
        ("1001", 0),
        ("1002", 0),
        ("1003", 0)
    ]

    cursor.executemany("""
        INSERT OR IGNORE INTO current_status
        (member_id,is_inside)
        VALUES (?,?)
    """, current)

    conn.commit()   # gym.dbに変更を保存
    conn.close()    # gym.dbの接続を終了

    print("初期データを登録しました。")

if __name__ == "__main__":
    seed_members()

UIの作成

index2.html
<!DOCTYPE html>
<html>

<head>
    <title>入退室管理システム</title>
</head>

<body>

<h1>入退室管理システム</h1>

<!--波カッコはJinja2の記法-->
<h2>現在利用人数:{{ count }}人</h2>

<hr>

<!--入力されたデータをapp2.pyの/scanへPOST送信-->
<form action="/scan" method="POST">

    <label>会員ID:</label>
    <!--入力フォーム-->
    <input
        type="text"
        name="member_id"
        placeholder="例:1001"
        required>

    <!--ボタンを押すとフォームを送信-->
    <button type="submit">
        確定
    </button>

</form>

<hr>

<h2>会員一覧</h2>

<table border="1">  <!--表を作成-->

<tr>
    <th>ID</th>     <!--表の列名-->
    <th>名前</th>
</tr>

<!--Jinja2のループ文-->
{% for member in members %}     
<tr>
    <td>{{ member["member_id"] }}</td>      <!--表にデータを表示-->
    <td>{{ member["name"] }}</td>
</tr>
{% endfor %}

</table>

<hr>

<h2>最新の入退室履歴</h2>

<table border="1">

<tr>
    <th>日時</th>
    <th>名前</th>
    <th>状態</th>
</tr>

{% for log in logs %}

<tr>
    <td>{{ log["entry_time"] }}</td>
    <td>{{ log["name"] }}</td>
    <td>{{ log["action"] }}</td>
</tr>

{% endfor %}

</table>

</body>
</html>

アプリ本体

app2.py
# 管理システムのメイン処理
from flask import Flask, render_template, request, redirect, url_for
from db import get_connection
from datetime import datetime

# Webサーバを作成
app = Flask(__name__)

# http://127.0.0.1:5000/に接続後, index()を実行
@app.route("/")
def index():

    conn = get_connection()
    cursor = conn.cursor()      # cursor; pythonからsqlを操作可能

    # 現在人数をカウント
    cursor.execute("""
        SELECT COUNT(*)
        FROM current_status
        WHERE is_inside=1
    """)

    # 取得したsqlの結果からカウント数だけを取り出す
    count = cursor.fetchone()[0]

    # 会員一覧
    cursor.execute("""
        SELECT *
        FROM members
        ORDER BY member_id
    """)

    members = cursor.fetchall()

    # 最新履歴(JOIN; 表を結合, ORDER BY entry_time DESC; 新しい順, LIMIT; 20件に制限)
    cursor.execute("""
        SELECT
            entry_log.entry_time,
            members.name,
            entry_log.action
        FROM entry_log
        JOIN members
        ON entry_log.member_id=members.member_id
        ORDER BY entry_time DESC
        LIMIT 20
    """)

    logs = cursor.fetchall()
    conn.close()

    # htmlの表示(index2.htmlに値を渡す)
    return render_template(
        "index2.html",
        count=count,
        members=members,
        logs=logs
    )

# フォーム受信した時に実行
@app.route("/scan", methods=["POST"])

def scan():
    # フォームから送られたデータを取得
    member_id = request.form["member_id"]   # member_id; index2.html内で付けたid, 入力された会員番号が入る

    conn = get_connection()
    cursor = conn.cursor()

    # 会員確認(WHERE; 条件文)
    cursor.execute("""
        SELECT *
        FROM members
        WHERE member_id=?
    """, (member_id,))

    # データベースから1行を取得
    member = cursor.fetchone()

    # 空の時
    if member is None:
        conn.close()

        return "会員が存在しません"

    # 現在の状態を取得
    cursor.execute("""
        SELECT is_inside
        FROM current_status
        WHERE member_id=?
    """, (member_id,))

    # データベースから1行を取得
    status = cursor.fetchone()

    # 入退判定
    if status["is_inside"] == 0:
        action = "IN"
        inside = 1
    else:
        action = "OUT"
        inside = 0

    # 履歴追加(INSERT INTO; テーブルにデータを追加)
    cursor.execute("""
        INSERT INTO entry_log
        (member_id, action, entry_time)
        VALUES (?,?,?)
    """, (member_id, action, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))

    # 状態更新(改行してるだけで1つのSQL文)
    cursor.execute("""
        UPDATE current_status
        SET
        is_inside=?,
        last_updated=?
        WHERE member_id=?
    """, (
        inside,
        datetime.now(),
        member_id
    ))

    conn.commit()
    conn.close()

    # 最初の画面に戻る
    return redirect(url_for("index"))

if __name__ == "__main__":
    app.run(debug=True)

結果

image.png

今後の予定

  • QRコードやカードリーダーを使用した入退管理システムの開発
  • UIをカッコよくする
  • AIを搭載する
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?