考勤是 HR 的基础数据来源:薪资、绩效、审批都和它绑得死死的。很多公司起初用 Excel 或打卡机拼凑,但随着人员增多、异地/外勤增多、弹性工作制兴起,单纯人工就维护不起了。做个靠谱的考勤模块,可以:
简单说:人事(HR)模块负责员工档案、合同、薪酬、绩效等;OA(办公自动化)负责请假、审批、公告、流程审批等。两者常结合成一套平台,考勤板块就是连接 HR(数据主体)和 OA(流程/审批)的桥梁。
下面给一个简化的架构图(文本版):
swift
┌────────────┐
│ 门禁/打卡机 │
└─────┬──────┘
│打卡数据
┌──────────┐ ┌────▼────┐ ┌────────────┐
│ 手机APP │◀────│ API 网关│────▶│ 后端微服务 │
│(Android/IOS)│ └────┬────┘ │ - 考勤服务 │
└──────────┘ │ │ - 审批服务 │
│ └────┬───────┘
┌─────▼────┐ │
│ 第三方集成│(企业微信/钉钉/门禁)│
└─────┬────┘ │
│ 消息/回调 │
┌─────▼────┐ ┌───▼────┐
│ 消息队列 │ │ 数据库 │
│ (Kafka/RabbitMQ) │ │ (MySQL) │
└─────┬────┘ └────────┘
│
┌─────▼────┐
│ 报表/BI │(OLAP/Redis缓存)
└─────────┘
技术栈建议:
列出你要求的所有功能,并做一点补充:
我用文字 + 简单流程图表示关键流程,便于复制到画图工具生成正式图。
流程: 员工 -> 手机/门禁机打卡 -> 打卡数据入队列 -> 解析(设备ID/员工ID/时间/位置/照片)-> 写入 raw_table -> 实时规则引擎判定(是否迟到/早退/缺卡)-> 写入考勤记录表 -> 通知员工(异常提醒)
简化流程图(ASCII):
css
[员工打卡] -> [API网关] -> [消息队列] -> [打卡解析服务] -> [raw_attendance] -> [规则引擎] -> [attendance_record]
关键要点:
流程: 员工在外勤范围或客户处,通过手机提交带GPS和拍照的打卡 -> 后端校验 GPS 与预定义坐标(或基于半径) -> 记录为外勤打卡(可选: 关联客户/项目)
要点:
流程: 员工 -> 补卡申请(选择缺卡记录或手填时间)-> 提交理由与附件 -> 送审批(主管/HR)-> 审批通过后,系统把原缺卡替换/标注 -> 考勤重新计算
要点:
通用流程: 员工填写申请 -> 系统初步校验(余额/规则) -> 流程引擎流转(主管/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
);
索引建议:
说明:下面代码是简化示例,生产环境需加权限校验、参数校验、日志、限流、幂等处理等。
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 });
}
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]);
}
}
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 });
}
重点展示打卡组件与外勤打卡带位置校验。
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 <button onClick={()=>handleClock()}>立即打卡</button>;
}
考勤的复杂度往往来自规则:排班、多班次、跨日班次、节假日、工时折算、加班与调休换算。给出几个关键策略:
常见报表:月度出勤汇总、异常明细、部门加班汇总、个人考勤明细、请假统计。下面给个月度出勤汇总 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;
做大数据量报表建议:
实施考勤系统不仅是写代码,还要做好推广、培训、与现有系统(薪资、门禁、OA)对接。
在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云人事及OA管理系统,简道云背靠国内BI龙头帆软,在数据处理、数据展示上的能力有绝对优势,数据分析支持高度自定义,任何分析需求都可以快速制作仪表盘,人事及OA管理系统实现了组织人事、考勤、绩效、薪酬、招聘等人事核心模块全面线上化、一体化,业务流程效率提升
员工小张在客户 A 外勤一天:
忘打卡是最常见的问题。我的建议是先把补卡流程做得方便且有足够凭证:员工可在系统提交补卡申请并上传凭证(如当日客户签字照片、邮件记录、定位信息或主管确认);补卡申请需要走至少一层主管审批(必要时 HR 复核),审批通过后打卡会补入 attendance_record,并在原始 raw 表中保留一条补卡记录以便审计。为了避免被滥用,建议设置补卡时间窗口(例如只能补过去 30 天内的记录),并对频繁补卡的员工设置告警或限制(例如一个月超过 N 次需 HR 面谈)。此外,把“当月最终考勤锁定”日程固定下来(比如月五之前完成确认),超过锁定日的补卡必须额外审批并注明原因。
位置伪造是技术与管理结合的问题。技术上可以做多重校验:一是 GPS 精度检测(拒绝精度极差的定位);二是强制拍照并可对照片进行简单的人脸比对(若公司有这项需求)或要求拍照与客户签字照一起提交;三是记录设备指纹与网络信息(WiFi SSID、基站信息)做辅助判断。管理上建议设计审批链:外勤打卡若被标记为异常(位置偏离预期很远或拍照异常),直接发起主管复核流程;长期存在异常则 HR 干预。此外,允许有些外勤场景为“免位置校验”的白名单(例如既定客户长期合作场景),通过白名单管理节省 False Positive 的审批成本。最终,既要有防作弊的技术能力,也要有合理的容错与人工复核机制。
排班复杂度高是考勤系统的痛点。建议先把班次标准化建模:每个 shift 包含上班点、下班点、是否跨天、午休段、允许打卡窗口、是否需要签到等信息。然后再建 roster(排班表),支持周期性规则(例如 2 白 2 夜)或基于模板按日期下发。规则引擎需要能读取 shift 与 roster 同时考虑请假/出差/调休等状态来计算最终出勤。对于弹性制,可配置“核算日工时规则”(如上班窗口 9:00-11:00 均视为上班,不记录迟到,但需保证日工作时长)。上线前务必对历史数据做回测,验证规则在不同场景下的行为,发现边界条件(如夜班跨日、夏令时调整)并补齐逻辑。最后把规则尽量做成可配置(而不是写死在代码里),以便 HR 无需开发就能调整。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。