如何基于双表查询展示员工月度考勤数据(含首次签到/末次签退)
解决方案
嘿,我帮你梳理下这个考勤查询的问题哈~你现在需要展示所有员工的月度每日考勤,要取当天最早的签到、最晚的签退,还要处理忘签的情况对吧?先明确下你的表结构,方便后续理解:
你的表结构
TblEmployee(考勤打卡记录表)
| attendance_id | employee_id | Scan_type | Date_and_time |
|---|---|---|---|
| 1 | 1 | IN | 15-Jan-19 8:00:00 AM |
| 2 | 1 | IN | 15-Jan-19 8:00:02 AM |
| 3 | 2 | IN | 15-Jan-19 8:05:01 AM |
| 4 | 2 | OUT | 15-Jan-19 4:00:00 PM |
| 5 | 1 | IN | 16-Jan-19 8:05:30 AM |
AttendanceTable(员工信息表)
| emp_id | emp_name |
|---|---|
| 1 | Salman |
| 2 | Tahir |
| 3 | Jameel |
你的现有代码分开查询了IN和OUT列表,但后续合并会比较繁琐,咱们可以用分组取极值+交叉连接+左连接的方式一次性完成,既满足需求又更高效。
具体实现步骤
1. 先提取每个员工每日的有效打卡记录
我们先对打卡记录按「员工ID+日期+打卡类型」分组,直接取出每个员工每天的首次IN和末次OUT:
// 获取每个员工每日的最早签到记录 var dailySignIns = _context.TblEmployee .Where(att => att.Scan_type == "IN") .GroupBy(att => new { att.employee_id, AttDate = att.Date_and_time.Date }) .Select(g => new { EmployeeId = g.Key.employee_id, g.Key.AttDate, SignInTime = g.Min(att => att.Date_and_time) // 取当日最早的IN时间 }); // 获取每个员工每日的最晚签退记录 var dailySignOuts = _context.TblEmployee .Where(att => att.Scan_type == "OUT") .GroupBy(att => new { att.employee_id, AttDate = att.Date_and_time.Date }) .Select(g => new { EmployeeId = g.Key.employee_id, g.Key.AttDate, SignOutTime = g.Max(att => att.Date_and_time) // 取当日最晚的OUT时间 });
2. 生成目标月份的所有日期
要展示整个月度的记录,得先拿到这个月的所有日期(这里以2019年1月为例,你可以根据需求动态调整):
// 目标月份,可根据实际需求传入 var targetMonth = new DateTime(2019, 1, 1); // 获取当月总天数 var totalDays = DateTime.DaysInMonth(targetMonth.Year, targetMonth.Month); // 生成当月所有日期的列表 var allMonthDates = Enumerable.Range(1, totalDays) .Select(day => new DateTime(targetMonth.Year, targetMonth.Month, day)) .ToList();
3. 关联所有员工、日期和打卡记录
把所有员工和当月所有日期做交叉连接,再左连接刚才的签到/签退记录,这样就能保证每个员工的每一天都有记录,哪怕没有打卡:
var monthlyAttendanceResult = from emp in _context.AttendanceTable from date in allMonthDates // 左连接签到记录 join signIn in dailySignIns on new { EmpId = emp.emp_id, date } equals new { signIn.EmployeeId, signIn.AttDate } into signInGroup from matchedSignIn in signInGroup.DefaultIfEmpty() // 左连接签退记录 join signOut in dailySignOuts on new { EmpId = emp.emp_id, date } equals new { signOut.EmployeeId, signOut.AttDate } into signOutGroup from matchedSignOut in signOutGroup.DefaultIfEmpty() select new { EmployeeId = emp.emp_id, EmployeeName = emp.emp_name, AttendanceDate = date.ToString("yyyy-MM-dd"), // 没有签到记录就显示absent,否则格式化时间 SignIn = matchedSignIn?.SignInTime.ToString("HH:mm:ss") ?? "absent", // 没有签退记录就显示absent,否则格式化时间 SignOut = matchedSignOut?.SignOutTime.ToString("HH:mm:ss") ?? "absent" };
4. 可选:调整输出格式
如果需要把时间显示成更友好的格式,或者把absent换成空字符串,直接修改上面的SignIn和SignOut字段即可。
核心逻辑说明
- 分组取极值:用
GroupBy+Min/Max比排序取第一条更高效,数据库层面就能完成计算,减少内存消耗。 - 交叉连接员工与日期:确保了哪怕员工当月完全没打卡,每一天的考勤记录都会显示出来,满足「所有员工月度考勤」的要求。
- 左连接处理空值:
DefaultIfEmpty()保证没有对应打卡记录时不会过滤掉该条数据,而是返回null,再用??运算符替换成你需要的提示文本。
内容的提问来源于stack exchange,提问作者Faisal Amin
相关产品推荐
相关产品推荐

