You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于双表查询展示员工月度考勤数据(含首次签到/末次签退)

解决方案

嘿,我帮你梳理下这个考勤查询的问题哈~你现在需要展示所有员工的月度每日考勤,要取当天最早的签到、最晚的签退,还要处理忘签的情况对吧?先明确下你的表结构,方便后续理解:

你的表结构

TblEmployee(考勤打卡记录表)

attendance_idemployee_idScan_typeDate_and_time
11IN15-Jan-19 8:00:00 AM
21IN15-Jan-19 8:00:02 AM
32IN15-Jan-19 8:05:01 AM
42OUT15-Jan-19 4:00:00 PM
51IN16-Jan-19 8:05:30 AM

AttendanceTable(员工信息表)

emp_idemp_name
1Salman
2Tahir
3Jameel

你的现有代码分开查询了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:08:21