简介基于PythonFlask搭建的考勤管理系统是一份面向计算机专业毕业设计或课程设计的完整参考项目。项目已获得导师认可答辩评分97分作者在macOS及Windows 10/11环境下做过严格调试下载即可运行适合需要快速搭建考勤管理原型或学习Flask开发流程的学生借鉴。压缩包共15个文件大小仅16KB包含核心Python程序、SQLite数据库文件、Pipfile依赖与锁文件、Markdown说明文档以及项目配置类XML文件结构紧凑便于按目录查看源码、数据库和部署说明。已有125人学习下载。通过这套源码读者可以了解Flask路由与模型设计、考勤数据存储与查询逻辑以及毕业设计文档的组织思路同时部署教程齐全可作为课程设计的起点模板帮助节省环境配置和项目搭建时间。1. 先理顺考勤管理系统到底管哪几件事考勤管理这个选题在 PythonFlask 的毕设项目中几乎成了“标配”。但大多数交上来的代码只做到了“能打卡、能查列表”离高分评定真正看的“功能闭环”还差一截。拆开讲一套合格的考勤管理系统源码应当构成三个闭环员工信息与角色权限的闭环、上下班打卡与迟到早退判定的闭环、月末把出勤记录聚合成统计报表的闭环。Flask 适合这个选题不是因为它比 Django 功能全而是因为它的请求-响应模型恰好能把这几个闭环拆成清晰的路由和模型代码量可控、逻辑外露评审核验时也容易讲清楚“哪段代码做了哪件事”。下面直接按数据模型、视图逻辑、部署排错三条线展开。2. 为什么用 Flask 做考勤选型理由与工程骨架2.1 轻量优先Flask 与考勤系统的三个匹配点考勤系统的实际规模不大员工表几千行、考勤记录每天每人大约 1 到 2 条、页面数量在十个左右。这种量级下Flask 比 Django 更合适原因有三点。路由直接对应业务动作打卡、查询、统计的接口一眼能看出来是什么功能不依赖框架层的自动生成。ORM 层用 Flask-SQLAlchemy 挂在 SQLite 上毕设环境零配置就能跑通不用单独装数据库服务。模板渲染与 JSON 接口可以共存后台管理页面用 render_template下拉刷新和统计报表用 jsonify切换成本低。毕设评审看重的是“你写的代码能不能讲清楚”Flask 的方式是把业务逻辑摆在明面。下面这张表是 Flask 组件与我通常构建考勤系统时使用的模块对应关系。Flask 组件在考勤管理系统中承担的作用路由与视图打卡、签到记录查询、请假申请、月度统计request 与 form接收前端提交的员工 ID、打卡动作、筛选条件session维系登录状态区分管理员和普通员工两种角色模板引擎渲染考勤日历、统计报表、员工列表页面SQLAlchemy定义员工、考勤记录、请假三张核心表2.2 请求-响应骨架从一个最小打卡视图说起Flask 开发的起点是理解一个请求从浏览器进来之后怎么走。员工点击“上班打卡”前端以 POST 方式把员工 ID 和动作类型发给后端后端视图函数取出参数写一条记录返回 JSON。下面这段是一个最小可用的打卡视图先把骨架立起来。from flask import Flask, request, jsonify from datetime import datetime app Flask(__name__) app.route(/check_in, methods[POST]) def check_in(): # 从前端表单中取出员工编号 employee_id request.form.get(employee_id) now datetime.now() return jsonify({ code: 0, message: ok, data: { employee_id: employee_id, time: now.strftime(%Y-%m-%d %H:%M:%S) } })这段代码里有三个关键点。request.form.get取的是application/x-www-form-urlencoded表单数据如果前端用 JSON 格式提交就要换成request.get_json()。datetime.now()在视图里每次调用都会取系统当前时间考勤系统对时间精度敏感这个位置就是后面要重点处理的时区入口。jsonify会把字典转成带正确Content-Type的响应体前端拿到后就能直接渲染。这个骨架看起来简单但它决定了整个考勤系统的交互方式所有打卡动作都是“前端提交参数、后端校验并写库、返回 JSON 状态码”。后续的迟到判定、重复打卡校验都是在return jsonify之前插入逻辑。2.3 用蓝图拆模块把管理员和员工的路由分开所有路由都堆在app.py里做到第五个视图就会乱。我一般会在一开始就用蓝图把角色拆开管理员的一组路由管全量记录和统计员工的一组路由管打卡和查看自己的记录。from flask import Blueprint # 管理员蓝图所有接口前缀是 /admin admin_bp Blueprint(admin, __name__, url_prefix/admin) # 员工蓝图所有接口前缀是 /employee emp_bp Blueprint(employee, __name__, url_prefix/employee) admin_bp.route(/records) def records(): # 管理员查看所有考勤记录后续在这里加过滤和分页 return all records emp_bp.route(/my_records) def my_records(): # 员工只能看自己的考勤记录需要从 session 取当前登录人 return my records蓝图注册到应用实例时只需要执行app.register_blueprint(admin_bp)和app.register_blueprint(emp_bp)。url_prefix是路由前缀它保证了两组接口在 URL 层面就不会撞车。更重要的是这里的边界替后续权限控制铺了路在管理员蓝图里可以单独加一段判断确认当前会话的role字段是否为admin。从这章可以看出Flask 开发考勤系统的核心不在于框架本身而在于通过蓝图层把“谁能做什么”先规定死。角色边界清晰了数据模型和统计逻辑才能往下落。3. 考勤系统数据模型三张核心表怎么设计3.1 员工表、签到表、请假表字段与关系考勤系统的数据模型是整个项目的地基。建表之前先回答一个问题哪些数据需要“存下来”哪些数据可以“现算”。员工的基本信息要存每天的打卡时间要存请假单要存而迟到次数、出勤天数这类统计指标不应该存成字段而是月末聚合时实时计算。表名核心字段作用employeeemp_no、name、department、on_duty_time、off_duty_time员工基础信息与上下班时间窗口attendanceemployee_id、work_date、check_in_time、check_out_time、status每人每天一条打卡记录leaveemployee_id、leave_type、start_date、end_date、reason请假登记用于月度出勤扣除这里有一个容易出错的设计点加班和跨天班次是否要单独建表。如果只做校内考勤、固定时间上下班三张表足够一旦出现晚上 22 点上班、次日 6 点下班的夜班work_date按哪一天归属就说不清了。我一般会先按“当天 0 点到 24 点”划为一天跨天班次用单独字段shift_type标记而不是把模型复杂化。毕业设计阶段先把三张表跑通比一开始就上多态继承稳妥得多。另一个关键决定是status字段的类型。不建议用字符串直接存“迟到”“早退”应当用SmallInteger存状态码在 Python 侧写一个映射表。原因有两个数据库层面整数比较比字符串快后续要统计迟到次数时func.sum(Attendance.status 1)这类聚合写法更干净。状态码映射我一般定义成这样# 0 正常, 1 迟到, 2 早退, 3 缺卡 STATUS_MAP {0: 正常, 1: 迟到, 2: 早退, 3: 缺卡}3.2 时间字段的坑存 DateTime 而不是字符串考勤记录里最容易埋坑的是时间字段。打卡时间要存DateTime这个大家基本不会错但work_date和check_in_time的区别需要想清楚。work_date是业务日期表示这条记录属于哪一天类型用Datecheck_in_time是打卡的具体时刻类型用DateTime。两者分开存是因为后续查询经常用“某一天的所有打卡记录”作过滤条件如果把日期也塞进 DateTime就必须写func.date(check_in_time) target_date这类函数索引会失效。时区问题也在这一步暴露。datetime.now()取的是运行 Flask 所在机器的本地时区而如果部署环境是 UTC 时区存进数据库的打卡时间会比实际晚 8 小时。毕设部署时查“今天”的记录会发现早上 8 点打的卡被算到了前一天。我处理这个问题的方式是统一在应用入口处固定时区from datetime import datetime class LocalTimezone: # 固定使用东八区的当前时间避免服务器时区影响打卡记录 staticmethod def now(): return datetime.now().astimezone()写数据库前调用LocalTimezone.now()而不是裸调datetime.now()。这一步能省掉后续排错的一大半时间。3.3 SQLAlchemy 模型代码建表与初始化模型层我通常写在models.py中用 Flask-SQLAlchemy 定义三张表。下面是一份可以直接运行的模型示例from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Employee(db.Model): __tablename__ employee id db.Column(db.Integer, primary_keyTrue) emp_no db.Column(db.String(20), uniqueTrue, nullableFalse) # 工号唯一 name db.Column(db.String(20), nullableFalse) department db.Column(db.String(50), nullableFalse) on_duty_time db.Column(db.String(5), default09:00) # 上班时间 off_duty_time db.Column(db.String(5), default18:00) # 下班时间 class Attendance(db.Model): __tablename__ attendance id db.Column(db.Integer, primary_keyTrue) employee_id db.Column(db.Integer, db.ForeignKey(employee.id), nullableFalse) work_date db.Column(db.Date, nullableFalse) # 业务日期 check_in_time db.Column(db.DateTime) # 上班打卡时刻 check_out_time db.Column(db.DateTime) # 下班打卡时刻 status db.Column(db.SmallInteger, default0) # 0正常 1迟到 2早退 3缺卡 # 同一员工同一天只能有一条记录这个约束是防重复打卡的最后一道防线 __table_args__ (db.UniqueConstraint(employee_id, work_date, nameuniq_emp_date),) class Leave(db.Model): __tablename__ leave id db.Column(db.Integer, primary_keyTrue) employee_id db.Column(db.Integer, db.ForeignKey(employee.id), nullableFalse) leave_type db.Column(db.String(20), nullableFalse) start_date db.Column(db.Date, nullableFalse) end_date db.Column(db.Date, nullableFalse) reason db.Column(db.String(200))模型建好后初始化数据库的操作是在 Flask 应用上下文里执行db.create_all()然后插入几条测试员工数据。这里有三处定义值得注意uniqueTrue保证工号不重复UniqueConstraint约束保证员工与日期的组合唯一这是防止同一天重复写入记录的数据层保障DateTime字段留空值None用于表示“已上班但未下班”的状态统计时要把这类记录当作未完成处理。4. 把考勤逻辑写进视图打卡、查询、统计怎么落地4.1 打卡路由迟到早退判定的时间窗口模型定好后接下来把业务规则翻译成视图代码。打卡逻辑分为两步先查当天是否已有记录有则更新下班时间没有则新建一条上班记录。先取记录再决定 insert 还是 update这是为了照顾“早上打一次卡、下午再打一次”的使用场景。emp_bp.route(/check, methods[POST]) def check(): # 从登录会话取员工对象而不是信任前端传来的员工 ID emp Employee.query.get(session.get(emp_id)) if not emp: return jsonify({code: 401, message: 未登录}) today LocalTimezone.now().date() record Attendance.query.filter_by(employee_idemp.id, work_datetoday).first() if not record: record Attendance(employee_idemp.id, work_datetoday) db.session.add(record) action request.form.get(action) # 前端传 sign_in 或 sign_out now_time LocalTimezone.now() if action sign_in: if record.check_in_time is not None: return jsonify({code: 1, message: 今日已打过上班卡}) record.check_in_time now_time # 当前时间晚于设定上班时间则标记迟到 if now_time.strftime(%H:%M) emp.on_duty_time: record.status 1 elif action sign_out: if record.check_out_time is not None: return jsonify({code: 1, message: 今日已打过下班卡}) record.check_out_time now_time # 只有正常状态才需要额外判断早退已迟到就不再叠加状态 if record.status 0 and now_time.strftime(%H:%M) emp.off_duty_time: record.status 2 db.session.commit() return jsonify({code: 0, message: ok})这段逻辑中session.get(emp_id)是从服务端会话取当前登录人不信任前端传参这是防止越权打卡的底线。比较时间用的是字符串HH:MM大小比较因为emp.on_duty_time本身就是09:00格式字符串比较结果与时间先后一致省去一次datetime.strptime转换。状态叠加规则是“迟到优先于早退”一天的状态只记录一次避免同时出现“迟到又早退”的二元判断。这里暴露了一个常见设计分歧同一天既迟到又早退应当单独记录还是合并成一个状态合并成一个是简化精确统计需要拆成两个布尔字段或把 status 设计成位标记。毕设项目建议合并后续统计口径更好讲。4.2 考勤查询按日期过滤与分页管理端查看考勤记录需要支持按日期筛选和分页展示。Flask-SQLAlchemy 的paginate方法免去了手动计算 offset 的麻烦直接返回分页对象前端拿到数据后渲染表格。admin_bp.route(/attendance) def attendance_list(): page request.args.get(page, 1, typeint) work_date_str request.args.get(work_date, ) query Attendance.query if work_date_str: # 把前端传来的字符串转成 Date 对象再过滤 target_date datetime.strptime(work_date_str, %Y-%m-%d).date() query query.filter_by(work_datetarget_date) pagination query.paginate(pagepage, per_page10, error_outFalse) rows [] for r in pagination.items: rows.append({ emp_no: r.employee.emp_no, name: r.employee.name, work_date: str(r.work_date), check_in: r.check_in_time.strftime(%H:%M:%S) if r.check_in_time else --, check_out: r.check_out_time.strftime(%H:%M:%S) if r.check_out_time else --, status: STATUS_MAP.get(r.status, 未知) }) return jsonify({total: pagination.total, page: page, rows: rows})error_outFalse的作用是当页码超出范围时返回空列表而不是抛 404这个参数在演示时很实用评审乱点也不会报错。r.employee是通过 SQLAlchemy 的外键关系自动联查出来的前提是模型里定义了relationship否则要自己写二次查询。如果不需要分页而是导出 CSV把pagination.items换成query.all()就行。4.3 月度统计聚合查询而不是循环累加月度统计是考勤系统的“高分点”也是这里最容易写出低效代码的部分。常见错误是先查出一个月所有记录然后放进 Python 循环里数迟到次数。正确做法是把聚合逻辑下推到数据库用group_by配合条件聚合一次性算完。from sqlalchemy import func admin_bp.route(/summary/int:year/int:month) def monthly_summary(year, month): month_start date(year, month, 1) if month 12: month_end date(year 1, 1, 1) else: month_end date(year, month 1, 1) stats db.session.query( Employee.department, func.count(Attendance.id).label(total_days), func.sum(db.case((Attendance.status 1, 1), else_0)).label(late_count), func.sum(db.case((Attendance.status 2, 1), else_0)).label(early_count) ).join(Attendance, Attendance.employee_id Employee.id)\ .filter(Attendance.work_date month_start, Attendance.work_date month_end)\ .group_by(Employee.department).all() result [{ department: dept, total_days: total, late: late, early: early } for dept, total, late, early in stats] return jsonify(result)db.case是 SQLAlchemy 里的条件表达式等价于 SQL 语句中的CASE WHEN status 1 THEN 1 ELSE 0 END配合func.sum就能统计出一列中满足条件的条数。这段代码把部门、总出勤天数、迟到次数、早退次数一次查出来性能上远优于在 Python 里循环。如果之后要加上“请假天数”把Leave表按员工关联进来再按日期区间做条件过滤即可。4.4 会话保持与防重复打卡的边界条件打卡接口看似完整但还差一层“登录会话”的保护。所有员工相关蓝图在进入视图前都应验证登录状态通常用before_request钩子统一处理。from flask import session, jsonify emp_bp.before_request def check_login(): if emp_id not in session: return jsonify({code: 401, message: 请先登录})before_request会在该蓝图下每个视图被调用前执行返回非None值时直接作为响应返回不再进入视图函数。这段逻辑保证了未登录用户无法通过直接拼 URL 访问打卡接口。双保险之外还要处理一个演示时常被问到的问题员工在 09:00 打了上班卡马上又打下班卡。这显然不符合真实场景。我一般会在sign_out分支里增加最小工作时长校验比如上班打卡 5 分钟内不允许打下班卡。5. 部署与排错这 6 个坑在评审演示前必须踩掉5.1 最容易翻车的运行时参数考勤系统跑到演示阶段代码逻辑通常已经没大问题翻车往往翻在环境配置上。下面这张表是我每次上真机演示前会逐项检查的清单。运行时问题典型现象处理方式SECRET_KEY 缺失session 报错或刷新后登录失效在应用初始化处设置随机字符串从环境变量读取时区未固定打卡时间比实际晚 8 小时统一使用固定时区的datetime获取当前时间模板缓存过期改完代码页面没变化调试模式下关闭模板缓存渲染时传入cache_timeout0SQLite 并发写冲突两台设备同时打卡报database is locked演示时保持单写入端生产环境切换 MySQL外键关系未定义查询r.employee报空引用在 Employee 模型中用db.relationship关联 Attendance浏览器缓存旧页面修改后的 JS 不生效模板中用url_for引用静态文件让 Flask 自动带版本参数第 2 项“时区未固定”是考勤系统独有的坑其他项目差几小时无所谓考勤打卡差 8 小时就是统计错误必须单独处理。第 4 项 SQLite 的并发限制在毕设场景下其实影响不大但要能在被问到时说出为什么安全SQLite 写锁是库级锁打卡操作集中在同一瞬间才会冲突演示规模下几乎不会触发。5.2 一条命令启动局域网演示环境评审现场最忌讳让所有人都挤在一台电脑前面看屏幕。更合适的做法是让 Flask 监听所有网卡然后用手机或另一台电脑访问。flask --app app.py run --host0.0.0.0 --port5000 --debug--host0.0.0.0让服务监听所有网络接口而不仅仅是 localhost--debug开启调试模式代码改动后自动重载服务。启动成功后在同一个局域网内的设备上访问http://电脑的局域网IP:5000就能看到系统。验证方式也很直接用两部设备分别登录管理员和员工的测试账号一边打卡一边刷新统计页面确认迟到状态和月度计数都跟着变化。最后的演示技巧是提前把work_date的测试数据调整到本月否则统计页面上数字全部为 0缺少说服力。本文还有配套的精品资源点击获取