用 Python 写了一个电子考勤系统
每天早上上班打卡这事儿,你肯定也烦过。 我这边有段时间更离谱:公司的指纹打卡机老死机,HR 一着急就拉个微信群:“大家自己记一下时间,晚点我统一录系统。” 结果你也懂的,最后就变成一堆聊天记录截图 + Excel 地狱。
那段时间我干脆用 Python 弄了个简单的电子考勤系统,跑在一台闲置的小服务器上,同事手机点一下就能打卡,HR 直接导出报表。今天就把思路捋一下,顺带把核心代码贴出来,你照着改就能用。
别一上来就写代码,先把需求讲人话一点:
员工能“打卡”:上班/下班记录时间 HR 能看报表:按人、按天看出勤情况 规则别太复杂:比如 9:00 之前算正常,之后算迟到,18:00 之前走算早退 部署简单:最好就是一个 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统计维度:按人、按部门、按周/月统计迟到次数、加班时长 地理位置:手机打卡可以顺带上传经纬度,简单做个“范围内打卡”
但不管怎么升级,底下这几个点基本是通用的:
用一张“原始打卡记录表”把事实记录下来 所有统计都是在原始数据之上做计算,别直接在表里写“状态” 业务规则(迟到早退、弹性时间)尽量都写在 Python 逻辑里,后期改规则不碰数据结构
大概就是这样,一个晚上能赶出来的小系统,却是真·把大家从截图+Excel 的泥潭里解救出来的那种。你可以先按这个版本跑起来,再按自己团队的情况一点点迭代。要是你后面想加人脸识别、门禁联动那种,再聊也不迟。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB