传统指纹打卡机故障频发,导致考勤数据混乱、HR统计困难。本文将分享如何利用Python快速开发一个部署简单、功能清晰的轻量级电子考勤系统。该系统使用Flask框架提供Web服务,SQLite存储数据,员工可通过手机网页打卡,HR可便捷查看报表。
需求分析与系统设计
先明确核心需求,避免过度设计:
- 打卡记录:员工可记录上下班时间。
- 报表查看:HR可按人、按天查询出勤情况。
- 考勤规则:支持简单的迟到(如9:00后)、早退(如18:00前)判断。
- 部署简易:单Python脚本+轻量Web服务,使用SQLite作为数据库。
系统架构分为三层:
- 存储层:用户表、打卡记录表。
- 业务层:打卡逻辑、工时计算、状态判断。
- 展示层:HTTP接口与简易HTML页面。
数据存储设计:使用SQLite
对于内网小工具,SQLite是无需独立服务、零配置的最佳选择。设计两张核心表:
users:存储员工基本信息。
attendance_records:存储每日打卡记录。设计为“一人一天一条记录”,将上班(check_in_time)和下班时间(check_out_time)放在同一行,便于后续统计。
使用Python标准库sqlite3进行初始化:
# db.py
import sqlite3
from contextlib import contextmanager
DB_PATH = “attendance.db”
@contextmanager
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
finally:
conn.close()
def init_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(“数据库初始化完成”)
核心业务逻辑实现
打卡的核心逻辑是:如果当天没有记录则插入新行,已有记录则更新对应的时间字段。
1. 员工获取与创建
# service.py
from datetime import datetime, date
from db import get_conn
def get_or_create_user(username: str, display_name: str = None) -> int:
if display_name is None:
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
2. 上班打卡与下班打卡
# service.py (续)
WORK_START = “09:00:00”
WORK_END = “18:00:00”
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”)
def check_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}
def check_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()
def analyze_one_day(username: str, target_date: str):
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()
if not 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 = []
if not check_in_time:
status_list.append(“未打上班卡”)
else:
if _time_obj(check_in_time) > _time_obj(WORK_START):
status_list.append(“迟到”)
else:
status_list.append(“正常上班”)
if not 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
result = analyze_one_day(“alice”, “2025-05-20”)
# 可能返回:{‘date’: ‘2025-05-20’, ‘username’: ‘alice’, … , ‘status’: ‘迟到,正常下班’}
使用Flask构建Web界面
为了让员工通过手机便捷访问,使用轻量级Web框架 Flask快速搭建服务。
# 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”])
def index():
return render_template_string(INDEX_HTML)
@app.route(“/checkin”, methods=[“POST”])
def web_checkin():
username = request.form.get(“username”)
display_name = request.form.get(“display_name”) or None
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”])
def web_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”])
def web_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”])
def api_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)
运行app.py后,在内网通过浏览器访问 http://服务器IP:5000/ 即可使用。
系统扩展思路
以上是一个可用的最小版本,后续可根据实际需求迭代:
- 身份认证:集成公司SSO或使用简单的Token鉴权。
- 灵活班次:支持早班、晚班等不同工时制度。
- 数据报表:使用pandas库将查询结果导出为Excel文件。
- 多维统计:按部门、按月统计迟到早退次数、加班时长等。
- 位置校验:打卡时附加GPS位置信息,确保在指定范围内。
开发此类系统的通用原则是:
- 用一张“原始记录表”忠实记录所有打卡事实。
- 所有统计和状态判断均在应用层逻辑中完成,便于后期规则调整。
- 保持核心数据模型稳定,业务规则变化不影响底层存储。
本系统基于 Python 生态快速构建,代码简洁,部署方便,能有效替代不稳定的硬件打卡机与繁琐的线下统计流程,是一个值得尝试的内部工具解决方案。