MFC+Access考勤管理系统:从课程设计到工程实践
2026/9/18 8:52:34 网站建设 项目流程

简介:这是一份软件工程课程设计报告,主题为员工考勤管理系统的分析与设计,适合软件工程、管理信息系统方向的学生作为课程设计或毕业设计的参考资料。报告基于Visual C++的MFC与Microsoft Access 2000,内容涵盖可行性分析、需求分析、总体设计、数据库结构设计等环节,详细描述了员工信息管理、考勤记录查询与出勤分析等功能模块,并给出了员工信息表、职工考勤表等表结构及系统功能模块图,有助于理解考勤系统的完整开发流程。资源包内仅有1个PDF文件,大小约581KB,内容为报告全文,便于阅读与打印。该资源已有94人学习,对于正在完成类似课设任务或希望快速掌握MFC+Access开发思路的学习者具有较好的参考价值。

1. 一份考勤管理系统的课程设计PDF,藏着MFC+Access的完整套路

前阵子整理旧资料时翻出一份《员工考勤管理系统分析与设计.pdf》,页脚写着课程设计、作者周勇、指导老师卢曼莎。单看题目,像是CS专业里再普通不过的软件工程课设,但顺着正文读下去会发现,它其实串起了一条很完整的桌面应用开发链路:可行性分析、需求分析、Access 2000 数据表设计、VC++ 6.0 的 MFC AppWizard 生成 SDI 工程、ODBC 绑定数据库、以及签到/签离/迟到早退这些具体业务逻辑的实现。现在做考勤系统,大家第一反应是 Web 或 SaaS,几乎不会有人再碰 MFC 和 Access,但把这样一个“老”项目拆开看,反而能把数据表设计、界面框架、业务逻辑和数据库访问之间的边界看得更清楚。这篇就按课程设计文档的顺序,把它从需求到实现完整复现一遍,并补上当年文档里没写清楚的建表语句、关键代码和现在重做时最容易踩的坑。

2. 需求分析与可行性研究:考勤系统先划边界再写代码

课设文档把可行性分析放在需求分析之前,这和软件工程过程的基本顺序一致:先判断有没有必要做、能不能做,再去细化功能边界。这里直接拆成三部分看:功能需求、性能与可靠性需求、可行性三视角分析。

2.1 功能需求:编号、姓名、部门、签到时间、签离时间是主数据线

从文档的“系统综合要求”和两张核心表可以看出,系统必须完成的功能并不复杂:登录验证、员工信息维护、考勤登记、考勤记录查询、迟到早退标记。员工信息表需要的字段是职工编号、职工性别、职工姓名、职工年龄、职业爱好、所在部门;职工考勤表需要的字段是职工编号、职工姓名、职工部门、签到时间、签离时间、迟到否、早退否。

用表格归纳如下:

功能模块涉及字段说明
登录验证账号/密码(对话框)文档中只有一个登录界面对话框,没有细说账号表
员工信息维护职工编号、姓名、性别、年龄、爱好、部门增删改查均围绕员工信息表
考勤登记签到时间、签离时间、迟到否、早退否写入职工考勤表
考勤查询职工姓名、签到时间、签离时间等文档给出的是按姓名顺序查找
迟到/早退标记迟到否、早退否用文本“是/否”标记

在需求阶段可以把验收场景写成类似 Gherkin 的描述,方便后续对照测试:

# attendance.feature Feature: 员工签到登记 Scenario: 员工正常签到 Given 系统已通过 ODBC 连接“考勤管理系统数据库” When 考勤人员触发签到操作 Then 系统读取当前时间写入签到时间字段 And 若当前时间晚于规定上班时间,迟到标记置为“是”

这里用 Gherkin 只是为了对齐语义,并不是说 MFC 工程要跑 BDD。需求阶段最重要的产出是字段级约定,比如“迟到否”到底是布尔型还是文本型、签到时间用 DATETIME 还是字符串,这两点不敲定,后面表结构和代码都会返工。

2.2 性能、可靠性与出错处理需求:单机版也要关心数据一致

课设文档写的是“Windows XP、VC++、Microsoft Access 2000”,说明这是一个单机或局域网共享文件模式的桌面系统。性能需求方面,课程设计级别不需要压测指标,但从表结构可以推断出数据规模:员工几百人、考勤记录按天累积。容量约束主要是 Access 2000 单文件上限 2GB,对课设来说完全够用。

可靠性需求主要体现在数据库文件的备份和恢复上。Access 是文件型数据库,直接复制.mdb文件就能备份,但代价是并发写入能力弱。如果多个考勤人员同时操作,Jet 引擎会出现锁冲突,所以文档里没有把“多用户同时写入”作为目标。出错处理方面,原文档的流程图里已经出现了“已经登记了”的判断,说明系统需要考虑重复签到的情况;此外还要处理“对不起,无此人”这类查询失败提示。更严谨的做法是给职工考勤表增加“职工编号 + 日期”复合唯一索引,防止同一个人同一天被插入多条签到记录,但原文档在表结构里没有体现这一点。

2.3 可行性三视角:为什么是 VC++ MFC + Access 2000

从经济可行性看,当时 Windows 和 Office 是企业标配,VC++ 6.0 和 Access 2000 是现成工具,开发与部署成本都很低。从技术可行性看,MFC 的 CRecordset 封装了 ODBC,开发者不需要手写大量数据库访问代码;Access 2000 的 Jet 引擎支持 SQL 和索引,足够支撑考勤这种轻量级业务。从法律可行性看,项目使用的是有授权的开发工具链即可,避免把未经授权的软件带入交付物。

这个选型放到今天看并不先进,但它回答了一个关键问题:为什么是“SDI 而不是 Dialog”?因为 SDI 自带 Document/View 架构,文档类适合持有 CRecordset,视图类负责表单展示。考勤系统既有员工信息对话框,又有出勤状况对话框,在 SDI 里切换和扩展都比把全部控件堆在单个对话框里更清晰。理解了这一层,后面看 MFC AppWizard 的配置步骤就不会觉得只是机械点下一步。

3. Access 2000 考勤库设计:两张表的字段、索引与 DDL

考勤管理系统的核心是表结构。原文档用表格手工画了“员工信息表”和“职工考勤表”,但没有给出完整的 CREATE TABLE 语句。为了能在一台干净的 Windows 环境里复现,这里按 Access 2000 / Jet SQL 把两张表补全,并说明字段类型和约束的取舍。

3.1 员工信息表:长整型主键与必填文本字段

原文档中“员工信息数据表”的结构如下:

字段名称数据类型字段大小索引/约束必填
职工编号数字长整型主键/唯一
职工性别文本2-
职工姓名文本20-
职工年龄数字长整型-
职业爱好文本30-
所在部门文本30-

职工编号用长整型数字做主键,是因为员工编号是自然编号,而且 Access 自增字段在 ODBC 写入时有时需要特殊处理,用外部传入的长整型更直接。字符串字段按文档给的大小就行,但注意 Access 2000 的TEXT(20)在 Jet 里表示 20 个字符宽度,中文不会按 2 个字节截断。实际通过 ODBC 读写时,字符集和字段长度在极端字符环境下可能产生空格补齐的问题,所以姓名给 20 个字符宽度是合理的,部门建议给到 30。

3.2 职工考勤表:DATETIME 字段与文本“是/否”的取舍

职工考勤表重点看“迟到否”和“早退否”两个字段。文档把它们定义为文本类型,而不是 Access 的“是/否”布尔类型。原因是 VC++ 6.0 的 CRecordset 通过 ODBC 读取 Jet 布尔字段时,返回的是BOOL,在某些 ODBC 驱动下会得到 -1 或 0,和 MFC 的BOOL混用容易出问题。

写成TEXT(10)字段后,MFC 侧直接映射为CString,判断时用== "是"即可,代码直观且不容易踩类型坑。这是一种“牺牲规范换稳定”的做法,对于课程设计这种规模完全值得。缺点是统计迟到次数时要对字符串做比较,不能直接用 SQL 的SUM

3.3 用 Jet SQL 在 Access 中执行建表

在 Access 2000 的查询窗口里可以直接执行以下 DDL:

CREATE TABLE [员工信息] ( [职工编号] LONG NOT NULL PRIMARY KEY, [职工姓名] TEXT(20) NOT NULL, [职工性别] TEXT(2) NOT NULL, [职工年龄] LONG NULL, [职业爱好] TEXT(50) NULL, [所在部门] TEXT(30) NOT NULL ); CREATE TABLE [职工考勤表] ( [自动编号] COUNTER PRIMARY KEY, [职工编号] LONG NOT NULL, [职工姓名] TEXT(20) NOT NULL, [职工部门] TEXT(30) NOT NULL, [签到时间] DATETIME NULL, [签离时间] DATETIME NULL, [迟到否] TEXT(10) NULL, [早退否] TEXT(10) NULL );

这里比原文档多增加了一个[自动编号]字段,因为同一职工会有多条考勤记录,单独用职工编号做主键会造成主键冲突。COUNTER是 Jet SQL 的自增类型,适合作为考勤表的物理主键。如果想让“同一员工同一天只能有一条签到记录”,可以再创建唯一索引:

CREATE UNIQUE INDEX IX_Attend_Unique ON [职工考勤表] ([职工编号], [签到时间]);

不过原文档没有做这一步,实际实现时靠代码先判断“是否已经登记”来控制,二选一即可。注意 Jet SQL 中中文表名和字段名最好用方括号括起来,避免被当成保留字。

4. MFC SDI + ODBC:搭建单文档考勤程序框架

建好数据库后,下一步是在 VC++ 6.0 里通过 MFC AppWizard 生成 SDI 工程。原文档的“程序概要设计”已经写得很清楚:单文档界面(SDI),封面资源是IDD_KQGLSYS_FORM,ODBC 数据源名为“考勤管理系统数据库”,绑定表为“职工考勤表”。这一章把配置步骤和关键生成文件说明完整。

4.1 为什么选 SDI 而不是基于对话框

很多人做课设时,第一步就会选“基于对话框”,因为界面布局所见即所得,代码量少。但对于考勤系统,SDI 的 Document/View 架构更合适。文档类(CKQGLSYSDoc)持有CRecordset派生对象,视图类(CKQGLSYSView)从CRecordView继承,直接把数据库字段映射到对话框控件。这样员工信息对话框和出勤状况对话框可以共存于同一个框架,配合菜单和工具栏切换。

MFC AppWizard 生成 SDI 工程时的默认类名如下:

文件名基类
CKQGLSYSAppKQGLSYS.h / KQGLSYS.cppCWinApp
CMainFrameMainFrm.h / MainFrm.cppCFrameWnd
CKQGLSYSDocKQGLSYSDoc.h / KQGLSYSDoc.cppCDocument
CKQGLSYSViewKQGLSYSView.h / KQGLSYSView.cppCRecordView
CKQGLSYSSetKQGLSYSSet.h / KQGLSYSSet.cppCRecordset

这套结构对后来维护者来说非常清晰:看到CKQGLSYSSet就知道它对应数据库表,看到CKQGLSYSView就知道界面在这里。

4.2 MFC AppWizard 的关键配置

复现步骤如下:

  1. 打开 VC++ 6.0,选择“文件 / 新建 / MFC AppWizard(EXE)”,工程名输入KQGLSYS
  2. 在 Step 1 选择“单个文档”,资源语言选择“中文[中国]”。
  3. 在 Step 2 选择“数据库查看,不使用文件支持”,然后点击Data Source按钮。
  4. Database Options里选择 ODBC,数据源名选择“考勤管理系统数据库”,记录集类型选Snapshot
  5. 选择要绑定的表[职工考勤表],点击 OK,完成向导。

这里的核心是第 3 步。所谓“不使用文件支持”,是指 MFC 不需要CArchive序列化,因为数据全部存在 Access 数据库里。如果选错了,向导会生成一堆Serialize相关代码,反而干扰数据库视图的使用。

4.3 CRecordset 派生类与连接字符串

向导会自动生成CKQGLSYSSet,但我们需要把关键成员变量补上,并按原文档的命名习惯对应字段:

// KQGLSYSSet.h class CKQGLSYSSet : public CRecordset { public: CKQGLSYSSet(CDatabase* pDatabase = NULL); virtual CString GetDefaultConnect(); virtual CString GetDefaultSQL(); long m_ZGBH; // 职工编号 CString m_ZGXM; // 职工姓名 CString m_ZGBM; // 职工部门 CString m_QDTIME; // 签到时间 CString m_QLTIME; // 签离时间 CString m_CDF; // 迟到否 CString m_ZTF; // 早退否 };
// KQGLSYSSet.cpp CString CKQGLSYSSet::GetDefaultConnect() { return _T("ODBC;DSN=考勤管理系统数据库;"); } CString CKQGLSYSSet::GetDefaultSQL() { return _T("[职工考勤表]"); }

GetDefaultConnect返回的是 ODBC 连接字符串,DSN名称必须和控制面板里配置的用户数据源一致。如果系统是 64 位 Windows,而 VC++ 6.0 编译出来的是 32 位程序,要使用 32 位 ODBC 管理器来创建 DSN,否则会报找不到数据源。GetDefaultSQL返回表名,用方括号包住中文表名,能避免解析歧义。

在视图的OnInitialUpdate中,把记录集与视图关联:

void CKQGLSYSView::OnInitialUpdate() { m_pSet = &GetDocument()->m_KQGLSYSSet; CRecordView::OnInitialUpdate(); }

这样视图上的控件就自动绑定到m_pSet对应的字段,MFC 的DDX_FieldText机制会把控件与记录字段关联起来。原文中提到的IDD_KQGLSYS_FORM就是这个视图使用的对话框资源模板。

5. 签到、签离与姓名查询:核心流程的 C++ 实现

框架搭好后,真正体现考勤逻辑的是签到时间写入、迟到早退判定和按姓名查询这三段代码。原文档用流程图描述了过程,这里直接转成可编译的 MFC 代码,并说明边界条件。

5.1 用 CTime::GetCurrentTime 生成签到时间

签到操作的核心是获取当前系统时间,写入记录集,并和上班时间比较:

void CKQGLSYSView::OnSignIn() { CTime now = CTime::GetCurrentTime(); CString strNow = now.Format(_T("%H:%M:%S")); CString strDateTime = now.Format(_T("%Y-%m-%d %H:%M:%S")); m_pSet->m_QDTIME = strDateTime; // 假设上班时间为 09:00:00 if (strNow > _T("09:00:00")) m_pSet->m_CDF = _T("是"); else m_pSet->m_CDF = _T("否"); m_pSet->Update(); UpdateData(FALSE); }

CTime::GetCurrentTime()返回的是本地系统时间,不需要自己处理时区。Format把时间转成字符串,%H:%M:%S是 24 小时制的时分秒。这里直接拿字符串和"09:00:00"比较,因为固定宽度且顺序就是字典序,在 24 小时制下是可靠的。如果业务涉及跨天倒班,这种比较就会出错,需要先解析成CTime再比较时间戳,但对于课设的固定白班场景,这种简化可以接受。

5.2 签离时间与早退判定

签离逻辑和签到对称:

void CKQGLSYSView::OnSignOut() { CTime now = CTime::GetCurrentTime(); CString strNow = now.Format(_T("%H:%M:%S")); CString strDateTime = now.Format(_T("%Y-%m-%d %H:%M:%S")); m_pSet->m_QLTIME = strDateTime; // 假设下班时间为 18:00:00 if (strNow < _T("18:00:00")) m_pSet->m_ZTF = _T("是"); else m_pSet->m_ZTF = _T("否"); m_pSet->Update(); UpdateData(FALSE); }

这里有一个容易被忽略的问题:签离时可能还没有定位到正确的记录。如果用户先查出一名员工,再点签离,当前记录就是该员工当天的考勤记录;如果用户直接新建记录,那 m_pSet 的位置可能不对。常见做法是先把“职工编号 + 日期”作为筛选条件定位到记录,再执行签离更新。原文档没有详细写这一步,但实际调试中很容易出现“签离写到了别的员工名下”。

5.3 按姓名顺序查找记录与 EOF 处理

原文档流程图里最经典的就是这段顺序查找逻辑:输入姓名,从首记录开始移动游标,逐条比较,直到 EOF 结束。

void CKQGLSYSView::OnQuery() { CString strName; m_editName.GetWindowText(strName); m_pSet->MoveFirst(); BOOL bFound = FALSE; while (!m_pSet->IsEOF()) { if (m_pSet->m_ZGXM == strName) { bFound = TRUE; break; } m_pSet->MoveNext(); } if (!bFound) { AfxMessageBox(_T("对不起,无此人")); return; } m_QTIME = m_pSet->m_QDTIME; m_QLTIME = m_pSet->m_QLTIME; m_CDF = m_pSet->m_CDF; m_ZTF = m_pSet->m_ZTF; UpdateData(FALSE); }

MoveFirst把游标移到结果集第一条,IsEOF()指示是否越过了最后一条记录。每次MoveNext后判断 EOF,可以避免在最后一条记录上再取字段值导致异常。查到目标记录后,把记录集字段赋值给视图成员变量,UpdateData(FALSE)刷新界面。顺序查找的时间复杂度是 O(n),员工人数达到几万时会有明显卡顿,所以下一章会改为参数化查询。

状态字段的对应关系如下:

MFC 成员变量数据库字段取值
m_QDTIME签到时间yyyy-MM-dd HH:mm:ss
m_QLTIME签离时间yyyy-MM-dd HH:mm:ss
m_CDF迟到否“是”或“否”
m_ZTF早退否“是”或“否”

6. 交完课设之后:把旧考勤系统改造成能继续用的样子

原文档的结尾做了面向过程和面向对象设计的比较,这部分理论没有问题,但没有给出实际优化方向。这里补三个在维护和重做这个系统时最值得做的事。

6.1 32 位 DSN 和 64 位 Windows 的坑

VC++ 6.0 编译出来的 MFC 程序是 32 位的。在 64 位 Windows 10/11 上,控制面板看到的“ODBC 数据源管理器(64 位)”不会对 32 位程序生效。必须用C:\Windows\SysWOW64\odbcad32.exe打开 32 位版本,创建用户 DSN“考勤管理系统数据库”。否则CRecordset打开时会报“找不到数据源名称且没有指定默认驱动程序”。

6.2 用 ADO 替代 ODBC,去掉 DSN 依赖

如果不想每台机器都手工配 DSN,可以用 ADO 直接连接 Access 文件:

_ConnectionPtr pConn; pConn.CreateInstance(__uuidof(Connection)); pConn->Open( _T("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\data\\考勤管理系统.mdb;"), _T(""), _T(""), adConnectUnspecified);

Jet 4.0 OLEDB 驱动可以直接打开 Access 2000 格式的.mdb文件,不需要 ODBC DSN,部署时只要保证目标机器有对应驱动。注意 Access 2000 文件在 64 位 Office 环境里可能打不开,如果只是查看数据,可以先用新版 Access 另存为 Access 2003 或.accdb格式,再换用ACE.OLEDB驱动连接。

6.3 参数化查询代替 MoveNext 循环

姓名查询如果改成 SQL 过滤,代码更简洁且性能更好:

m_pSet->m_strFilter = _T("[职工姓名] = ?"); m_pSet->m_paramName = strName; m_pSet->Requery();

m_strFilterCRecordset提供的过滤条件,问号是参数占位符;m_paramName需要在记录集类里定义为与字段类型匹配的成员变量,并在DoFieldExchange中通过RFX_Text绑定。Requery会重新执行查询,让 Jet 引擎在数据库端完成筛选,返回的结果集只包含匹配记录,查询速度比 MoveNext 循环快一个量级。

如果要在课设基础上增加月报统计,可以在 Access 里建一个按职工编号分组的查询,统计迟到次数和早退次数:

SELECT 职工编号, SUM(IIF(迟到否 = "是", 1, 0)) AS 迟到次数 FROM 职工考勤表 GROUP BY 职工编号;

再把该查询作为记录集数据源绑定到CRecordset,就能复用现有 MFC 视图,不需要额外写列表控件。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询