Làm rõ quy mô: 5.000 nhân viên, mỗi người 2-4 lần chấm công/ngày → khoảng 20.000 bản ghi/ngày, dồn vào hai khung giờ ngắn. Tổng tải rất nhỏ; phần khó nằm ở tính đúng đắn và quy tắc nghiệp vụ, không ở scale.
Data model:
attendance_events(id, employee_id, ts timestamptz, kind, source, device_id,
lat, lng, dedupe_key unique)
work_shifts(employee_id, work_date, shift_start, shift_end)
timesheets(employee_id, work_date, first_in, last_out, worked_minutes, status)Giữ attendance_events là bản ghi thô chỉ ghi thêm — không sửa, không xoá. timesheets là bảng kết quả do job tính lại từ event. Cách này cho phép chạy lại khi quy tắc thay đổi và giữ được dấu vết khi có tranh chấp lương.
Những trường hợp thực tế phải xử lý:
- Chấm công trùng: nhân viên bấm hai lần → dedupe_key = employee + phút + kind với unique index, hoặc bỏ qua event cách nhau dưới 1 phút.
- Offline: máy quét/app mất mạng → lưu cục bộ và gửi lại sau, nên server phải nhận ts do thiết bị ghi chứ không lấy giờ nhận request; kèm receivedAt để phát hiện lệch.
- Ca đêm vắt qua nửa đêm → work_date là ngày của ca, không phải ngày của timestamp.
- Chấm hộ → ràng buộc bằng vị trí/thiết bị/ảnh, và ghi source để audit.
Kiến trúc: một API nhận event, ghi Postgres, job chạy cuối ngày (và chạy lại theo yêu cầu) dựng timesheets; xuất bảng lương đọc từ timesheets. Không cần queue hay cache ở quy mô này.
Đánh đổi: tách event thô và bảng tổng hợp làm tăng một bước xử lý, nhưng đây là dữ liệu liên quan tới lương — khả năng tính lại và giải trình quan trọng hơn việc tiết kiệm một bảng.