多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

员工考勤管理系统数据库设计:从打卡流水到月度汇总的完整方案

员工考勤管理系统数据库设计:从打卡流水到月度汇总的完整方案 简介员工考勤管理系统数据库设计文档围绕考勤业务核心给出完整的数据库逻辑结构方案适合数据库课程设计、考勤类系统开发及毕业设计参考。文档详细设计了员工基本信息表、部门信息表、考勤类型信息表、员工考勤信息表和用户信息表5个实体明确员工编号、部门编号等主外键关系并给出考勤类型、罚金、雇佣日期等关键字段定义。同时系统功能需求覆盖用户增删改、超级用户登录、员工信息维护以及按姓名模糊、按部门、按雇佣时间段查询员工信息也支持按员工、考勤类型、时间段单独或组合查询考勤并可按月按部门统计考勤次数与罚金小计/总计为开发者提供了从建表到功能落地的完整思路。资源仅含1个doc文档压缩包大小57KB文档精炼、文字描述清楚。目前已有262人学习作为轻量级数据库设计参考资料可快速帮助学习者掌握考勤系统的表结构设计要点与统计需求。1. 员工考勤管理系统数据库设计从打卡流水到月度汇总的骨架说一个很多开发者在考勤系统上翻车的真相考勤系统最难受的往往不是硬件对接而是数据库设计没扛住业务规则。打卡机打点、门禁刷脸、手机 GPS 定位数据哗啦一下全进来了但如果你不知道这些数据在表里怎么归档、怎么算迟到早退、怎么区分请假出差那后续的报表和多表联查全是灾难。所谓员工考勤管理系统数据库设计本质上就是把「打卡、请假、加班、出差、排班」这几件事拆成实体关系再把时间规则转换成字段约束和状态机。这套设计最优的价值落在哪第一是给毕业设计或中小企业的管理系统做落地方案第二是给已经在跑但表结构混乱的旧系统做重构参考。我会从需求建模讲到建表 SQL再讲到最容易被忽略的状态字典和跨天排班顺带把查询性能和后续文档的交付方式一起说清楚。整个过程不依赖任何商业框架MySQL 8.0 起步你本地装个服务就能复现。2. 先建模再建表考勤业务的状态流转与实体边界2.1 考勤业务闭环从原始流水到结果归档的四层逻辑我拿到考勤需求时第一件事不是打开 Navicat 建表而是把业务闭环画出来。一个完整的考勤数据流有四个层次员工基础信息层、排班计划层、原始流水层和计算结果层。前面两层是静态配置后面两层是运行时数据。员工基础信息层解决「谁在考勤」的问题。工号、姓名、部门、入职日期、离职日期这些字段本身不难但有个容易忽略的点离职员工的历史考勤记录不能跟着员工状态一起逻辑删除。所以员工表一定要有离职日期和在职状态两个字段而且考勤流水表只存员工 ID不冗余工号和姓名避免离职改单后历史报表跟着变。排班计划层解决「应该按什么时间上下班」的问题。很多考勤系统翻车不是打卡数据丢了而是班次时间配错了。这部分我会独立设计一张排班表把不同岗位的班次模板和员工维度的排班实例分开。常见做法是两张表班次模板表存固定上下班时间员工排班表存具体某一天用哪个班次这样弹性工时和调班才有地方落。原始流水层是考勤系统的地基。闸机、手机、考勤机的每一下打卡动作都追加到这里一条一录不做修改不做删除。流水表只回答一个问题谁在什么时间从哪里打了一次卡。真正判断迟到早退的逻辑在下一层做这样即使规则改版流水数据依旧可信。计算结果层是把原始流水按日、按月加工后的产物。日结果表存每一天每个员工的考勤判定汇总表按月生成出勤天数、迟到次数、早退次数、加班时长直接对接薪资模块。这一层的核心是「可重算」修改了迟到阈值或排班后历史结果要能一键批量重算所以结果表设计时就要预留规则版本号。2.2 状态字典先行为什么说枚举值和状态字段决定系统上限我见过太多考勤系统的状态字段乱得离谱有的用 0/1/2 但注释只有作者自己懂有的用中文值「迟到/早退」导致统计时字符集出问题有的同一个状态在五张表里含义完全不同。这些都是状态建模没做好的后遗症。考勤状态看起来只是「正常、迟到、早退、缺卡、请假、出差、外勤」几个词但你深入想一步就会发现状态不是单值而是复合的。比如某员工早上迟到 10 分钟、下午早退 5 分钟同日既加班又调休一张日结果表如果只放一个状态字段是存不下的。常见做法是主状态加惩罚时长字段主状态字段存当天最严重的异常类别迟到分钟数、早退分钟数单独用数字字段存这样既能直观统计也能复核明细。还有一个容易被忽略的状态类别是「缺卡」和「未排班」的区别。前者是排了班但一张卡都没打后者是那天本来就不上班。这两个如果混成一个状态月底统计出勤率会严重失真。我在结果表里会用att_status和schedule_flag两个字段分开存schedule_flag表示当天是否被排班att_status只有排班才可能有值。状态字典表本身不建议用单独的数据库表来存。考勤状态是高度稳定的业务枚举用一个带注释的 INT 字段配合代码层枚举类就够了。如果你用 TinyINT 表示状态记得在字段注释里把 0 到 9 的每个取值写清含义这是成本最低的文档。2.3 实体关系梳理员工、部门、排班、流水、申请五张主表的关系把业务闭环拆成实体后考勤数据库的核心表就清晰了部门表、员工表、班次模板表、员工排班表、考勤流水表、日结果表、请假单表、加班单表、出差外勤单表。它们之间的外键关系是员工属于部门排班引用员工和班次模板流水引用员工日结果引用员工和排班请假加班出差的申请表全部引用员工。这里要特别说一个设计取舍请假、加班、出差这种单据类数据是走独立表还是统一设计成一张通用审批表。我的建议是独立表。三者的字段差异很大——请假要算时长和假种加班要按节假日类型算倍率出差要记录地点和天数强行合并成通用审批表会在类型判断上写满 if-else。独立表配合一个统一的审批状态字段是维护成本最低的方案。3. 核心表结构与建表 SQL从部门到月度汇总的五张关键表3.1 部门与员工表最基础的两张表最容易埋雷部门表没什么高深的但注意两个点支持多级部门时会用到父级 ID而且查询时要考虑递归取子部门的场景。我习惯加一个dept_path字段存祖先路径比如/1/12/34/这样查某个部门下所有员工时可以直接 LIKE 前缀性能远好于递归查询。员工表是几乎所有考勤查询的起点字段我给到最小必要集员工 ID、工号、姓名、部门 ID、入职日期、离职日期、在职状态、默认班次 ID。注意工号虽然是唯一键但员工 ID 才是主键因为离职后工号可能被重新分配给新人工号一旦复用历史考勤归属就会错乱。CREATE TABLE emp_dept ( dept_id INT UNSIGNED AUTO_INCREMENT COMMENT 部门ID, dept_name VARCHAR(50) NOT NULL COMMENT 部门名称, parent_dept_id INT UNSIGNED DEFAULT 0 COMMENT 父部门ID0表示根, dept_path VARCHAR(200) NOT NULL DEFAULT / COMMENT 祖先路径如/1/12/, dept_status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (dept_id), KEY idx_dept_path (dept_path) ) ENGINEInnoDB COMMENT 部门表; CREATE TABLE emp_employee ( emp_id INT UNSIGNED AUTO_INCREMENT COMMENT 员工ID主键, emp_code VARCHAR(20) NOT NULL COMMENT 工号业务唯一, emp_name VARCHAR(50) NOT NULL COMMENT 姓名, dept_id INT UNSIGNED NOT NULL COMMENT 所属部门ID, hire_date DATE NOT NULL COMMENT 入职日期, leave_date DATE DEFAULT NULL COMMENT 离职日期null表示在职, emp_status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 2离职 3停薪留职, default_shift_id INT UNSIGNED DEFAULT NULL COMMENT 默认班次ID, PRIMARY KEY (emp_id), UNIQUE KEY uk_emp_code (emp_code), KEY idx_dept_id (dept_id) ) ENGINEInnoDB COMMENT 员工表;逻辑说明两张表都以 INT UNSIGNED 作为主键避免负数和类型容量争议。dept_path是典型的空间换时间做法省去递归查询子部门。emp_code单独做唯一索引但因为离职可能复用所以业务查询尽量用emp_id。参数说明dept_path的祖先路径每次部门移动都要更新如果组织架构调整频繁这个字段的管理成本会变高default_shift_id只是兜底默认值真正某一天用哪个班次必须查排班表否则临时调班没有依据。3.2 班次模板表与员工排班表弹性工时和跨天班次怎么落班次模板表存的是「一类班次的上下班规则」不是某个人某一天的具体安排。字段包括班次 ID、班次名称、上班时间、下班时间、是否跨天、宽限分钟数。跨天标记太重要了夜班 22:00 到次日 06:00 如果不标记跨天日期归属会全部算错。员工排班表才是真正的逐日排班。每个员工每天一行引用班次模板 ID没有排班的日子不插数据。这个设计的好处是支持临时调班某天 A 员工从白班换到夜班排班表里改这一行就可以不影响班次模板也不影响别的员工。CREATE TABLE att_shift_template ( shift_id INT UNSIGNED AUTO_INCREMENT COMMENT 班次模板ID, shift_name VARCHAR(30) NOT NULL COMMENT 班次名称如白班/夜班/行政班, work_start_time TIME NOT NULL COMMENT 计划上班时间, work_end_time TIME NOT NULL COMMENT 计划下班时间, is_cross_day TINYINT NOT NULL DEFAULT 0 COMMENT 1跨天 0不跨天, late_grace_min INT NOT NULL DEFAULT 0 COMMENT 迟到宽限分钟超过才算迟到, early_grace_min INT NOT NULL DEFAULT 0 COMMENT 早退宽限分钟, work_hours DECIMAL(4,1) NOT NULL COMMENT 标准工时如8.0, PRIMARY KEY (shift_id) ) ENGINEInnoDB COMMENT 班次模板表; CREATE TABLE att_emp_schedule ( schedule_id INT UNSIGNED AUTO_INCREMENT COMMENT 排班ID, emp_id INT UNSIGNED NOT NULL COMMENT 员工ID, work_date DATE NOT NULL COMMENT 排班日期, shift_id INT UNSIGNED NOT NULL COMMENT 使用的班次模板ID, schedule_flag TINYINT NOT NULL DEFAULT 1 COMMENT 1出勤班 0休息日, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最后修改时间, PRIMARY KEY (schedule_id), UNIQUE KEY uk_emp_date (emp_id, work_date), KEY idx_work_date (work_date) ) ENGINEInnoDB COMMENT 员工排班表;逻辑说明uk_emp_date唯一键保证一个员工一天只能有一行排班记录。schedule_flag用来存法定节假日和调休日的休息标记比如周六补班时插一行schedule_flag1且指定班次模板日期本身就是周六也不影响。参数说明late_grace_min是业务规则的落地点很多公司允许 5 分钟弹性这里存 5 即可计算每月迟到次数时直接用actual_late_minutes late_grace_min判断不要再在 SQL 里写死 5。3.3 考勤流水表追加写入、不删不改、按月分区考勤流水表是整个系统数据量最大的表。一个 500 人的公司每人每天至少 2 条打卡上班和下班一个月就是 3 万条一年 36 万条。更大的坑是换班或补卡场景可能一天打 6 次卡。所以流水表必须考虑写入性能和归档策略。我推荐的方案是按月分区每个月一个分区让旧数据自然归档。索引上主索引用(emp_id, punch_time)做联合索引这是所有查询的基础。流水表本身不做 UPDATE补卡记录也直接 INSERT 一条带来源标记的数据避免锁竞争和并发冲突。punch_time用 DATETIME 存精确到秒不要用 TIME因为跨天排班场景 TIME 无法排序。CREATE TABLE att_raw_record ( record_id BIGINT UNSIGNED AUTO_INCREMENT COMMENT 流水ID, emp_id INT UNSIGNED NOT NULL COMMENT 员工ID, punch_time DATETIME NOT NULL COMMENT 打卡时间, punch_type TINYINT NOT NULL DEFAULT 0 COMMENT 0自动 1上班卡 2下班卡 3加班开始 4加班结束 9补卡, source_type TINYINT NOT NULL DEFAULT 0 COMMENT 0考勤机 1门禁 2手机GPS 3手工补录, device_sn VARCHAR(50) DEFAULT NULL COMMENT 设备编号, raw_data VARCHAR(255) DEFAULT NULL COMMENT 原始报文冗余排查用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 写入时间, PRIMARY KEY (record_id, punch_time), KEY idx_emp_time (emp_id, punch_time) ) ENGINEInnoDB COMMENT 考勤流水表 PARTITION BY RANGE COLUMNS(punch_time) ( PARTITION p202401 VALUES LESS THAN (2024-02-01), PARTITION p202402 VALUES LESS THAN (2024-03-01), PARTITION p202403 VALUES LESS THAN (2024-04-01) );逻辑说明分区列必须参与主键所以主键是(record_id, punch_time)联合主键。punch_type和source_type分开存避免把「打的是什么卡」和「从哪打的卡」混在一个字段里。补卡记录单独给一个类型方便月底统计区分真实打卡和人工干预记录。参数说明分区需要每月提前建。常见做法是写一个定时任务月底自动ALTER TABLE att_raw_record ADD PARTITION否则下月数据写入时会卡在最新分区上raw_data字段保留硬件原始报文排查「为什么这个人打卡没识别」这类问题时不用去翻硬件日志。3.4 日结果表状态、分钟数、规则版本一个都不能少日结果表是考勤判定的输出物。它不做实时计算而是每天凌晨或下班后由定时任务批量生成。work_date存业务归属日期如果班次跨天这个日期指的是「上班签到的那一天」而不是打卡自然日这一点是用跨天排班时最容易搞错的地方。日结果表需要把「主状态」「迟到分钟」「早退分钟」「实际工时」分层存放。为什么不用一个状态字段算出一切因为月底统计维度不同算迟到率要迟到次数算加班要实际工时算全勤要把请假的排除分开存才能各取所需而不是在统计时对复合状态字段做字符串匹配。CREATE TABLE att_daily_result ( result_id BIGINT UNSIGNED AUTO_INCREMENT COMMENT 日结果ID, emp_id INT UNSIGNED NOT NULL COMMENT 员工ID, work_date DATE NOT NULL COMMENT 业务归属日期, schedule_id INT UNSIGNED DEFAULT NULL COMMENT 关联的排班ID, first_punch_time DATETIME DEFAULT NULL COMMENT 当日首次上班打卡, last_punch_time DATETIME DEFAULT NULL COMMENT 当日最后下班打卡, att_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未排班 1正常 2迟到 3早退 4迟到早退 5缺卡 9异常, late_minutes INT NOT NULL DEFAULT 0 COMMENT 迟到分钟, early_minutes INT NOT NULL DEFAULT 0 COMMENT 早退分钟, actual_work_seconds INT NOT NULL DEFAULT 0 COMMENT 实际在岗秒数, rule_version VARCHAR(20) NOT NULL DEFAULT v1 COMMENT 判定规则版本, calc_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 计算时间, PRIMARY KEY (result_id), UNIQUE KEY uk_emp_date (emp_id, work_date), KEY idx_work_date_status (work_date, att_status) ) ENGINEInnoDB COMMENT 考勤日结果表;逻辑说明first_punch_time和last_punch_time存的是从流水表提取的最早最晚打卡时间后续计算在岗时长直接在结果表上进行不用反复回流水表att_status0表示当天未排班是正常的休息日数据统计出勤率时要过滤掉这个状态。参数说明actual_work_seconds按秒存是为了加班分钟计算时的精度不要用 INT 存小时否则换算会丢精度rule_version是洗数据后悔药改一次判定规则就升一个版本号重算历史数据时能区分新旧结果。4. 规则参数化与单据表请假出差加班怎么与考勤结果联动4.1 请假单、加班单、出差外勤单三张独立单据表的设计要点如果说流水表是考勤系统的心脏单据表就是动脉。请假、加班、出差外勤必须全部落到数据库表里并且要和日结果表建立软关联否则月底对账时员工说「我那天请假了你怎么算我缺卡」你连反驳的依据都没有。请假单表的字段设计重点是时长和假种。时长精确到小时假种用 TINYINT 枚举。审批状态要同时存在因为审批中的假单不能参与考勤判定。加班单表的核心是加班日期、起止时间和加班类型因为工作日加班、休息日加班、法定节假日加班的倍率不同这个字段直接决定薪资模块的计算结果。出差外勤表相对简单重点是起始日和结束日以及目的地字段它不影响迟到早退判定但要在日结果表里标记为「外勤状态」否则出勤率会把出差人员当成旷工。CREATE TABLE att_leave ( leave_id INT UNSIGNED AUTO_INCREMENT COMMENT 请假单ID, emp_id INT UNSIGNED NOT NULL COMMENT 员工ID, leave_type TINYINT NOT NULL COMMENT 1年假 2事假 3病假 4调休 9其他, start_time DATETIME NOT NULL COMMENT 请假开始时间, end_time DATETIME NOT NULL COMMENT 请假结束时间, duration_hours DECIMAL(4,1) NOT NULL COMMENT 请假时长小时, leave_reason VARCHAR(255) DEFAULT NULL COMMENT 请假事由, apply_status TINYINT NOT NULL DEFAULT 0 COMMENT 0审批中 1通过 2驳回 3已撤销, approve_time DATETIME DEFAULT NULL COMMENT 审批时间, PRIMARY KEY (leave_id), KEY idx_emp_time (emp_id, start_time, end_time) ) ENGINEInnoDB COMMENT 请假单表; CREATE TABLE att_overtime ( ot_id INT UNSIGNED AUTO_INCREMENT COMMENT 加班单ID, emp_id INT UNSIGNED NOT NULL COMMENT 员工ID, ot_date DATE NOT NULL COMMENT 加班日期, start_time TIME NOT NULL COMMENT 加班开始时间, end_time TIME NOT NULL COMMENT 加班结束时间, duration_hours DECIMAL(4,1) NOT NULL COMMENT 加班时长小时, ot_type TINYINT NOT NULL DEFAULT 0 COMMENT 0工作日 1休息日 2法定节假日, apply_status TINYINT NOT NULL DEFAULT 0 COMMENT 0审批中 1通过 2驳回, PRIMARY KEY (ot_id), KEY idx_emp_date (emp_id, ot_date) ) ENGINEInnoDB COMMENT 加班单表;逻辑说明请假单和加班单都用了KEY idx_emp_time因为月底生成日结果表时要查某个员工某段时间内是否有已通过的请假或加班记录这个索引是查询的命脉。apply_status必须过滤审批中的假单如果参与判定员工可以钻漏洞先打假单再撤销。参数说明duration_hours用 DECIMAL(4,1) 存小数小时比如 4.5 小时别用 INT。ot_type是加班倍率的依据月末汇总时用SUM(duration_hours * rate)计算加班折算时长休息日和法定节假日的倍率通常写在薪资配置里不要写死在本表。4.2 结果重算的触发逻辑什么时候批量重建日结果我常说日结果表是「可摧毁」的——它不应该是手工维护的数据而是随时可以从流水表和单据表重算出来的派生数据。这样设计带来的好处是改规则、补数据、修 bug 后一键重算不会出现新旧结果并存还互相矛盾的问题。实际落地时的重算触发点有三个。第一个是每天凌晨的定时任务全量计算昨天的日结果第二个是补卡或改审批单后的定点重算只重算受影响员工受影响日期第三个是规则版本升级后的全量重算。第三个触发点非常耗时一两万人的公司全量重算可能要跑十几分钟所以必须支持按员工范围分批重算避免锁表。补卡和改单是重算的高频场景。「员工早上忘了打卡下午找行政补录了一条 9:00 的上班卡」补录动作写入att_raw_record之后对应员工的日结果必须重算。重算逻辑里请假审批通过和撤销都要做同样的操作撤销一个假单后当天可能从「请假」变成「缺卡」这种联动如果不做月底对账就会前后对不上。5. 考勤数据库设计避坑5 个反复出现的真实故障5.1 考勤机时间漂移导致全员迟到误判现象某天早上考勤机时间慢 8 分钟8:00 上班员工实际 7:55 到但打卡时间显示 8:03日结果表把一百多个正常员工全判成迟到。这类问题在分布式考勤机上特别常见设备时钟靠电池维持温度变化或断电都会漂移。原因考勤判定直接用了设备本地时间戳设备时间不准确时判定结果跟着错。更隐蔽的是不同考勤机之间时间偏差同一员工在不同门禁打卡一个快 2 分钟一个慢 3 分钟。解决流水表入库时用服务器时间NOW()替换设备时间。具体做法是att_raw_record表增加server_time字段入库时取应用服务器时间存储设备时间保留在raw_data字段里只做审计参考。判定迟到早退一律用server_time这样设备怎么漂移都不影响结果。5.2 跨天排班日期归属错乱现象夜班员工 22:00 上班次日 06:00 下班考勤结果把一次完整的夜班拆成两天22:00 算前一天06:00 算后一天两头都不完整工时统计一片混乱。原因日结果表的work_date按打卡自然日DATE(punch_time)提取跨天时上班卡和下班卡落在了两个自然日。解决日结果表生成时work_date一律按排班模板的work_start_time归属。比如夜班 22:00 上班那么当天 22:00 之后的打卡记录都归属当天次日 06:00 的下班卡也归属前一天。核心逻辑是把判定基准从「打卡日」变成「工作日」。5.3 修改排班后历史日结果不同步现象管理员把某员工 3 月的班次从白班调整成夜班结果 3 月的历史日结果还是按白班计算的迟到分钟数完全不对。原因日结果表在生成时把排班信息冗余进来了排班表att_emp_schedule改了但日结果表没有级联重算。解决排班表的update_time字段加触发器或者由业务代码感知排班变更后把受影响日期范围内该员工的日结果删除标记为待重算状态。我推荐的做法是直接删日结果行定时任务会自动补齐不需要额外建状态表。5.4 多表联查超时与索引失效现象月底统计报表时按部门、时间范围、状态条件组合查询一次联查员工表、排班表、流水表、日结果表数据库直接慢查询报警接口超时 30 秒返回不了。原因日结果表的联合索引设计不合理查询条件出现dept_id和att_status组合时现有索引(emp_id, work_date)无法覆盖走了全表扫描。解决给日结果表增加冗余索引(dept_id, work_date, att_status)是常用手段但更彻底的做法是预聚合。月末汇总时直接生成一张月度汇总表把每个员工当月的迟到次数、早退次数、出勤天数提前算好报表查询不再碰流水表只查汇总表。5.5 一卡多打导致工时翻倍现象员工下班忘打卡又补打或者出门买东西回来再打一次一天有 6 条打卡记录结果算出当日工时 12 小时加班报表直接爆炸。原因实际工时计算用了最早最晚打卡时间中间那些多余打卡没有去重逻辑。解决合理做法是用第一次和最后一次打卡计算总时长中间的记录作为过度打卡忽略。但更严格的系统采用「时间段匹配」逻辑班次开始前 2 小时内第一次打卡作为上班卡班次结束后 3 小时内最后一次作为下班卡超过时间窗口的打卡只记录不判定。把这段逻辑做成存储过程或者业务代码不要完全依赖 SQL 语句。6. 从设计到落地数据字典、演示数据和索引体检三板斧很多开发者的数据库设计止步于建表语句等到写文档或交接时才头疼。我交付考勤数据库设计方案时有三件必做的事能帮你避免后期反复解释字段含义的尴尬。第一件是数据字典生成。用一张database_schema_meta表或者直接用information_schema查询生成 Markdown 格式的数据字典。每张表的字段名、类型、注释、是否可空、默认值列全配合一次SHOW CREATE TABLE补齐索引信息。字段注释在建表时就要写完整这比事后任何文档都有说服力。第二件是造演示数据。写一个 Python 脚本生成三个月的模拟数据50 个员工分布在 5 个部门按班次模板生成排班随机生成迟到、早退、请假、加班记录再把考勤流水按时段随机分布。这一步能快速验证你的表设计能不能支撑真实场景的查询和统计按固定逻辑生成的数据自洽性也容易检查。import random import datetime from faker import Faker fake Faker(zh_CN) emp_ids list(range(1, 51)) shift_map {1: 09:00, 2: 22:00} # 1白班 2夜班 def generate_raw_records(days90): records [] for d in range(days): work_date datetime.date(2024, 1, 1) datetime.timedelta(daysd) if work_date.weekday() 5: # 周末多数人不上班 continue for emp_id in emp_ids: shift_type random.choice([1, 2]) work_start shift_map[shift_type] morning_offset random.randint(-5, 20) # 早到/迟到分钟 punch_in datetime.datetime.combine(work_date, datetime.time(9, 0)) \ datetime.timedelta(minutesmorning_offset) punch_out punch_in datetime.timedelta(hoursrandom.randint(8, 10)) records.append((emp_id, punch_in, shift_type)) records.append((emp_id, punch_out, shift_type)) return records逻辑说明脚本用Faker库生成中文本名按工作日逻辑跳过周末打卡时间基点是班次开始时间加上随机偏移量这样生成的数据既有规律又能模拟真实分布方便后续验证 SQL。参数说明morning_offset控制在 -5 到 20 分钟之间既包含正常打卡也包含少量迟到场景验证结果表判定时两者都有样本。月度总计 45 个工作日左右 × 50 员工 × 2 条打卡数据量在 4500 条左右完全足够测试索引和统计查询。第三件是索引体检。对每条慢查询语句执行EXPLAIN检查type字段是否达到ref或rangeExtra里有没有Using filesort或Using temporary。常见的坑是在日结果表查att_status IN (2,3)时索引失效解决方式是给(work_date, att_status)建联合索引覆盖月末统计常规查询。这三件做完你的考勤数据库设计基本就是完整可交付的状态了。我自己做这类项目时经历过最惨的一次翻车上线一个多月后员工对补卡流水提出异议因为旧版本结果表没有记录计算规则数据对不上没法追溯。所以我现在每个结果表都带rule_version明细和汇总对不上时先查版本号再查重算逻辑问题定位时间从半天缩到了半小时。最后分享一个坚持了很久的习惯每周检查一次数据库慢查询日志重点看哪些查询在月底突然变慢。考勤系统有天然的周期性峰值每月最后一天晚上全公司的出勤汇总都在跑提前半个月摸清哪些 SQL 该加索引、哪些汇总该走预聚合比憋到月底再救火靠谱得多。这套设计方案你照着落地从建表到跑通月报基本不会遇到结构性障碍。希望帮到你。本文还有配套的精品资源点击获取
返回列表