简介这份资料包面向使用 C# 与 VB.NET 开发中控考勤机对接程序的开发者围绕设备连接、人员管理、考勤记录读写与数据管理等常见需求提供 SDK、接口文档和可参考的示例工程。压缩包共 1264 个文件约 11.08MB以 221 个 cs 源码、214 个 dll 库、133 个 exe 程序为主并配有 txt 说明、doc/docx 文档、resx 资源、mdb/db 数据库及 sln/csproj 工程文件便于按模块查找接口调用、界面实现和部署依赖。已有 2623 人学习下载说明其在同类设备二次开发中具有一定的参考覆盖面。读者可从中了解 SDK 初始化、TCP/IP 通讯、用户与考勤规则设置、记录上传下载、报表与异常处理等实现思路并借助现成示例缩短从接口理解到工程搭建的过程。1. 一台每天几百人打卡的机器想取数据时没有现成接口需求往往在周五下午出现行政说考勤机的数据要接进内部系统设备已经安装在车间门口既没有后台账号也没有接口文档只有设备面板上那串 IP。自己写程序从零去猜通讯协议基本上是白费功夫。这个开发包解决的就是这个问题厂商标配的 SDK 加一份 C# 封装让你在自己的程序里直接连接设备、读取打卡流水、下发人员与指纹把硬件当成一个可调用的服务。适合正在做企业内部考勤对接、门禁联动的开发者尤其是手里只有 C# 技术栈、又不想自己实现底层通讯协议的人。设备用的是标准通讯协议端口固定为 4370包里的 Demo 已经覆盖连接、监听、拉取、下发四类操作。2. 压缩包拆开三个 DLL、两套文档和 Demo 目录的真实分工拿到这类开发包第一件事是先把目录分清楚因为里面通常混着三种不同的 SDK。它们的用途完全不一样如果混用就会白白花掉半天时间。2.1 目录结构与文件清单考勤机开发包/ ├─ 01_文档/ │ ├─ 独立SDK开发手册.pdf # 主力接口参数全在这 │ ├─ 设备通讯协议说明.pdf # 想自己写协议时看 │ ├─ PULLSDK开发指南.pdf # 设备主动推送方案 │ └─ 错误码对照表.xlsx ├─ 02_SDK/ │ ├─ x86/zkemkeeper.dll # COM 组件独立 SDK 核心 │ ├─ x64/zkemkeeper.dll │ └─ 指纹仪/libzkfpcsharp.dll # 指纹采集器用的跟考勤机无关 ├─ 03_Demo/ │ ├─ CSharp_连接与实时事件/ │ ├─ CSharp_批量拉取打卡记录/ │ ├─ CSharp_人员与指纹下发/ │ └─ CSharp_指纹采集器Demo/ ├─ 04_工具/ # 搜设备、升级固件、导入导出 └─ 05_驱动/这里最容易混淆的是两个 DLL。zkemkeeper.dll是一个 COM 组件注册后在 C# 里以zkemkeeper命名空间出现负责跟考勤机通信走 TCP 4370 端口。libzkfpcsharp.dll负责驱动插在电脑 USB 口上的指纹采集盒跟考勤机本身没关系。很多人拿到包后就把两个 DLL 都拷进项目结果编译时报一串找不到入口点的错误原因就是把两套东西混在了一起。只做联网考勤对接第二个 DLL 根本不用碰。文档也有两份要分开看。独立 SDK 手册讲的是本文主要用的这套接口PULL SDK 是另一种思路设备主动把记录推送到你指定的服务端客户端不用一直连接着设备。设备数量多、又希望实时性好的场景后一种更省事因为它不占用连接数、也不怕网络抖动代价是需要在设备菜单里配置服务器地址。提示先翻错误码对照表那页。这个 SDK 的接口几乎都只返回 bool失败的具体原因全靠GetLastError取出来而错误码表里的描述往往是“操作失败”这种模糊措辞提前看一遍能省掉后来的猜测。2.2 COM 组件的注册写代码之前必须过的第一关zkemkeeper.dll不是普通的托管 DLL不能靠“添加引用”解决必须先用regsvr32注册到系统。这里有一个容易出错的地方即使你的开发机是 64 位系统而 Visual Studio 编译出的进程是 32 位也必须用 32 位的注册程序。# 64 位系统上注册 32 位 COM 组件必须走 SysWOW64 那个 C:\Windows\SysWOW64\regsvr32.exe D:\考勤机开发包\02_SDK\x86\zkemkeeper.dll # 如果进程确定是 64 位再用这个 C:\Windows\System32\regsvr32.exe D:\考勤机开发包\02_SDK\x64\zkemkeeper.dll执行命令后应该弹出“已成功”的对话框。没弹框就是路径写错或者没用管理员权限运行 CMD。注册完之后回到 Visual Studio在“引用”里点右键选择“添加引用”到 COM 页签搜索zkemkeeper勾上即可。引用属性里的“嵌入互操作类型”要改成False否则有些事件签名在运行时会对不上。然后是最关键的一步很多人卡在这里项目的平台目标不能是AnyCPU。把“生成”页里的平台目标改成x86或者干脆把AnyCPU的“首选 32 位”勾上。COM 互操作按进程位数走64 位进程去找 32 位注册表项必然报0x80040154。这个错误我见过太多次每次都有人花两小时去查代码其实是项目属性里一个下拉框。2.3 一份最小可运行的连接测试在往项目里塞业务逻辑之前先写二十行代码确认通道是通的。Demo 目录里那份完整例子反而容易让人分心因为里面掺了界面代码。using System; using zkemkeeper; class Program { static void Main() { var zk new CZKEMClass(); // 设备上设过通讯密码就要填一样的值没设过传 0 zk.SetCommPassword(0); // 4370 是这类设备的固定通讯端口IP 用面板上查到的那个 if (!zk.Connect_Net(192.168.1.201, 4370)) { int err 0; zk.GetLastError(1, ref err); // 第一个参数是机器号单机填 1 Console.WriteLine($连接失败错误码 {err}); return; } Console.WriteLine(连接成功); // 批量操作前先禁用设备避免读写交叉 zk.EnableDevice(1, false); int userCount 0, logCount 0; zk.GetDeviceStatus(1, 1, ref userCount); // 1 用户数 zk.GetDeviceStatus(1, 5, ref logCount); // 5 考勤记录数 Console.WriteLine($设备内用户 {userCount} 人记录 {logCount} 条); zk.EnableDevice(1, true); zk.Disconnect(); } }Connect_Net返回 bool成功即建立了一条长连接后续所有接口都在这个连接上执行。SetCommPassword必须在连接前调用连接之后再设无效这是文档里一句带过但极容易忽略的细节。GetDeviceStatus的第二个参数是状态码1 到 6 分别对应用户数、指纹数、密码数、操作日志数、考勤记录数和当前记录指针。各家固件对这几个码的定义略有出入第一次接入时把这六个数全部打印出来对着设备菜单里的统计页核对一遍比读文档快。EnableDevice(1, false)的作用是让设备暂时不接受面板操作。批量拉取几千条记录时如果不禁用用户在面板上按一下拉取循环就可能被卡住或者漏掉记录。用完一定要恢复成true否则设备看起来就像死机了一样。3. 连接与实时事件一次打卡从设备到 C# 回调的完整链路拉取记录这种“事后补数据”的方式很多场景不够用门禁联动要求刷卡瞬间就开门车间大屏要求打卡立刻刷新。这类需求靠轮询实现既浪费又慢必须用 SDK 的事件回调。3.1 Connect_Net 背后发生的事Connect_Net(ip, port)建立的是标准的 TCP 长连接设备侧对这个连接有数量上限一般在个位数到十几之间具体看型号。所以正确做法是整个应用共用一个CZKEMClass实例封装成单例或者依赖注入里的 Singleton 服务不要每来一个请求就 new 一个。我见过一个 Web 项目在每次接口调用里 new 一遍、连接、取数、断开上线三天就把设备连接池占满面板直接提示通讯繁忙。通讯密码Comm Key是设备菜单里的一个数字默认为 0。它的作用只是防止别人随便连接不参与加密。如果现场有人改过你连接时会一直返回 false错误码也看不出所以然这属于典型的“玄学”故障——代码没错、网络也通就是一个数字对不上。机器号Machine Number是另一个容易忽略的参数。单台设备基本都是 1但如果是通过 RS485 组网的设备每台有独立的机器号Connect_Net之后所有接口的第一个参数都得传对应的号。传错了不会报错只会安静地返回 false查起来相当难受。3.2 事件注册与线程安全事件必须先注册再连接或者连接后立即注册。private readonly CZKEMClass _zk new CZKEMClass(); private readonly ConcurrentQueuePunchRecord _queue new ConcurrentQueuePunchRecord(); public void Start(string ip) { // 65535 表示订阅全部事件类型常用的是 1(考勤)、4(新用户)、2(指纹) _zk.OnAttTransactionEx Zk_OnAttTransactionEx; _zk.OnFinger Zk_OnFinger; _zk.SetCommPassword(0); if (!_zk.Connect_Net(ip, 4370)) throw new InvalidOperationException(设备连接失败); if (!_zk.RegEvent(1, 65535)) throw new InvalidOperationException(事件注册失败); } private void Zk_OnAttTransactionEx(string sEnrollNumber, int iIsInValid, int iAttState, int iVerifyMethod, int iYear, int iMonth, int iDay, int iHour, int iMinute, int iSecond, int iWorkCode) { // 这个回调跑在 SDK 自己的通讯线程上不是 UI 线程 var punchTime new DateTime(iYear, iMonth, iDay, iHour, iMinute, iSecond); _queue.Enqueue(new PunchRecord { EnrollNumber sEnrollNumber, // 工号字符串可能是 1~9 位 PunchTime punchTime, VerifyMode iVerifyMethod, // 1 指纹 2 密码 3 卡 15 人脸固件不同略有差异 IsValid iIsInValid 1, // 1 有效0 被设备判为无效 WorkCode iWorkCode }); }参数逐个说明sEnrollNumber是工号字符串不是数字中间可能有前导零存库时别用 int 类型接否则0012会变成12跟人员表对不上。iVerifyMethod是验证方式做报表时经常按它区分“刷卡”和“按指纹”。iIsInValid表示设备是否认为这次打卡有效有些设备配置了时间段限制非工作时间的打卡会标为无效但这不代表数据没被记下来——无效记录同样需要入库只是统计时过滤。回调运行在 SDK 的内部线程上直接在里面更新 WinForm 控件会抛跨线程异常直接在里面写数据库会阻塞通讯线程严重时导致后续事件丢失。正确做法是扔进ConcurrentQueue或者Channel由后台消费者批量入库。我在 Demo 里看到有人直接在回调里MessageBox.Show测试时没问题现场一秒来五条打卡就直接卡死。还有一个隐蔽的坑CZKEMClass实例如果被 GC 回收事件就静默失效了。所以承载它的对象必须是长生命周期的比如注册成单例或者用静态字段持有别在一个方法里 new 完就丢掉。3.3 心跳与断线重连长连接不会永远可靠。车间断电、交换机重启、网线被叉车挂断都是日常。设备掉线后Connect_Net建立的连接不会自动恢复事件也不会再来程序表面上还运行正常——这才是最可怕的。稳妥做法是加一个定时器每分钟调一次GetDeviceStatus取用户数这种便宜的状态值。返回 false 就说明连接已经断了此时先Disconnect释放旧句柄再重新Connect_Net加RegEvent。private void HeartbeatTick(object state) { int dummy 0; if (_zk.GetDeviceStatus(1, 1, ref dummy)) return; // 还活着 _reconnectLock.Wait(); try { _zk.Disconnect(); Thread.Sleep(3000); // 给设备侧留出释放连接的时间 if (_zk.Connect_Net(_ip, 4370)) _zk.RegEvent(1, 65535); } finally { _reconnectLock.Release(); } }Disconnect之后必须等一下再重连。设备侧的连接回收有延迟立刻重连有概率被拒表现为一直连不上等半分钟又自己好了很容易被误判成网络问题。这三秒的等待是照文档写不出来的属于实际调试时才会碰到的经验。注意事件丢失期间的打卡记录不会丢设备本地有存储。重连之后补一次ReadAllGLogData把断线窗口的数据拉回来配合幂等入库即可。别指望事件流是完整的它只是“实时通道”不是“可靠通道”。4. 考勤流水与人员档案两条最常用的数据通道实际项目里 80% 的代码都在处理两件事把记录取出来把人员写进去。这两条链路各有各的脾气。4.1 拉取打卡记录的正确节奏拉取记录是三步固定动作先发一条“准备数据”的指令然后用循环一条条取取完判断返回值。public ListPunchRecord PullAllLogs() { var list new ListPunchRecord(); _zk.EnableDevice(1, false); // 拉取期间锁住面板 try { // 这一步是让设备把记录指针复位到第一条不调用直接 Get 会取到空 if (!_zk.ReadAllGLogData(1)) throw new InvalidOperationException(读取记录失败); string enrollNumber ; int verifyMode 0, inOutMode 0, workCode 0; int year 0, month 0, day 0, hour 0, minute 0, second 0; while (_zk.GetGeneralLogData(1, ref enrollNumber, ref verifyMode, ref inOutMode, ref year, ref month, ref day, ref hour, ref minute, ref second, ref workCode)) { list.Add(new PunchRecord { EnrollNumber enrollNumber, PunchTime new DateTime( year 100 ? year 2000 : year, // 老固件返回两位年需要补 month, day, hour, minute, second), VerifyMode verifyMode, InOutMode inOutMode, // 0 出勤 1 外出 2 返回 3 加班看设备配置 WorkCode workCode }); } } finally { _zk.EnableDevice(1, true); // 无论如何都要恢复 } return list; }几个参数的含义值得单独说。inOutMode是设备上配置的出入状态默认情况下大多数固件不区分进出全部返回 0只有配了“上班/下班”按键或者双读头的设备才有意义。做考勤统计时不要依赖它判断进出方向应该按时间窗口自己算。workCode是对接 ERP 工时字段时用的一般设备不启用。ref参数必须传初始值。C# 里ref传未赋值变量会编译失败但这几个变量在循环里会被反复改写如果不每次重置也没关系因为GetGeneralLogData成功时会重新赋值。真正的问题在于循环结束条件它返回 false 可能是“取完了”也可能是“中途出错”两者无法区分。稳妥做法是先GetDeviceStatus(1, 5, ref count)拿到总条数取完后比对一下取到的数量和这个数对不上就标记本次同步异常下轮重试。数据量大的时候这套接口会明显变慢。五千条记录大概要十几秒因为每条都是一次往返。SDK 里另有一套SSR_前缀的方法参数更多、一次能带回更多字段速度也快一些。如果设备固件支持优先用SSR_GetGeneralLogData。再大的话就别拉取了直接上 PULL SDK 让设备主动推。4.2 下发人员与指纹模板写人员比读记录简单但有几个参数一旦搞错设备面板上会出现“有名字没指纹”或者“指纹属于别人”的情况。public bool PushUser(string enrollNumber, string name, int privilege, string cardNo) { _zk.EnableDevice(1, false); try { // 参数机器号, 工号, 姓名, 密码, 权限(0 普通 2 管理员), 是否启用 if (!_zk.SetUserInfo(1, enrollNumber, name, , privilege, true)) return false; // 卡号必须走这个接口SetUserInfo 里没有卡号字段 if (!string.IsNullOrEmpty(cardNo)) _zk.SetStrCardNo(1, enrollNumber, cardNo); // 指纹模板iFingerIndex 0~9 对应十根手指iFlag1 表示模板有效 if (_tmp ! null !_zk.SetUserTmpExStr(1, enrollNumber, 0, 1, _tmp)) return false; return true; } finally { _zk.EnableDevice(1, true); } }SetUserInfo的姓名参数对中文支持好坏取决于固件版本和设备的字库。有些设备能存 UTF-8有些只认 GBK还有一部分固件会截断成四五个字节。现场遇到过名字变成问号或者半截的情况不要怀疑代码先拿设备面板手动录一个中文名看看显示是否正常——面板都显示不对那就是固件字库的问题改代码没用只能改成存拼音或者英文。SetStrCardNo和SetUserInfo里的卡号是两套东西前者是字符串卡号支持 10 位物理卡号后者是早期的数字卡号。现在基本都是 M1 卡或者 CPU 卡走字符串接口。两个接口混用会导致设备上卡号显示正确、但刷卡时对不上人。指纹模板下发需要先从采集器或者别的设备把模板读出来。GetUserTmpExStr拿到的是一段 Base64 字符串直接存库换设备时原样写回去。模板不能跨算法版本使用同一型号不同固件之间也可能不兼容下线设备之前一定要把模板导出来验证一遍。4.3 落地表结构与增量同步同步任务最怕的是重复导入。设备本身没有自增主键同一个人同一秒打两次卡字段完全一样靠业务字段去重会误杀。常见做法是加一个联合唯一索引工号 打卡时间 验证方式。入库时用INSERT IGNORE或者ON DUPLICATE KEY UPDATE让数据库兜底。CREATE TABLE attendance_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, enroll_no VARCHAR(16) NOT NULL, punch_time DATETIME NOT NULL, verify_mode TINYINT NOT NULL DEFAULT 1, device_sn VARCHAR(32) NOT NULL, raw_flag TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_punch (enroll_no, punch_time, verify_mode, device_sn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;把device_sn放进唯一键是为了多设备场景。同一个人在两台设备上同一秒刷卡业务上是两次记录不能去重。单设备项目里这一列也可以去掉但建议先留着将来加设备不用改表。增量策略上不要每次全量拉。设备里的记录数会一直涨全量拉取的时间会线性增长。更稳的方式是记录上一次同步到的最大时间戳每次只处理比它新的记录。注意不能用“设备当前最大记录数”做游标因为有人清空设备或者循环覆盖之后指针会跳变。5. 避坑与排查COM 注册、位数、连接、事件、时间的五类翻车现场这一章记录的是我在几个项目里真实遇到过的问题。这些问题都不是代码逻辑错误光看代码永远找不出来。5.1 注册失败0x80040154 与 SysWOW64 里的那个 regsvr32现象是运行到new CZKEMClass()这一行直接抛异常提示“检索 COM 类工厂失败”或者错误码0x80040154堆栈指向的是对象构造跟业务代码毫无关系。原因基本只有三种DLL 根本没注册注册的是 64 位版本但进程是 32 位或反过来注册成功了但注册表项在另一个用户下。第二个最隐蔽因为regsvr32弹了成功提示人也以为万事大吉。解决方式是先确认进程位数。任务管理器里看进程有没有带(32 位)标记或者在代码里打印IntPtr.Size4 就是 32 位。32 位进程一律用C:\Windows\SysWOW64\regsvr32.exe注册 x86 目录下那个 DLL64 位进程用System32目录注册 x64 版本。两条命令都跑一遍也不冲突只是注册表位置不同。注册完最好重启一次 Visual Studio让 COM 引用重新解析。5.2 平台目标AnyCPU 编译通过一到客户机上就崩现象比上一条更让人费解开发机上跑得好好的拷到客户机上就报同样的类未注册错误。客户机上明明也用管理员跑过注册脚本了。原因是开发机装了完整的 SDK 或者有其他软件已经把组件注册进 64 位注册表AnyCPU 在 64 位系统上默认跑成 64 位进程正好能命中客户机只注册了 32 位那份进程一跑就落空。解决办法是把项目平台目标固定下来不要留 AnyCPU。用 x86 就一路 x86Web 项目的话在 IIS 应用池里把“启用 32 位应用程序”设成 True。这一步在部署文档里必须写清楚不然每次上线都要远程排查一遍。5.3 Connect_Net 恒为 false4370 端口之外的三个变量现象是连接一直失败GetLastError返回的错误码翻遍对照表也只写“操作失败”。先telnet 设备IP 4370确认端口通不通不通就是网络或者设备通讯功能没开。端口通了还连不上问题集中在三处。通讯密码设备菜单里如果设了非 0 的值代码里SetCommPassword必须一致而且要在Connect_Net之前调用。机器号组网设备要传对应的号单机传 1。设备连接数占满之前有程序异常退出没断开设备侧还留着僵尸连接重启设备或者等几分钟自动释放。还有一种情况是防火墙只放行了 ICMPping 得通但 TCP 4370 被拦这种在现场网络里很常见。用Test-NetConnection 设备IP -Port 4370测试别用 ping 判断。5.4 实时事件不触发RegEvent 掩码与对象生命周期现象是连接成功了代码里也挂了OnAttTransactionEx人在设备上按指纹程序毫无反应日志里一条都没有。第一个原因是没调RegEvent。Connect_Net只建立数据通道事件是另一套订阅机制必须显式注册而且掩码要包含 1考勤事件。传 0 或者传错值就什么也收不到。第二个原因是对象被回收。SDK 的事件订阅在内部是弱引用宿主对象如果没有强引用GC 一跑事件就断了表现为程序刚启动时能收到跑一阵子就静默了。把持有实例的类注册成单例即可。第三个原因是事件触发后程序处理太慢SDK 内部队列溢出丢事件。回调里只做入队别做任何耗时操作。5.5 时间与去重年份差 2000 和重复导入现象有两种。一种是导进去的打卡时间全部落在 1900 年前后另一种是每次同步都重复导入同一批记录数据翻倍增长。时间问题出在年份字段上。新固件返回四位年2024一些老固件返回的是相对值或者两位年24。代码里统一加一层判断小于 100 就加 2000。这个问题不会报错只会安安静静地把数据写歪等做月度统计时才发现那时候补数据就更麻烦了。重复导入的原因是没有落库去重或者用“先删后插”的策略但删除条件写错。用联合唯一索引加INSERT IGNORE最省心业务代码只负责取数去重交给数据库。另外设备上的记录清空之前一定要确认本地已经完整落库ClearGLog是真删设备端没留备份没有后悔药。6. 二次开发把 SDK 包封装成能测的服务层直接在每个业务类里 newCZKEMClass是短期最省事的写法但这种代码没法做单元测试也没法在没有设备的环境里启动。我的做法是抽一层接口出来让业务逻辑只依赖接口。public interface IAttendanceDevice { bool Connect(); void Disconnect(); IReadOnlyListPunchRecord PullLogs(); bool PushUser(UserInfo user); event ActionPunchRecord Punched; // 实时打卡 }实现类包住CZKEMClass和那套线程安全队列业务侧只订阅Punched事件。测试时写一个FakeAttendanceDevice用内存列表模拟设备既能跑通入库逻辑也能造出“断线”“重复打卡”“乱序时间”这些边界数据。我在一个项目里就是这么做的把那批回调时序问题在本地复现了七七八八省掉了大量现场调试。验证手段上建议在实现类里加一个诊断方法把GetDeviceStatus的六个状态码、当前连接状态、事件注册掩码、最近一次错误码全部打出来。上线前跑一遍对着设备面板核对比事后翻日志快得多。最后一条习惯任何涉及EnableDevice(1, false)的代码一律写在try/finally里别问我为什么。希望帮到你。本文还有配套的精品资源点击获取