考勤是 HR 的基础数据来源:薪资、绩效、审批都和它绑得死死的。很多公司起初用 Excel 或打卡机拼凑,但随着人员增多、异地/外勤增多、弹性工作制兴起,单纯人工就维护不起了。做个靠谱的考勤模块,可以:
- 降低 HR 人工统计错误、减少加班核算争议;
- 给薪酬/绩效/报表提供可信数据;
- 支持多种打卡场景(门禁/手机/外勤/企业微信/钉钉);
- 把审批流、补卡、请假、加班等流程自动串起来,便于监管与审计。
简单说:人事(HR)模块负责员工档案、合同、薪酬、绩效等;OA(办公自动化)负责请假、审批、公告、流程审批等。两者常结合成一套平台,考勤板块就是连接 HR(数据主体)和 OA(流程/审批)的桥梁。
本文主要内容
- 总体架构图(技术栈建议 + 部署拓扑)
- 考勤板块功能清单(含你列的功能)
- 每个功能的业务流程图与关键点
- 数据库设计(ER 图、表结构关键字段)
- 后端实现思路 + 关键接口(代码示例)
- 前端实现思路 + 关键页面/组件(代码示例)
- 考勤算法(排班规则、异常判定、补卡合并逻辑)
- 报表与导出(SQL 示例、常见报表)
- 实现效果与上线建议(测试、迁移、运维)
- FAQ
一、总体架构图(文字版 + 拓扑建议)
下面给一个简化的架构图(文本版):
swift
┌────────────┐
│ 门禁/打卡机 │
└─────┬──────┘
│打卡数据
┌──────────┐ ┌────▼────┐ ┌────────────┐
│ 手机APP │◀────│ API 网关│────▶│ 后端微服务 │
│(Android/IOS)│ └────┬────┘ │ - 考勤服务 │
└──────────┘ │ │ - 审批服务 │
│ └────┬───────┘
┌─────▼────┐ │
│ 第三方集成│(企业微信/钉钉/门禁)│
└─────┬────┘ │
│ 消息/回调 │
┌─────▼────┐ ┌───▼────┐
│ 消息队列 │ │ 数据库 │
│ (Kafka/RabbitMQ) │ │ (MySQL) │
└─────┬────┘ └────────┘
│
┌─────▼────┐
│ 报表/BI │(OLAP/Redis缓存)
└─────────┘
技术栈建议:
- 后端:Node.js + TypeScript / Java Spring Boot(团队熟悉的)
- 数据库:MySQL(关系数据)+ Redis(缓存、当天打卡快速查询)
- 消息:Kafka 或 RabbitMQ(异步处理打卡、加班审批通知)
- 前端:React 或 Vue + 企业微信/钉钉 H5 集成
- 打卡设备:支持 HTTP 或 SDK 的门禁机,手机打卡使用 GPS + 照片 + 签名
二、功能清单(必须覆盖的模块)
列出你要求的所有功能,并做一点补充:
- 考勤工作台(个人/管理端)
- 考勤报表(日报/月报/异常统计/部门汇总)
- 考勤打卡(上班/下班/中间卡)
- 外勤打卡(带位置、拍照、范围校验)
- 补卡申请(员工发起,指定时间、理由、附件)
- 请假申请(假种管理、时长计算、审批流)
- 加班申请(小时制/天制、审批与薪资对接)
- 调休申请(加班转调休、有效期管理)
- 出差申请(带审批、行程、差旅费对接)
- 考勤确认(HR/主管确认、锁定考勤/最终结算)
三、每个功能的业务流程(带流程图说明)
我用文字 + 简单流程图表示关键流程,便于复制到画图工具生成正式图。
1.考勤打卡(普通)
流程: 员工 -> 手机/门禁机打卡 -> 打卡数据入队列 -> 解析(设备ID/员工ID/时间/位置/照片)-> 写入 raw_table -> 实时规则引擎判定(是否迟到/早退/缺卡)-> 写入考勤记录表 -> 通知员工(异常提醒)
简化流程图(ASCII):
css
[员工打卡] -> [API网关] -> [消息队列] -> [打卡解析服务] -> [raw_attendance] -> [规则引擎] -> [attendance_record]
关键要点:
- 打卡时间精确到秒
- 设备和用户绑定,防止代打卡
- 异常(迟到/早退/未打卡)即时通知主管/员工
2.外勤打卡
流程: 员工在外勤范围或客户处,通过手机提交带GPS和拍照的打卡 -> 后端校验 GPS 与预定义坐标(或基于半径) -> 记录为外勤打卡(可选: 关联客户/项目)
要点:
- GPS 抖动容错(比如 50m 内为有效)
- 必需拍照 + 人脸或签名(防代打)
- 支持离线缓存并在网络恢复后补传
3.补卡申请
流程: 员工 -> 补卡申请(选择缺卡记录或手填时间)-> 提交理由与附件 -> 送审批(主管/HR)-> 审批通过后,系统把原缺卡替换/标注 -> 考勤重新计算
要点:
- 补卡记录需保留 audit trail(谁审核、时间、理由)
- 补卡时需要限制补卡时间窗口(比如最多补过去 30 天内的打卡)
4.请假/加班/调休/出差申请(统一审批流)
通用流程: 员工填写申请 -> 系统初步校验(余额/规则) -> 流程引擎流转(主管/HR/财务) -> 审批通过 -> 更新考勤与余额(请假扣除天数/加班转调休等)
要点:
- 建议把审批流作为可配置的规则(不同部门有不同审批链)
- 请假时长自动计算(兼容小时制与天制,考虑跨天/跨班次)
- 加班产生的调休需记录有效期并提供调休审批
5.考勤确认(HR/主管)
流程: 系统按月生成考勤汇总 -> 主管/HR 审核、锁定 -> 生成工资表输入 -> 后续补卡/变更应记录变动
要点:
- 锁定后禁止随意修改,任何修改需二次审批
- 支持回溯修改并记录变更记录
四、数据库设计(关键表与字段)
给出简化版核心表结构(MySQL)。真实系统还需要 audit、索引、归档表,这里只提供关键字段示例。
sql
-- 员工表
CREATE TABLE employee (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_no VARCHAR(50) UNIQUE,
name VARCHAR(100),
dept_id BIGINT,
job_title VARCHAR(100),
work_status ENUM('active','left','on_leave'),
created_at DATETIME,
updated_at DATETIME
);
-- 原始打卡记录(raw)
CREATE TABLE raw_attendance (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_id BIGINT NOT NULL,
device_id VARCHAR(100),
clock_time DATETIME NOT NULL,
latitude DECIMAL(10,7),
longitude DECIMAL(10,7),
photo_url VARCHAR(255),
source ENUM('device','mobile','third_party'),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 处理后的考勤记录
CREATE TABLE attendance_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_id BIGINT NOT NULL,
date DATE NOT NULL,
shift_id BIGINT, -- 排班
clock_in DATETIME,
clock_out DATETIME,
total_work_minutes INT,
status JSON, -- { "am":"normal","pm":"late", ... }
is_locked BOOLEAN DEFAULT FALSE,
updated_at DATETIME
);
-- 请假/加班/补卡申请表 (统一申请表)
CREATE TABLE attendance_request (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_id BIGINT NOT NULL,
req_type ENUM('leave','overtime','补卡','外勤','出差','调休'),
start_time DATETIME,
end_time DATETIME,
hours DECIMAL(5,2), -- 申请时长
reason TEXT,
attachments JSON,
status ENUM('pending','approved','rejected','cancelled'),
applicant_id BIGINT,
approver_chain JSON, -- 审批链与意见
created_at DATETIME,
updated_at DATETIME
);
索引建议:
- raw_attendance: (emp_id, clock_time)
- attendance_record: (emp_id, date)
- attendance_request: (emp_id, status, created_at)
五、后端实现思路 + 关键接口
说明:下面代码是简化示例,生产环境需加权限校验、参数校验、日志、限流、幂等处理等。
1.接收打卡(API)
ts
// src/controllers/clockController.ts
import { Request, Response } from 'express';
import db from '../db';
import { publishToQueue } from '../mq';
export async function receiveClock(req: Request, res: Response) {
const { empId, deviceId, clockTime, lat, lng, photo } = req.body;
if (!empId || !clockTime) return res.status(400).send({ error: '缺少参数' });
// 保存 raw
const [result] = await db.execute(
'INSERT INTO raw_attendance (emp_id, device_id, clock_time, latitude, longitude, photo_url) VALUES (?, ?, ?, ?, ?, ?)',
[empId, deviceId, clockTime, lat || null, lng || null, photo || null]
);
// 异步发送到消息队列,由 worker 处理规则
await publishToQueue('attendance_raw', { id: result.insertId });
return res.json({ ok: true, rawId: result.insertId });
}
2.Worker:处理 raw -> attendance_record
ts
// src/workers/processRaw.ts
import db from '../db';
export async function processRawRecord(rawId: number) {
const [rows] = await db.execute('SELECT * FROM raw_attendance WHERE id=?', [rawId]);
if (!rows.length) return;
const raw = rows[0];
const empId = raw.emp_id;
const date = raw.clock_time.toISOString().slice(0,10);
// 简化逻辑:如果已有上班时间则把此作为下班
const [existing] = await db.execute('SELECT * FROM attendance_record WHERE emp_id=? AND date=?', [empId, date]);
if (!existing.length) {
await db.execute(
'INSERT INTO attendance_record (emp_id, date, clock_in, updated_at) VALUES (?, ?, ?, NOW())',
[empId, date, raw.clock_time]
);
} else {
const rec = existing[0];
// 更新下班时间为latest time
let clockOut = rec.clock_out ? (new Date(rec.clock_out) < new Date(raw.clock_time) ? raw.clock_time : rec.clock_out) : raw.clock_time;
// 计算工作分钟数(简化)
const inTime = new Date(rec.clock_in);
const outTime = new Date(clockOut);
const minutes = Math.max(0, Math.round((outTime.getTime()-inTime.getTime())/60000));
await db.execute('UPDATE attendance_record SET clock_out=?, total_work_minutes=?, updated_at=NOW() WHERE id=?', [clockOut, minutes, rec.id]);
}
}
3.申请相关接口(补卡/请假)
ts
// src/controllers/requestController.ts
export async function createRequest(req: Request, res: Response) {
const { empId, reqType, startTime, endTime, hours, reason, attachments } = req.body;
// 校验
// 插入
const [result] = await db.execute(
'INSERT INTO attendance_request (emp_id, req_type, start_time, end_time, hours, reason, attachments, status, applicant_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, "pending", ?, NOW())',
[empId, reqType, startTime, endTime, hours, reason, JSON.stringify(attachments || []), empId]
);
// 发起审批流(示例:简单串行的审批)
// sendNotificationToApprover(...)
res.json({ ok: true, requestId: result.insertId });
}
六、前端实现思路 + 关键页面/组件(React 示例片段)
重点展示打卡组件与外勤打卡带位置校验。
1.手机打卡页(简化)
jsx
// ClockButton.jsx
import React from 'react';
import axios from 'axios';
export default function ClockButton({ empId }) {
async function handleClock(type='in') {
// 获取位置
navigator.geolocation.getCurrentPosition(async pos => {
const payload = {
empId,
deviceId: 'mobile-'+navigator.userAgent,
clockTime: new Date().toISOString(),
lat: pos.coords.latitude,
lng: pos.coords.longitude,
};
// 上传拍照可以通过 input file 或相机API
const res = await axios.post('/api/clock', payload);
if (res.data.ok) alert('打卡成功');
else alert('打卡失败');
}, err => {
alert('获取位置失败: '+err.message);
}, { enableHighAccuracy: true, timeout: 10000 });
}
return handleClock()}>立即打卡;
}
2.补卡/申请页(关键字段)
- 选择缺卡记录或手填时间
- 上传凭证(照片)
- 填写理由 -> 提交
七、考勤算法与规则(核心要点)
考勤的复杂度往往来自规则:排班、多班次、跨日班次、节假日、工时折算、加班与调休换算。给出几个关键策略:
1.排班与班次建模
- 定义 shift:包含上班时间、下班时间、允许打卡窗口、是否跨天、是否需要午休分段。
- 排班表 roster:给员工按日期分配 shift_id(支持轮班、周期性排班)。
2.迟到/早退/旷工判定
- 早上允许缓冲(例:上班缓冲 5 分钟),超过则标记迟到并记录差值
- 未打卡且整日未上班且未请假为旷工
- 如果存在多次打卡则取最早上班和最晚下班;中午出入视为中间休息
3.加班与调休
- 加班审批通过后,生成加班记录并给出加班小时数与可转调休时长(例如 1:1 或 1:1.5)
- 调休需在有效期内(例如一年内)使用,否则自动转为工资结算或失效
4.外勤位置校验策略
- 允许 50~200 米误差(视精度)
- 结合 WiFi/蓝牙/基站辅助判断
- 拍照与人脸识别作为强校验手段(提高防作弊)
八、考勤报表 & SQL 示例
常见报表:月度出勤汇总、异常明细、部门加班汇总、个人考勤明细、请假统计。下面给个月度出勤汇总 SQL(简化):
sql
SELECT e.emp_no, e.name, a.date,
a.clock_in, a.clock_out, a.total_work_minutes,
JSON_EXTRACT(a.status, '$.am') AS morning_status,
JSON_EXTRACT(a.status, '$.pm') AS afternoon_status
FROM attendance_record a
JOIN employee e ON e.id = a.emp_id
WHERE a.date BETWEEN '2025-07-01' AND '2025-07-31'
AND e.dept_id = 10
ORDER BY e.emp_no, a.date;
做大数据量报表建议:
- 每月把 attendance_record 按月份做聚合表(OLAP)
- 用 Redis 缓存频繁请求的部门月报
- 导出支持 CSV、Excel(后端用 streaming)
九、实现效果与上线建议
实施考勤系统不仅是写代码,还要做好推广、培训、与现有系统(薪资、门禁、OA)对接。
上线前的关键检查
- 数据迁移:把历史打卡/请假/加班数据从旧系统或 Excel 导入,设计脚本并做多次预演。
- 规则回测:用历史 3-6 个月的数据跑新系统的规则,核对异常率、迟到早退统计,找出规则缺陷。
- 并发与延迟测试:高峰期(上下班打卡)同时请求压力测试,确保消息队列与 DB 能承受。
- 审核与审计:所有手工改动(补卡、审批)必须有审计日志,便于查账。
- 用户培训与沟通:HR、主管和员工三角沟通,明确规则与申诉通道。
运营期间建议
- 每月固定窗口 HR 与主管核对并锁定考勤(比如每月 5 日前)
- 定期清理 raw 表(只在 raw 存 30 天,历史走归档)
- 自动提醒机制(未打卡、超时审批、调休到期)
十、代码仓与测试建议(快速清单)
- 单元测试:规则引擎(迟到/早退/跨天班次)
- 集成测试:模拟打卡设备、补卡流程、审批流
- 接口文档:使用 OpenAPI/Swagger
- 权限控制:RBAC,主管/HR/财务不同权限
- 数据备份:每日增量 + 每周全备份
在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云人事及OA管理系统,简道云背靠国内BI龙头帆软,在数据处理、数据展示上的能力有绝对优势,数据分析支持高度自定义,任何分析需求都可以快速制作仪表盘,人事及OA管理系统实现了组织人事、考勤、绩效、薪酬、招聘等人事核心模块全面线上化、一体化,业务流程效率提升
十一、示例场景演示(一个完整操作流)
员工小张在客户 A 外勤一天:
- 上午 9:05 在客户现场用手机打卡(带 GPS、拍照) -> 后端判定为外勤有效。
- 下午 18:10 下班前打卡 -> raw 入库 -> worker 合并为当天出勤 9:05 - 18:10,总工时 9 小时。
- 因为中午忘打卡回公司打卡,晚间发现缺卡 -> 小张提交补卡申请(附聊天记录/客户签字照片) -> 主管审批通过 -> HR 最终确认 -> attendance_record 更新并纳入月结工资。
- 小张当月加班 4 小时 -> 提交加班申请并通过 -> 生成可用调休 4 小时(或工资结算),并记录有效期。
十二、常见问题与注意点
- 打卡防作弊很难做到 100%:结合 GPS、照片、人脸与设备绑定,多维度降低代打率。
- 法律合规性:不同国家/地区工时法规不同,做系统时一定和 HR/法务对接。
- 用户体验:打卡尽量简洁,异常说明/审批流透明,减少员工投诉。
十三、FAQ
FAQ 1:如何处理员工忘记打卡但其实到岗了的情况?
忘打卡是最常见的问题。我的建议是先把补卡流程做得方便且有足够凭证:员工可在系统提交补卡申请并上传凭证(如当日客户签字照片、邮件记录、定位信息或主管确认);补卡申请需要走至少一层主管审批(必要时 HR 复核),审批通过后打卡会补入 attendance_record,并在原始 raw 表中保留一条补卡记录以便审计。为了避免被滥用,建议设置补卡时间窗口(例如只能补过去 30 天内的记录),并对频繁补卡的员工设置告警或限制(例如一个月超过 N 次需 HR 面谈)。此外,把“当月最终考勤锁定”日程固定下来(比如月五之前完成确认),超过锁定日的补卡必须额外审批并注明原因。
FAQ 2:外勤打卡如何防止位置伪造?
位置伪造是技术与管理结合的问题。技术上可以做多重校验:一是 GPS 精度检测(拒绝精度极差的定位);二是强制拍照并可对照片进行简单的人脸比对(若公司有这项需求)或要求拍照与客户签字照一起提交;三是记录设备指纹与网络信息(WiFi SSID、基站信息)做辅助判断。管理上建议设计审批链:外勤打卡若被标记为异常(位置偏离预期很远或拍照异常),直接发起主管复核流程;长期存在异常则 HR 干预。此外,允许有些外勤场景为“免位置校验”的白名单(例如既定客户长期合作场景),通过白名单管理节省 False Positive 的审批成本。最终,既要有防作弊的技术能力,也要有合理的容错与人工复核机制。
FAQ 3:如何设计考勤规则以适配多种排班(轮班、跨天、弹性)?
排班复杂度高是考勤系统的痛点。建议先把班次标准化建模:每个 shift 包含上班点、下班点、是否跨天、午休段、允许打卡窗口、是否需要签到等信息。然后再建 roster(排班表),支持周期性规则(例如 2 白 2 夜)或基于模板按日期下发。规则引擎需要能读取 shift 与 roster 同时考虑请假/出差/调休等状态来计算最终出勤。对于弹性制,可配置“核算日工时规则”(如上班窗口 9:00-11:00 均视为上班,不记录迟到,但需保证日工作时长)。上线前务必对历史数据做回测,验证规则在不同场景下的行为,发现边界条件(如夜班跨日、夏令时调整)并补齐逻辑。最后把规则尽量做成可配置(而不是写死在代码里),以便 HR 无需开发就能调整。