尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

考勤系统开发实战:原型、数据库与源码三件套

考勤系统开发实战:原型、数据库与源码三件套 简介这是一套考勤登记管理系统的完整项目资料包适合正在做课程设计、毕业设计或初入职场的开发人员参考。资源同时提供前后端实现源码、可交互原型以及数据库脚本能够帮助学习者快速理解考勤业务从页面设计到数据存储的完整链路。压缩包共4个文件主要包含sql与bak两种数据库备份文件分别用于还原数据表结构与初始记录pdf用于查看整体设计方案和流程说明zip内为可直接运行或二次开发的项目源码。据后台统计已有716人学习下载。通过这套资料读者可以拿到数据库建表脚本、系统原型图以及核心功能实现代码便于对照学习打卡登记、请假审批、出勤统计等常见模块的开发方式也能减少从零搭建环境的时间成本。整体包体仅963KB文件组织精简适合快速部署和本地调试。1. 考勤登记管理系统的“源码原型数据库”三件套能帮你省掉什么接手一套考勤登记管理系统最常见的交付形态就是标题里这三个词源码、原型、数据库。源码管业务逻辑原型管页面和流程对齐数据库管时间、人和状态。这套组合能解决的问题很直接——中小团队内部考勤、外包项目的快速交付、还有课程设计里永远不缺的“考勤系统”题目。反直觉的一点是代码反而是三件套里最省事的部分真正让项目返工的是数据库字段和原型里的状态机没对齐。这篇文章按我自己做考勤系统的顺序讲先画原型再定库最后写接口顺带把几个翻车点摆出来。全程不依赖特定框架逻辑换 Java、PHP 都成立。2. 先画原型再写代码考勤系统的页面流转与数据字典怎么定2.1 为什么考勤系统必须先画原型再建表考勤登记管理系统看起来需求清晰实际是“每个人脑子里都有一套”的典型业务。上班时间几点算迟到、迟到几分钟内不算、补卡要不要审批、请假按小时还是按天扣——这些规则不定清楚数据库表结构一定改到怀疑人生。我见过最离谱的案例是开发直接建表上线两周后产品说要支持“一天打两次上班卡”的弹性班次整张考勤记录表推倒重来。所以现在我做这类系统第一件事永远是画原型。原型的价值不是给领导看界面而是把页面字段、按钮、状态变化画出来让所有人对着同一组页面说话。画原型时重点标出三类内容输入字段打卡时间、工号、请假事由、展示字段出勤天数、迟到次数、状态值正常、迟到、早退、缺卡、待审批。这三类字段列齐全数据库的表结构基本就定了七八成。原型设计阶段花三天数据库设计阶段就能省三个星期。2.2 原型页面清单六张页面把角色和流程钉死考勤系统按角色切最少六张页面能跑通主流程。我做原型时不会一上来画高保真先用一张页面清单把每个页面的角色、核心字段、跳转关系列出来确认无误再动手画图。页面清单长这样页面角色核心字段状态流转登录页全员工号、密码登录成功进入打卡页或管理页打卡页员工打卡按钮、当前时间、班次、打卡结果正常 / 迟到 / 早退 / 重复打卡考勤记录页员工、管理员日期范围、上下班时间、状态按天展示可筛选月份请假申请页员工假期类型、开始时间、结束时间、时长、事由待审批 → 通过 / 驳回请假审批页管理员申请单列表、审批意见通过 / 驳回统计页管理员月份、出勤天数、迟到次数、请假时长汇总报表可导出这张清单里最容易被漏掉的是“重复打卡”的状态。很多考勤系统原型只画了“打上班卡 → 打下班卡”的直线流程没考虑员工手滑多打了一次、或者早上打了卡又出去再回来。我在打卡页原型上会专门加一个提示区当天该类型已打卡时页面显示“如需修改请走补卡流程”而不是静默失败。2.3 打卡异常流和补卡流原型里最容易漏的状态机正常打卡流程谁都会画真正体现原型功底的是异常流。考勤状态我一般拆成五态正常、迟到、早退、缺卡、请假。其中缺卡不是人手动选的是系统在日终批处理时自动算出来的——当天只有一条上班卡没有下班卡状态就置为缺卡。这个逻辑必须在原型里写清楚否则开发会做成“状态手动改”。补卡流程是另一个高频踩坑点。原型里补卡不能设计成“直接改记录”否则员工可以自己把迟到改成正常。我一般这样设计打卡页提供“补卡申请”入口提交后生成一条补卡单状态为待审批管理员审批通过后才把考勤记录的状态修正。补卡单上要带原始打卡时间和补卡原因审批页能看到这两条信息。这套流程对应数据库里的补卡记录表和审批状态字段原型不画这一步数据库就少一张表。2.4 从页面字段到数据字典这一步决定数据库会不会返工原型确认后我会把每张页面的字段整理成数据字典一个页面字段对应一个表的字段。比如打卡页的“打卡时间”对应考勤记录表的 clock_time“打卡结果”对应 status 字段“班次”在原型里如果出现下拉框数据库就必须有考勤规则表或班次表。整理的时候我会用一张字段对照表把页面原型区域的字段名、数据库表名、字段类型、是否必填写在一起发给开发评审。数据字典里最容易漏的是“操作人”和“操作时间”。考勤记录谁录的、什么时候录的审批单谁批的、什么时候批的这些问题在原型上不体现到对账的时候才发现查不出来。所以我在每张业务表里都会预留 created_at、updated_at在审批表里额外加 approver_id 和 approve_time。这四个月段字段在原型阶段就要写进数据字典不要等上线后补。3. 数据库设计考勤登记系统的表结构、索引与时区选型3.1 事件流水表 vs 天级宽表考勤数据存哪里更合理考勤系统的表结构设计第一个岔路口就是选“流水表”还是“宽表”。宽表是一天一行字段里直接放上班时间、下班时间查询汇总时很直观流水表是每个动作一行上班打卡插一条、下班打卡插一条补卡再插一条。我第一次做考勤系统选了宽表后来支持补卡和弹性班次时改到崩溃——宽表一旦要记“第二次上班卡”要么加字段要么改造成子表哪种都别扭。现在我做考勤登记一律用事件流水表。考勤本质是事件流上班是一个事件下班是一个事件补卡是另一个事件。流水表核心字段就三个谁、哪一天、什么类型再加一个打卡时间。请假、外勤、补卡都能往里面插记录不需要改表结构。代价是汇总时要多做一次行转列或条件聚合但对现代数据库来说这点计算量可以忽略。经验是业务状态越多、异常类型越杂越应该选流水表。3.2 三张核心表与用户表的建表SQL数据库最少四张表用户表、考勤记录表、请假表、补卡表。用户表管员工和角色考勤记录表存流水请假表存请假单补卡表存补卡单。先看用户表和考勤记录表的建表语句CREATE TABLE sys_user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL COMMENT 工号, emp_name VARCHAR(50) NOT NULL COMMENT 姓名, dept_id INT UNSIGNED DEFAULT 0 COMMENT 部门ID, role TINYINT NOT NULL DEFAULT 2 COMMENT 1管理员 2员工, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 0离职, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE att_record ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, employee_id INT UNSIGNED NOT NULL COMMENT 用户ID, work_date DATE NOT NULL COMMENT 考勤日期, clock_type TINYINT NOT NULL COMMENT 1上班 2下班 3补卡, clock_time DATETIME NOT NULL COMMENT 实际打卡时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1迟到 2早退 3缺卡, source TINYINT NOT NULL DEFAULT 1 COMMENT 1打卡机 2手动录入, remark VARCHAR(255) DEFAULT COMMENT 备注, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_date_type (employee_id, work_date, clock_type), KEY idx_emp_date (employee_id, work_date), KEY idx_work_date (work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;逻辑说明att_record 表用 clock_type 区分上下班和补卡每条记录是一个独立事件。status 字段在插入时就计算好后面汇总直接累加避免月底大量重算。注意唯一索引 uk_emp_date_type这是防重复打卡的关键后面避坑章节会细说。参数说明employee_id 用整型关联 sys_user.id不用工号做外键因为工号可能变更而 ID 不会变clock_time 用 DATETIME 而不是 TIMESTAMP原因见 3.4。请假表和补卡表是同一套审批结构可以合成一张流程表也可以拆两张。我习惯拆开避免一张表里混两种业务导致查询条件复杂。请假表至少要有开始时间、结束时间、请假类型、时长小时、原因、审批状态、审批人、审批时间。补卡表则要带原始打卡时间和补卡原因。3.3 索引设计唯一索引是防重复打卡的第一道闸索引设计决定了考勤系统在数据量上来后还能不能流畅查。考勤记录表的查询模式基本固定查某人某月、查某部门某天、统计某月异常次数。所以索引不是越多越好而是对着查询模式建。我维护三条索引就够唯一索引 uk_emp_date_type 同时承担防重和按员工按日期查两张用途普通索引 idx_emp_date 用于按月范围查询idx_work_date 用于全局按天查询。插入时的判重就靠 uk_emp_date_type。打卡接口执行 INSERT 时如果同一个人同一天同一类型已经存在数据库直接报 Duplicate Entry代码捕获后返回“已打卡”提示。这里有个并发细节先 SELECT 再 INSERT 在高并发下存在竞态两个人同时提交时两个 SELECT 都查不到然后两个 INSERT 都成功。解决办法是让唯一索引做最终防线代码层先查一次只是为了友好提示真正拦截靠索引。补卡记录也要纳入同一套唯一键否则会出现一天两条类型为 3 的补卡记录。3.4 时间字段与时区DATETIME、TIMESTAMP和连接串参数考勤系统的时间字段最容易翻车。业务上我们要的是“打卡那一刻的本地时间”不关心时区转换。TIMESTAMP 类型在 MySQL 中会按会话时区做转换一旦应用服务器和数据库服务器的时区不一致查出来的时间可能差 8 小时。我统一用 DATETIME 存业务时间DATETIME 不做任何时区转换存什么就是什么适合考勤这种强本地属性的业务。创建时间和更新时间也建议用 DATETIME配合 DEFAULT CURRENT_TIMESTAMP 使用。数据库连接串里的时区参数必须显式指定。以 MySQL 为例JDBC 连接串要加 serverTimezoneAsia/Shanghai用 Python 的 SQLAlchemy 则在 URL 里加 charsetutf8mb4并在 engine 创建时传 timezone 相关配置。SQLite 没有时区问题本地演示可以用它但正式环境我建议直接上 MySQL 或 PostgreSQL因为考勤汇总语句里用到的日期函数在各数据库行为不同。另一个坑是 MySQL 的 sql_mode 里如果有 NO_ZERO_DATE插入“0000-00-00”会直接报错初始化数据时要注意默认时间不要写成零值日期。4. 源码落地Flask 实现考勤打卡和月度汇总4.1 环境与依赖轻量后端 连接池代码实现我选用 Flask SQLAlchemy 做演示因为能最快跑通增删改查。逻辑换了 Spring Boot 或 PHP 一样成立。考勤系统对后端的要求不高但对数据库连接的管理很敏感尤其是几十号人同时打卡的时段。我用 SQLAlchemy 的 create_engine 统一管理连接池不在函数里手动创建连接避免连接泄漏from sqlalchemy import create_engine import os DATABASE_URL os.getenv( DATABASE_URL, mysqlpymysql://root:password127.0.0.1:3306/attendance?charsetutf8mb4 ) engine create_engine( DATABASE_URL, pool_size10, max_overflow20, pool_pre_pingTrue, pool_recycle3600 )参数说明pool_size 是连接池常驻连接数设为 10 基本够中小团队用max_overflow 是峰值时额外创建的连接上限20 意味着最多 30 个并发pool_pre_ping 会在每次取连接前做一次轻量探测避免拿到已经断开的连接pool_recycle 让超过 3600 秒的连接被回收重建绕开 MySQL 的 wait_timeout 断连问题。这组参数是我做了多个考勤系统后固定下来的起点。4.2 打卡接口判重、迟到标记和补卡入口打卡接口是考勤系统的核心代码要同时处理三件事判断当天该类型是否已打卡、确定状态正常/迟到/早退、写入流水。先看实现from flask import Flask, request, jsonify from datetime import datetime, date, time from sqlalchemy import text app Flask(__name__) def get_clock_type(now): # 12点前算上班卡12点后算下班卡。真实系统应按班次配置判断 return 1 if now.hour 12 else 2 def calc_status(clock_type, clock_time): # 上班卡晚于 09:00 标迟到下班卡早于 18:00 标早退 # 宽限分钟可以放到配置表里这里先写死 if clock_type 1 and clock_time.time() time(9, 5): return 1 if clock_type 2 and clock_time.time() time(17, 55): return 2 return 0 app.route(/api/clock, methods[POST]) def clock_in(): data request.get_json(silentTrue) or {} emp_id data.get(emp_id) if not emp_id: return jsonify({code: 1, msg: emp_id不能为空}), 200 now datetime.now() work_date date.today() clock_type get_clock_type(now) status calc_status(clock_type, now) with engine.begin() as conn: try: conn.execute( text( INSERT INTO att_record (employee_id, work_date, clock_type, clock_time, status) VALUES (:e, :d, :t, :now, :s) ), {e: emp_id, d: work_date, t: clock_type, now: now, s: status} ) except Exception: # 唯一索引冲突说明当天已打过同类卡 return jsonify({code: 1, msg: 当天该类型已打卡如需修改请走补卡流程}), 200 return jsonify({code: 0, msg: ok, clock_time: now.strftime(%Y-%m-%d %H:%M:%S)})逻辑说明先用 get_clock_type 判断是上班还是下班卡再用 calc_status 计算状态最后用 INSERT 写入。注意这里没有先 SELECT 再 INSERT而是直接插入靠唯一索引捕获重复——这样并发下也不会出现两条重复卡。status 在入库时就算好月底报表直接 SUM不用重算。参数说明迟到宽限 5 分钟、早退宽限 5 分钟在演示里写死在 time(9,5) 和 time(17,55)真实项目要拆到考勤规则表里按部门甚至按人配置。补卡入口走单独接口生成补卡单而不是改原记录app.route(/api/clock/retro, methods[POST]) def retro_apply(): data request.get_json(silentTrue) or {} emp_id data.get(emp_id) work_date data.get(work_date) clock_type data.get(clock_type) reason data.get(reason, ) if not all([emp_id, work_date, clock_type]): return jsonify({code: 1, msg: 参数不完整}), 200 with engine.begin() as conn: conn.execute( text( INSERT INTO att_retro (employee_id, work_date, clock_type, reason, status) VALUES (:e, :d, :t, :r, 0) ), {e: emp_id, d: work_date, t: clock_type, r: reason} ) return jsonify({code: 0, msg: 补卡申请已提交}), 200补卡接口只插入申请单状态为待审批原考勤记录不动。这样审批通过后我们可以在审批逻辑里把原记录状态修正为正常同时保留申请单作为审计痕迹。这套设计对应原型里的补卡流程避免员工直接篡改打卡时间。4.3 月度汇总把状态在入库时算好报表期不用重算月度汇总用一条 SQL 完成。考勤登记管理系统最常见的报表需求是某月出勤几天、迟到几次、早退几次、请假几小时。因为状态在插入时就写好了这里只需要按状态计数SELECT DATE_FORMAT(work_date, %Y-%m) AS month, employee_id, COUNT(DISTINCT work_date) AS attend_days, SUM(CASE WHEN clock_type 1 AND status 1 THEN 1 ELSE 0 END) AS late_times, SUM(CASE WHEN clock_type 2 AND status 2 THEN 1 ELSE 0 END) AS early_times, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) AS miss_days FROM att_record WHERE work_date BETWEEN :month_start AND :month_end AND clock_type IN (1, 2) GROUP BY month, employee_id;逻辑说明attend_days 用 COUNT(DISTINCT work_date)因为一天有两条记录上班下班直接 COUNT(*) 会把出勤天数算成两倍。迟到只统计上班卡clock_type1早退只统计下班卡clock_type2用条件聚合把两类状态分开。缺卡统计的是状态为 3 的记录数这个状态由日终批处理写入。参数说明month_start 和 month_end 是月初和月末的日期查询接口里用时间字符串传进来即可。如果数据量超过百万行建议给 work_date 加普通索引并限制只能查最近 12 个月。4.4 和原型/数据库怎么对得上接口字段对照表代码写完后我会做一次“原型字段 → 表字段 → 接口字段”的对照检查防止前后端各说各话。比如打卡页原型的“打卡时间”对应数据库 att_record.clock_time对应接口返回值里的 clock_time“打卡结果”对应 status 的 0/1/2/3。接口返回的 JSON 字段名和数据库字段名保持一致能省掉一层转换。我用对照表的形式把三张核心表的字段和接口映射列在一起发给前端同事对齐比口头沟通高效得多。原型页面字段数据库字段接口返回字段示例值打卡时间att_record.clock_timeclock_time2025-03-17 09:00:12打卡结果att_record.statusstatus1迟到请假类型att_leave.leave_typeleave_type年假 / 事假 / 病假审批状态att_leave.statusstatus0待审 1通过 2驳回补卡原因att_retro.reasonreason早上地铁故障这套对照表做完前后端联调基本一遍过。我见过太多项目返工是因为同一个字段在数据库叫 status在接口里叫 clock_result在页面上叫“考勤结果”看起来是小事实际排查问题时要绕一大圈。5. 常见问题与避坑考勤系统从原型到上线的5个翻车点5.1 一天打五张卡唯一索引没建现象员工早上对着打卡机多按了几次考勤记录表里同一个人同一天出现 5 条上班卡记录月底统计迟到次数直接翻倍。原因建表时没在 (employee_id, work_date, clock_type) 上建唯一索引接口里也没有判重逻辑。打卡机连续触发、前端重复提交时数据库照单全收。解决建表时就把 uk_emp_date_type 唯一索引加上接口直接用 INSERT 捕获 Duplicate Entry 异常返回提示。已产生的脏数据先按 employee_id、work_date、clock_type 分组保留最早一条其余删除。注意补卡记录也要参与唯一约束否则会出现多条补卡单同时生效。5.2 打卡时间凭空少8小时时区参数不统一现象数据库里的 clock_time 存的是 01:00员工实际打卡时间是 09:00。或者查出来显示 1970-01-01 08:00 这种奇怪时间。原因应用服务器时区是 UTC数据库时区是 Asia/ShanghaiTIMESTAMP 类型在存储时做了时区转换。还有一种是连接串里没指定 serverTimezoneJDBC 驱动用了 JVM 默认时区去解析 TIMESTAMP。解决业务时间字段全部用 DATETIME不碰 TIMESTAMP。DATETIME 存什么就是什么不依赖时区配置。连接串显式加 serverTimezoneAsia/ShanghaiJDBC或 charsetutf8mb4SQLAlchemy/PyMySQL。历史数据如果已经乱了按 8 小时差值 UPDATE 修正但前提是确认原来存的是 UTC 时间。5.3 给考勤表加字段线上入库全卡住现象上线后要加一个“外勤地点”字段执行 ALTER TABLE att_record ADD COLUMN location VARCHAR(100)结果打卡接口全部超时页面转圈。原因大表 ALTER TABLE 在 MySQL 5.7 及更早版本会锁全表DML 操作全部阻塞。考勤表如果有几百万行加字段的操作可能持续几分钟到几十分钟。解决小表百万行以内低峰期直接 ALTER先看表行数再动手。大表用 pt-osc 或 gh-ost 做在线变更原理是建影子表、同步增量、最后切换。日常开发时就把字段设计完整原型阶段的数据字典多花点时间数据库结构尽量一次定稿。应急处理时可以先新加一张扩展表用 employee_id work_date 关联避免阻塞业务。5.4 数据库连一会儿就 Too many connections现象打卡高峰期接口报 “Too many connections”数据库 CPU 不高但连接数打满重启后过一阵又复现。原因代码里每次请求手动创建数据库连接用完没 close。或者 SQLAlchemy 的 session 没有关闭连接没有归还到连接池。还有一种情况是连接池被长时间占用的慢查询拖死。解决统一用 SQLAlchemy engine 管理连接禁止在函数里裸调 pymysql 连接。写操作使用 with engine.begin() 事务块事务结束连接自动归还。配置 pool_pre_pingTrue 和 pool_recycle3600把死连接和老连接清理掉。慢查询单独排查给 work_date、employee_id 建索引避免全表扫描把连接占住不放。5.5 Excel批量导入考勤记录日期变成数字现象用 Excel 模板批量导入考勤机导出的 xlsx日期列变成 45000.123 这种数字中文列变成乱码导致考勤记录错乱。原因xlsx 里的日期本质是序列值1900 系统下以 1899-12-30 为 0 起算45000 对应 2023-03-15。代码如果用 Excel 单元格的原始值直接入库就会存成数字。CSV 文件则可能被 Excel 以 ANSI 编码另存代码按 UTF-8 读直接乱码。解决用 openpyxl 读取时判断 cell 的数据类型如果是日期类型或 is_date 为 True用 cell.value 直接拿到 datetime 对象再格式化成字符串。读取 CSV 时强制以 utf-8-sig 编码打开兼容 Excel 的带 BOM UTF-8 和 ANSI 两种情况。导入前做校验打卡时间列必须是合法日期格式工号必须存在于 sys_user 表否则整行拒绝并给出错误行号。批量导入的 SQL 用 executemany 批量插入遇到重复数据按唯一索引跳过而不是中断。6. 进阶考勤数据交给工资系统前先做这三件事考勤登记管理系统做到能打卡、能请假、能出报表只能算跑通。真正检验设计的是月底把考勤数据交给工资系统那一刻。第一件事是汇总裁断工资计算期间不允许员工再补卡或改审批系统在每月最后一天跑一个批处理把所有待审批的补卡单和请假单冻结生成快照表。这个快照表就是工资系统的数据源之后的历史修改一律不进快照避免工资算完又变。我一般把快照表命名为 salary_att_snapshot字段照抄汇总结果加一个 snapshot_time 标记版本。第二件事是请假天数合并扣减。考勤汇总只算出勤天数和迟到次数但工资系统需要知道员工当月请了几天假。请假表存的是开始时间和结束时间可能是按小时请的要和考勤记录按天对齐。我的做法是把请假单展开成“每天一条”的临时表再和考勤记录表按 employee_id work_date 关联请假当天的出勤状态标记为请假扣减出勤天数。这一步在 SQL 里用日期递归或数字辅助表实现不要在应用层循环逐天判断。第三件事是输出一张工资系统能直接消费的宽表。工资系统不关心你内部是流水表还是状态机它只想要一张“员工、月份、出勤天数、迟到次数、请假小时数、应扣款”的宽表。我最后汇总结算时会生成一张视图把三张表的数据 JOIN 好工资系统直接 SELECT 就行。如果担心月底大查询拖慢业务库可以用现成的数据库同步软件把考勤库只读副本同步到报表库在副本上跑汇总。这是我做了三个考勤系统后才总结出来的流程前两次都是工资系统跑一半发现补卡单还在审批流里结果数据对不上整个部门陪着查账。现在先把这三件事固化进代码里月末流程基本不用人盯。希望帮到你。本文还有配套的精品资源点击获取
返回列表