Python技术迷

用 Python 写了一个电子考勤系统

每天早上上班打卡这事儿,你肯定也烦过。 我这边有段时间更离谱:公司的指纹打卡机老死机,HR 一着急就拉个微信群:“大家自己记一下时间,晚点我统一录系统。” 结果你也懂的,最后就变成一堆聊天记录截图 + Excel 地狱。

那段时间我干脆用 Python 弄了个简单的电子考勤系统,跑在一台闲置的小服务器上,同事手机点一下就能打卡,HR 直接导出报表。今天就把思路捋一下,顺带把核心代码贴出来,你照着改就能用。

别一上来就写代码,先把需求讲人话一点:

  1. 员工能“打卡”:上班/下班记录时间
  2. HR 能看报表:按人、按天看出勤情况
  3. 规则别太复杂:比如 9:00 之前算正常,之后算迟到,18:00 之前走算早退
  4. 部署简单:最好就是一个 Python 脚本 + 一个小 Web 服务,SQLite 存数据就够用

所以整个系统大致拆成三块:

  • 存储层:一张用户表,一张打卡记录表
  • 业务逻辑:打卡、算工时、判断迟到早退
  • 展示层:一个简单的 HTTP 接口 + 最简陋的 HTML 页面

用 SQLite 把数据先存起来

先别搞什么 MySQL、PostgreSQL,内网小工具,SQLite 是最省心的。

先设计两张表:

  • users:员工信息
  • attendance_records:打卡记录

用 Python 自带的 sqlite3 就够了:

# db.py
import sqlite3
from contextlib import contextmanager

DB_PATH = "attendance.db"

@contextmanager
defget_conn():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
try:
yield conn
        conn.commit()
finally:
        conn.close()

definit_db():
with get_conn() as conn:
        c = conn.cursor()
# 员工表
        c.execute(
"""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT NOT NULL UNIQUE,
                display_name TEXT NOT NULL
            )
            """

        )
# 打卡记录表
        c.execute(
"""
            CREATE TABLE IF NOT EXISTS attendance_records (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                date TEXT NOT NULL,          -- 2025-05-20
                check_in_time TEXT,          -- 09:01:02
                check_out_time TEXT,         -- 18:10:11
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL,
                UNIQUE(user_id, date),
                FOREIGN KEY(user_id) REFERENCES users(id)
            )
            """

        )

if __name__ == "__main__":
    init_db()
    print("数据库初始化完成")

这个结构很刻意地简单: 一人一天一行,进出时间都放同一行,后面统计会很舒服。

写两个最核心的函数:上班打卡、下班打卡

打卡逻辑其实就两件事:

  • 今天如果还没记录,就插入一条
  • 如果今天已经有记录,就只更新 check_in 或 check_out

顺手加一个获取/创建用户的工具函数:

# service.py
from datetime import datetime, date
from db import get_conn

WORK_START = "09:00:00"
WORK_END = "18:00:00"

defget_or_create_user(username: str, display_name: str = None) -> int:
if display_name isNone:
        display_name = username
with get_conn() as conn:
        c = conn.cursor()
        c.execute("SELECT id FROM users WHERE username = ?", (username,))
        row = c.fetchone()
if row:
return row["id"]
        c.execute(
"INSERT INTO users (username, display_name) VALUES (?, ?)",
            (username, display_name),
        )
return c.lastrowid

def_today_str() -> str:
return date.today().strftime("%Y-%m-%d")

def_now_time_str() -> str:
return datetime.now().strftime("%H:%M:%S")

def_now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

defcheck_in(username: str, display_name: str = None):
    user_id = get_or_create_user(username, display_name)
    today = _today_str()
    now_time = _now_time_str()
    now_full = _now_str()

with get_conn() as conn:
        c = conn.cursor()
        c.execute(
"SELECT id, check_in_time FROM attendance_records WHERE user_id=? AND date=?",
            (user_id, today),
        )
        row = c.fetchone()
if row:
# 已经有记录了,只更新上班时间
            c.execute(
"""
                UPDATE attendance_records
                SET check_in_time=?, updated_at=?
                WHERE id=?
                """
,
                (now_time, now_full, row["id"]),
            )
else:
# 新增一天记录
            c.execute(
"""
                INSERT INTO attendance_records (
                    user_id, date, check_in_time, check_out_time, created_at, updated_at
                ) VALUES (?, ?, ?, NULL, ?, ?)
                """
,
                (user_id, today, now_time, now_full, now_full),
            )

return {"user_id": user_id, "date": today, "check_in_time": now_time}

defcheck_out(username: str, display_name: str = None):
    user_id = get_or_create_user(username, display_name)
    today = _today_str()
    now_time = _now_time_str()
    now_full = _now_str()

with get_conn() as conn:
        c = conn.cursor()
        c.execute(
"SELECT id, check_out_time FROM attendance_records WHERE user_id=? AND date=?",
            (user_id, today),
        )
        row = c.fetchone()
if row:
            c.execute(
"""
                UPDATE attendance_records
                SET check_out_time=?, updated_at=?
                WHERE id=?
                """
,
                (now_time, now_full, row["id"]),
            )
else:
# 有人早上忘记打卡,直接下班打卡,也给他建一条记录
            c.execute(
"""
                INSERT INTO attendance_records (
                    user_id, date, check_in_time, check_out_time, created_at, updated_at
                ) VALUES (?, ?, NULL, ?, ?, ?)
                """
,
                (user_id, today, now_time, now_full, now_full),
            )

return {"user_id": user_id, "date": today, "check_out_time": now_time}

现在你在 Python 交互里跑一下:

from db import init_db
from service import check_in, check_out

init_db()
print(check_in("alice", "小艾"))
print(check_out("alice"))

就已经能往库里写各种打卡记录了。

怎么算“迟到早退”这种考勤结果?

光有原始时间,还看不出今天到底是正常还是异常。 简单粗暴一点:按固定上班 9:00、下班 18:00 来算。

写个小函数,把一天的数据算成“标签”:

# analysis.py
from datetime import datetime
from db import get_conn

WORK_START = "09:00:00"
WORK_END = "18:00:00"

def_time_obj(t: str):
return datetime.strptime(t, "%H:%M:%S").time()

defanalyze_one_day(username: str, target_date: str):
"""
    target_date: '2025-05-20'
    """

with get_conn() as conn:
        c = conn.cursor()
        c.execute(
"""
            SELECT ar.check_in_time, ar.check_out_time, u.display_name
            FROM attendance_records ar
            JOIN users u ON ar.user_id = u.id
            WHERE u.username=? AND ar.date=?
            """
,
            (username, target_date),
        )
        row = c.fetchone()

ifnot row:
return {
"date": target_date,
"username": username,
"status": "缺勤",
"detail": "当天没有任何打卡记录",
        }

    check_in_time = row["check_in_time"]
    check_out_time = row["check_out_time"]
    display_name = row["display_name"]

    status_list = []

ifnot check_in_time:
        status_list.append("未打上班卡")
else:
if _time_obj(check_in_time) > _time_obj(WORK_START):
            status_list.append("迟到")
else:
            status_list.append("正常上班")

ifnot check_out_time:
        status_list.append("未打下班卡")
else:
if _time_obj(check_out_time) < _time_obj(WORK_END):
            status_list.append("早退")
else:
            status_list.append("正常下班")

    status = ",".join(status_list)

return {
"date": target_date,
"username": username,
"display_name": display_name,
"check_in_time": check_in_time,
"check_out_time": check_out_time,
"status": status,
    }

比如:

from analysis import analyze_one_day

print(analyze_one_day("alice", "2025-05-20"))
# 可能返回:
# {
#   'date': '2025-05-20',
#   'username': 'alice',
#   'display_name': '小艾',
#   'check_in_time': '09:10:01',
#   'check_out_time': '18:20:10',
#   'status': '迟到,正常下班'
# }

规则你可以根据自己公司实际改,比如允许 10 分钟弹性,或者加个“加班”状态。

用 Flask 搭一个最简单的 Web 打卡页面

光在命令行打卡肯定不现实,大家都用手机。 这里用 Flask 搭一个超简单的 Web 接口:

  • GET / :展示一个输入名字 + 上班/下班按钮的页面
  • POST /checkin:上班打卡
  • POST /checkout:下班打卡
  • GET /report?username=xx&date=xx:看某天自己的记录
# app.py
from flask import Flask, request, render_template_string, jsonify
from db import init_db
from service import check_in, check_out
from analysis import analyze_one_day

app = Flask(__name__)

INDEX_HTML = """
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>简易考勤系统</title>
</head>
<body>
    <h3>简易考勤系统</h3>
    <form method="post" action="/checkin">
        <label>用户名:
            <input name="username" required>
        </label>
        <label>昵称(可选):
            <input name="display_name">
        </label>
        <button type="submit">上班打卡</button>
    </form>

    <br>

    <form method="post" action="/checkout">
        <label>用户名:
            <input name="username" required>
        </label>
        <button type="submit">下班打卡</button>
    </form>

    <br>

    <form method="get" action="/report">
        <label>用户名:
            <input name="username" required>
        </label>
        <label>日期(YYYY-MM-DD):
            <input name="date" required>
        </label>
        <button type="submit">查看当天记录</button>
    </form>

    {% if message %}
    <p style="color: green;">{{ message }}</p>
    {% endif %}

    {% if report %}
    <h4>考勤结果</h4>
    <pre>{{ report | safe }}</pre>
    {% endif %}
</body>
</html>
"""


@app.route("/", methods=["GET"])
defindex():
return render_template_string(INDEX_HTML)

@app.route("/checkin", methods=["POST"])
defweb_checkin():
    username = request.form.get("username")
    display_name = request.form.get("display_name") orNone
    result = check_in(username, display_name)
return render_template_string(
        INDEX_HTML,
        message=f"{username} 上班打卡成功:{result['check_in_time']}",
        report=None,
    )

@app.route("/checkout", methods=["POST"])
defweb_checkout():
    username = request.form.get("username")
    result = check_out(username)
return render_template_string(
        INDEX_HTML,
        message=f"{username} 下班打卡成功:{result['check_out_time']}",
        report=None,
    )

@app.route("/report", methods=["GET"])
defweb_report():
    username = request.args.get("username")
    date = request.args.get("date")
    result = analyze_one_day(username, date)
return render_template_string(
        INDEX_HTML,
        message=None,
        report=result,
    )

# 给前端或其他系统用的纯 JSON 接口
@app.route("/api/report", methods=["GET"])
defapi_report():
    username = request.args.get("username")
    date = request.args.get("date")
    result = analyze_one_day(username, date)
return jsonify(result)

if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)

把这个跑起来之后,内网机器访问 http://服务器IP:5000/,就能打卡和查记录了。

这个小系统还能怎么玩?

上面那一套其实就是一个“能用”的最小版本,真要往实际环境丢,还可以慢慢加东西:

  • 登录鉴权:简单点搞个固定 token,或者接公司现成的 SSO
  • 更灵活的班次:支持早班、晚班、不定时工时,打卡记录分多个时段
  • 导出 Excel 报表:直接用 pandas 把 SQLite 查出来的结果写成 xlsx 发给 HR
  • 统计维度:按人、按部门、按周/月统计迟到次数、加班时长
  • 地理位置:手机打卡可以顺带上传经纬度,简单做个“范围内打卡”

但不管怎么升级,底下这几个点基本是通用的:

  1. 用一张“原始打卡记录表”把事实记录下来
  2. 所有统计都是在原始数据之上做计算,别直接在表里写“状态”
  3. 业务规则(迟到早退、弹性时间)尽量都写在 Python 逻辑里,后期改规则不碰数据结构

大概就是这样,一个晚上能赶出来的小系统,却是真·把大家从截图+Excel 的泥潭里解救出来的那种。你可以先按这个版本跑起来,再按自己团队的情况一点点迭代。要是你后面想加人脸识别、门禁联动那种,再聊也不迟。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB