简介:这是基于C#开发的海康人脸识别Demo,面向需要在DS-K56系列人脸终端上做二次开发的工程师,直接弥补了官方SDK包缺少C#版示例、接口文档杂乱的不足。压缩包共83个文件,整体仅13.59MB,其中34个dll为海康SDK运行依赖,10个cs为可直接阅读的C#源码,3个exe可用于快速验证效果,另有配置、工程、说明与图片文件,从完整解决方案到主窗体代码一应俱全,调试文件也便于定位问题。功能上已实现登录、布防与撤防、远程采集人脸、下发人员信息和人脸信息、识别记录抓取等全流程,并基于DS-K5603-Z型号实测可用,省去了逐个接口摸索的过程。核心SDK封装与界面交互逻辑分开呈现,注释清晰,特别适合参考代码组织方式或在此之上继续扩展。已有2543人学习下载,对刚开始接触海康人脸方案的人很有参考价值,可节省不少前期摸索时间。
1. 海康人脸识别C#落地:一台电脑管一整支人脸底库
我在一个园区门禁项目里第一次接触DS-K56系列人脸一体机时,最先被问到的需求是:新入职员工能不能不用一个个跑去设备前录脸?答案就是这套C# Demo——它把远程采集、人脸下发、布防、撤防、登录、识别报警串成完整链路,让你坐在电脑前就能把一张人脸照片推进设备底库,设备端识别到人后把报警推送回程序里。这套代码最适合集成商、安防工程师和做考勤/闸机/门禁二次开发的C#程序员。它解决的不是“人脸识别算法怎么做”,而是“怎么用C#把海康设备的能力真正管起来”——底层调用设备网络SDK和人脸识别SDK,上层用一套清晰的服务封装,让业务代码不用天天跟非托管内存较劲。
2. 架构与选型:设备网络SDK + 人脸识别SDK双通道
2.1 双SDK的职责边界:HCNetSDK与人脸识别SDK各自管什么
海康设备的C#开发绕不开两个SDK,很多人第一次上手时搞不清它们的分工,代码写得一团乱。设备网络SDK(HCNetSDK)负责的是“和设备对话”这一类基础能力:登录、布防、报警回调、抓拍、对讲、远程配置。人脸识别SDK负责的是“和人脸底库对话”这一类业务能力:向设备下发人脸照片、删除人脸、查询底库状态、设置比对阈值。
我一般把两者的关系理解成“管道”和“货物”:HCNetSDK是管道,负责建连、维持会话、收报警;人脸识别SDK是运货的,把人脸照片和底库ID绑定后通过设备的能力接口写进设备。DS-K56这类一体机的人脸识别本身跑在设备端,PC端不需要做特征提取,所以C#侧主要工作是正确调用SDK接口、处理回调、把图片数据转成SDK要求的格式。先调用NET_DVR_Init初始化SDK,再登录、布防、下发,这一套顺序不能乱。
注意:不同批次设备的SDK版本对接口签名有细微差异,比如登录接口就有V30和V40两个版本,V40支持子序列号和更多的能力集返回,建议优先用V40系接口。
2.2 工程目录结构:一个Demo如何分层
这个Demo的分层值得直接抄作业。UI层只负责展示和触发操作,业务服务层封装了设备登录、人脸库维护、报警分发三块核心逻辑,最底下是SDK互操作封装和ISAPI客户端。这样分的主要原因是:SDK回调是非托管线程进来的,如果让UI代码直接处理回调,十有八九会死在跨线程访问控件上;而且设备SDK的接口很零散,不封一层的话,业务代码会被DllImport和指针结构体塞满。
HikVisionFaceDemo/ ├── HikVisionFaceDemo.sln ├── src/ │ ├── HikVisionFaceDemo.App/ # WinForms 界面层 │ │ ├── Forms/ │ │ │ ├── MainForm.cs │ │ │ └── FaceEnrollForm.cs │ │ └── Program.cs │ ├── HikVisionFaceDemo.Services/ # 业务服务层 │ │ ├── DeviceService.cs # 登录、布防、撤防 │ │ ├── FaceLibService.cs # 采集、编码、下发、删除 │ │ └── AlarmService.cs # 报警分发、事件模型 │ └── HikVisionFaceDemo.Sdk/ # SDK 互操作封装 │ ├── Interop/ │ │ ├── HCNetSDK.cs │ │ └── PlayCtrl.cs │ ├── Models/ │ │ ├── DeviceSession.cs │ │ └── AlarmEvent.cs │ └── ISAPI/ │ └── FaceLibClient.cs # 走HTTP下发人脸 └── assets/ └── face_samples/ # 模拟人脸样本图每个项目引用的关系是单向的:App引用Services,Services引用Sdk,Sdk不依赖上层。这个方向如果反了,后面加功能会非常痛苦。FaceLibClient单独拆出来是因为下发人脸时我倾向走设备的ISAPI协议而不是人脸识别SDK的扩展接口——ISAPI是个HTTP接口,调试起来直观,抓包能看到完整的请求和响应,出了问题定位成本低。
2.3 线程模型与回调:报警为什么不能放在UI线程
设备SDK的报警回调机制是这样的:你调用NET_DVR_SetupAlarmChan_V41布防时注册一个回调函数,之后设备一旦检测到事件,SDK内部的工作线程就会调用这个回调。这个线程是SDK自己创建的非托管线程,不是你的UI线程。
很多新手在这里翻车:直接在回调里写textBox1.Text = ...,程序不是闪退就是界面卡死。正确做法是把回调里拿到的报警数据封装成托管对象,然后用控件的BeginInvoke或者一个线程安全的事件队列切回UI线程。我在Demo里用的是事件+BeginInvoke的轻量方案,没有引入复杂的消息队列,因为单设备场景下报警频率不高,没必要过度设计。但如果你要接多台设备,建议在AlarmService里用一个ConcurrentQueue先把报警事件暂存,再由UI定时器或独立线程批量消费,避免高频回调把界面拖死。
3. 核心模块拆解:登录、远程采集、人脸下发的完整链路
3.1 设备登录:IPv4端口参数与返回会话管理
登录是整个Demo的地基,登录不成功后面全是空谈。设备网络SDK的登录接口是NET_DVR_Login_V40,C#这边通过DllImport直接P/Invoke调用。登录时要传设备IP、端口(默认8000)、用户名、密码,以及一个是否异步登录的标志。返回的userId是一个整型句柄,之后所有操作——布防、抓拍、撤防——都依赖这个句柄。
// Interop/HCNetSDK.cs —— 声明非托管接口 [DllImport("HCNetSDK.dll", EntryPoint = "NET_DVR_Login_V40")] public static extern int NET_DVR_Login_V40( ref NET_DVR_USER_LOGIN_INFO pLoginInfo, ref NET_DVR_DEVICEINFO_V40 lpDeviceInfo); // DeviceService.cs —— 登录封装 public DeviceSession Login(string ip, int port, string username, string password) { NET_DVR_USER_LOGIN_INFO loginInfo = new NET_DVR_USER_LOGIN_INFO(); loginInfo.wPort = (ushort)port; loginInfo.sDeviceAddress = ip; // IPv4 地址字符串 loginInfo.sUserName = username; loginInfo.sPassword = password; loginInfo.bUseAsynLogin = false; // 同步登录,失败立即返回 NET_DVR_DEVICEINFO_V40 deviceInfo = new NET_DVR_DEVICEINFO_V40(); int userId = NET_DVR_Login_V40(ref loginInfo, ref deviceInfo); if (userId < 0) { int errCode = NET_DVR_GetLastError(); throw new Exception($"登录失败,错误码:{errCode}"); } return new DeviceSession { UserId = userId, SerialNumber = deviceInfo.sSerialNumber, ChannelCount = deviceInfo.byChanNum, StartChan = deviceInfo.byStartChan }; }这里有几个参数值得说清楚。bUseAsynLogin如果置true,接口会立即返回0然后通过回调通知结果,对UI程序来说反而复杂,Demo用同步方式最省事。sSerialNumber是设备序列号,多设备管理场景下用它做字典的key比用IP更靠谱,因为DHCP环境下IP可能变。登录失败时通过NET_DVR_GetLastError拿错误码,常见的有17(账号或密码错误)、29(端口不通)、31(设备不在线),这几个错误码记下来,联调时能省一半排查时间。
3.2 远程采集人脸:抓拍图片的获取与保存
“远程采集”有两种理解:一种是从设备实时视频流里抓一帧画面作为人脸底库图,另一种是直接把本地已有的人脸照片传给设备。Demo里两个都支持,但代码路径完全不同。本地照片准入库比较简单,直接读文件转Base64就行;从设备实时抓拍则要走设备网络SDK的抓图接口。
// FaceLibService.cs —— 从设备通道抓拍一帧JPEG public byte[] CaptureFromDevice(DeviceSession session, int channel) { byte[] jpegBuffer = new byte[1024 * 1024]; uint sizeReturned = 0; bool ret = NET_DVR_CaptureJPEGPicture( session.UserId, channel, // 通道号,DS-K56通常为 1 jpegBuffer, (uint)jpegBuffer.Length, ref sizeReturned); if (!ret) { int errCode = NET_DVR_GetLastError(); throw new Exception($"抓拍失败,错误码:{errCode}"); } byte[] result = new byte[sizeReturned]; Array.Copy(jpegBuffer, result, sizeReturned); return result; }抓拍接口输出的是原始JPEG字节流,直接写文件就能得到一张jpg。注意抓拍到的图片可能很大,DS-K56的500万像素传感器输出的JPEG经常在300KB以上,这种图片直接下发很容易失败,我一般会先用Image.FromStream加载再用Encoder参数压缩到100KB以内。压缩这一步很多人会偷懒跳过,等到下发失败返回“内存不足”才知道后悔。
注意:部分门禁一体机的固件版本不支持NET_DVR_CaptureJPEGPicture,返回错误码查询不到具体原因时,可以改用NET_DVR_GetDVRConfig接口去取设备的抓图配置,或者干脆在设备上开定时抓拍,从报警回调里接抓拍图片。踩过一次这个坑的人都会理解什么叫“同一型号不同固件两套行为”。
3.3 人脸下发:Base64编码与底库ID绑定
人脸下发是整套Demo里最核心也最容易翻车的模块。DS-K56支持通过ISAPI协议在HTTP层面管理人脸底库,我推荐优先走这条路径,因为人脸识别SDK的扩展接口在不同版本里变化较大,而ISAPI基本稳定。第一步要把人脸图片压缩并转成Base64字符串,第二步构造JSON请求,第三步带Digest认证POST到设备的ISAPI端点。
// FaceLibService.cs —— 通过ISAPI下发人脸到底库 public bool AddFace(string deviceIp, string imagePath, int faceLibId, int faceId, string name) { byte[] imageBytes = CompressJpeg(File.ReadAllBytes(imagePath), 80); string base64Image = Convert.ToBase64String(imageBytes); string json = $@"{{ ""faceLibType"": ""blackFD"", ""FDID"": {faceLibId}, ""FPID"": {faceId}, ""faceInfo"": {{ ""name"": ""{name}"", ""faceData"": [ {{ ""faceImageData"": {{ ""data"": ""{base64Image}"", ""dataType"": ""jpg"" }}, ""faceImageFeature"": {{ ""faceFeature"": null }} }} ] }} }}"; string url = $"http://{deviceIp}/ISAPI/Intelligent/FaceLib/FaceData?format=json"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.ContentType = "application/json; charset=utf-8"; request.Headers.Add("Authorization", BuildDigestHeader(url, json)); // 写入body、读取响应,返回设备给出的状态码 using (StreamWriter writer = new StreamWriter(request.GetRequestStream(), Encoding.UTF8)) { writer.Write(json); } using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { string resultXml = reader.ReadToEnd(); return resultXml.Contains("statusValue") && resultXml.Contains("true"); } }参数里FDID是底库编号,FPID是底库里的人脸ID,这两个值要全局规划好。我在做项目时会按部门分段划分FPID:比如1000到1999给A部门,2000到2999给B部门,这样后续批量删除和管理都有规律可循。name字段是设备端显示的人名,注意ISAPI网关对编码敏感,务必保证JSON是UTF-8编码,否则中文名字在设备的Web管理页上会显示成乱码。
图片数据这一栏里faceImageFeature默认给null,设备端会自动提取特征值。如果你的网络环境差、图片又大,可以在下发的HTTP请求上开启压缩或使用短连接。设备返回的XML里有个statusValue字段,解析后如果是true说明下发成功,false则往往是图片不符合要求或底库ID超范围。我把Digest认证的Builder单独抽成了方法,因为这个协议要求先拿401挑战再带Authorization头重试,不封装的话每个ISAPI调用都要复制一遍逻辑。
4. 布防撤防与报警回调:识别事件从设备到UI的最后一公里
4.1 布防与撤防:报警生命周期的管理
布防的意思是告诉设备“现在开始把报警事件推给我”。DS-K56这类门禁一体机支持多种报警类型,人脸识别事件只是其中一种。布防时要用NET_DVR_SetupAlarmChan_V41接口,传入布防参数结构体,返回一个布防句柄。这个句柄要和登录句柄一起管理,程序退出或设备重连时必须先撤防再释放,否则下一次布防可能失败。
// DeviceService.cs —— 布防 public int SetupAlarm(int userId) { NET_DVR_SETUPALARM_PARAM alarmParam = new NET_DVR_SETUPALARM_PARAM(); alarmParam.dwSize = Marshal.SizeOf(typeof(NET_DVR_SETUPALARM_PARAM)); alarmParam.byLevel = 1; // 布防等级,1为最高 alarmParam.byAlarmInfoType = 1; // 报警消息扩展类型,必须为1 alarmParam.byRetAlarmTypeV40 = 0; // 不强制返回V40报警类型 int alarmHandle = NET_DVR_SetupAlarmChan_V41(userId, ref alarmParam); if (alarmHandle < 0) { int errCode = NET_DVR_GetLastError(); throw new Exception($"布防失败,错误码:{errCode}"); } return alarmHandle; } // DeviceService.cs —— 撤防 public void CloseAlarm(int alarmHandle) { if (alarmHandle >= 0) { NET_DVR_AlarmHR_Stop_V41(alarmHandle); } }byLevel表示布防等级,一般设备上1级布防会推送全部报警,等级越低推送的事件类型越少。byAlarmInfoType是最容易漏掉的一项,如果它为0,回调里收到的事件消息就缺少结构化的人脸信息,只剩一个命令号,你要的人名、相似度、抓拍图全都没有。很多人在这一步卡了一整天,最后发现就是这一个字节的差别。撤防接口在不同版本的SDK里叫法不同,新版是NET_DVR_AlarmHR_Stop_V41,老版是NET_DVR_StopAlarmChan,两个都试试,哪个能编译通过用哪个。
4.2 报警回调里的事件分发:人脸识别事件与抓拍事件
报警回调函数是在SDK的非托管线程里执行的,SDK把报警命令号和数据结构指针传给你,你负责把它们翻译成业务事件。DS-K56上人脸识别报警的命令号在头文件里有对应的宏定义,比如人脸识别比对成功事件、抓拍事件、门禁事件等。回调里第一步是判断命令号,第二步是把指针转换成对应的结构体,第三步封装成托管事件抛出去。
// AlarmCallback.cs —— SDK报警回调入口 private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, int dwBufLen, IntPtr pUser) { // 判断是否为设备上传的人脸识别事件,具体命令号以SDK头文件为准 if (lCommand == AlarmCommand.NET_DVR_ALARM_FACE_RECOGNITION) { NET_VCA_FACE_RECOGNITION_RESULT result = Marshal.PtrToStructure<NET_VCA_FACE_RECOGNITION_RESULT>(pAlarmInfo); AlarmEvent evt = new AlarmEvent { FaceId = result.dwFPID, // 底库中的人脸ID Similarity = result.dwSimilarity, // 比对相似度 Name = result.sName, // 底库中登记的姓名 Time = result.dwTime, // 抓拍时间 FaceImageUrl = result.sUrl // 设备返回的人脸图URL }; // 切回UI线程再触发事件 mainForm.BeginInvoke(new Action(() => { AlarmRaised?.Invoke(this, evt); })); } }NET_VCA_FACE_RECOGNITION_RESULT结构体里的字段是跨语言的,字符串类型通常是ANSI字符数组,C#侧用Marshal.PtrToStructure可以直接转换。dwTime字段是Unix时间戳还是设备自1970年起秒数,不同固件有差异,联调时先用一个已知时间做对比,确认后再写转换逻辑。sUrl字段是设备内存里临时存的抓拍图地址,用HTTP能直接拉取这张图片,用于在界面上展示“刚刚是谁刷了脸”。
回调里的所有耗时操作都要避免。比如不要在回调里直接拿sUrl去下载图片,那会阻塞SDK的工作线程。正确做法是把URL存下来,放到UI线程或者后台线程池里去拉取。我曾经因为回调里同步请求图片导致设备报警积压,一分钟后回调风暴把程序直接搞崩溃了,这个教训很深刻。
4.3 避坑记录:五个真实的翻车现场
第一个坑是“换台电脑就报无法加载DLL”。现象是程序在自己开发机上跑得好好的,部署到客户电脑上启动就报DllNotFoundException。原因是设备的SDK只提供64位动态库,而项目默认平台目标是AnyCPU且勾选了“首选32位”,运行时CLR以32位进程加载,自然找不到64位版本的HCNetSDK.dll。解决方法是解决方案平台切换到x64,或者去掉“首选32位”选项,并确保HCNetSDK.dll、HCCore.dll等文件都放在x64目录下。
第二个坑是“下发人脸一直返回设备繁忙”。现象是连续下发几张人脸后,设备返回状态码显示失败,错误码对应“内存不足”或“资源忙”。原因是每张图片都几百KB,设备端解码图片、提取特征需要时间,短时间高频请求会把设备的内存打满。解决方法是把图片压缩到80到100KB范围,并且在下发请求之间加300毫秒左右的间隔,批量导入时用队列控制并发数。
第三个坑是“中文姓名在设备端显示乱码”。现象是下发成功、识别也正常,但设备Web管理端的人脸列表里名字是乱码,英文名正常。原因是ISAPI网关对JSON请求体的编码解析不确定,默认按GBK处理了UTF-8内容。解决方法是在HTTP请求头显式带上charset=utf-8,同时保证StreamWriter写入时也指定UTF-8编码。这两个地方编码不一致是最容易踩的。
第四个坑是“布防成功却收不到识别事件”。现象是布防接口返回正常,但设备有人经过时程序没反应,用抓包工具看设备根本没推数据。原因是布防参数的byAlarmInfoType没有设为1,设备认为你只想要精简报警,就不推送结构化的人脸数据。解决方法是布防参数里把byLevel和byAlarmInfoType都设为1,然后重新布防。
第五个坑是“程序跑一晚上内存涨了几个G”。现象是界面不卡但内存不断增长,最终程序被系统杀掉。原因是报警回调里每次New了对象但没释放非托管资源,或者重复布防产生了句柄泄漏。解决方法是为每次布防维护一个句柄字典,撤防时统一清理;回调里如果从sUrl下载了图片流,务必在using块里及时释放。这个问题的排查方法是任务管理器里观察句柄数和GDIObjects,如果持续上升就能确认是泄漏。
5. 运行与调试:从SDK依赖到真机联调的完整步骤
5.1 环境准备:64位依赖与DLL放置
跑这个Demo之前先把运行环境理顺。SDK解压后通常有lib文件夹,里面是一堆动态库,真正运行时必须出现在程序输出目录里的核心文件包括:HCNetSDK.dll(主SDK)、HCCore.dll(核心依赖)、HCPreview.dll(预览)、PlayCtrl.dll(播放库)、SuperRender.dll(渲染库),还有负责日志的hcclt.dll。不同版本SDK压缩包里附带的文件略有差异,以你解压得到的lib为准,尽量全量拷贝而不是只拷一两个,否则运行时会碰到意外缺失。
bin\x64\ ├── HCNetSDK.dll ├── HCCore.dll ├── HCPreview.dll ├── PlayCtrl.dll ├── SuperRender.dll ├── hcclt.dll ├── HikVisionFaceDemo.exe └── appsettings.json把整个lib目录的内容复制到输出目录后,右键项目属性把平台目标改成x64。如果还报加载失败,打开事件查看器的应用程序日志,里面会记录加载失败的模块路径,照着修正路径就行。顺便说一句,SDK初始化时建议在Program.cs的Main方法里调用一次NET_DVR_Init,程序退出前调用NET_DVR_Cleanup,这两个初始化接口一般不写在类库项目里,而是放在App的启动和退出逻辑里,保证生命周期是完整的。
5.2 配置文件与连接参数
连接参数放在appsettings.json里,好处是换设备、换密码不用重新编译。设备IP、端口、账号、密码是登录必需项,底库ID区间和比对阈值是人脸管理模块的配置。DS-K56的默认SDK端口是8000,但有些项目的网络管理员会改端口,一定要确认实际端口而不是想当然。
{ "Device": { "Ip": "192.168.1.64", "Port": 8000, "UserName": "admin", "Password": "your_password" }, "FaceLib": { "StartId": 1, "EndId": 1000, "ComparisonThreshold": 65, "Channel": 1 } }CompareThreshold这里指的是设备端的比对阈值,通常设置在60到75之间,数值越低越容易误识别,越高越容易拒识。这个参数在Demo里通过人脸识别SDK或设备配置接口下发到设备端,具体接口名看头文件。我在实际项目中会把阈值做成界面上的一个滑块,方便现场调试时快速调整,而不是每次都改配置文件重启。设备密码如果开启了强密码策略,要注意密码里包含特殊字符时JSON的转义问题,否则登录一直报错又说不出原因。
5.3 真机联调步骤:从登录日志到报警验证
真机联调别一上来就点“布防”按钮。我的习惯是分五步走,每一步确认没问题再进下一步。第一步,用浏览器访问设备的IP,确认HTTP/HTTPS服务通,ISAPI协议能访问,如果连浏览器都打不开就别谈SDK。第二步,在设备Web管理端确认SDK端口号是8000,如果被改了就在配置文件里同步修改。第三步,运行Demo点击登录,如果失败看错误码——17是密码错,29是端口不通,31是设备不在线,日志里我会把错误码和对应含义都打出来。
第四步,测试抓拍和下发。抓拍一张图看文件大小和内容是否正常,再走下发流程,下发成功后到设备Web管理端的人脸列表里确认是否多了一条记录以及中文名是否正常。第五步,布防后站在设备前触发一次识别,看日志里有没有报警回调进来。如果回调进来了但界面没弹窗,优先排查BeginInvoke是否写错,而不是怀疑设备没推送。联调过程中把每一步的日志都保存下来,现场出了问题能快速定位是网络、设备还是程序的问题。
提示:如果在局域网里抓包看ISAPI请求,记得Wireshark过滤条件是tcp.port == 8000或http,DS-K56的Web端口和SDK端口是分开的,别抓错网卡。
6. 二次开发:从Demo到生产级系统的三个改造方向
第一个改造方向是把WinForms换成Windows服务或者Web API。Demo跑在桌面上,但生产环境里设备往往部署在弱电间或者前端岗亭,电脑不一定有显示器。我一般把DeviceService和AlarmService抽出来放到一个控制台宿主里,对外暴露一个简单的REST接口给业务系统调用,报警事件则通过Webhook推送到后端。这样调整后,人脸下发可以由HR系统在人入职时自动触发,不用人工在界面上点按钮。
第二个改造方向是人脸底库的增量同步。Demo里是单张下发,生产环境里有离职、入职、跨部门调动,底库需要动态增删。我会给每张人脸加一个版本号字段,本地的业务数据库作为主数据源,每天凌晨跑一次增量同步任务:比对本地库和设备库的人脸ID,把差异部分下发或删除。这个方案比全量下发靠谱得多,全量下发几百人时设备经常忙不过来,增量同步压力小且可追踪。
第三个改造方向是报警数据的落库和推送。Demo里报警只弹窗展示,生产环境需要把每一次识别记录写入数据库,用于考勤统计或者门禁记录回溯。我在AlarmService里加了一个持久化模块,把人脸ID、姓名、相似度、抓拍时间、抓拍图URL写入一张alarm_log表,同时提供一个PushWebhookAsync方法把事件推给业务系统的接口。
// AlarmService.cs —— 报警事件落库并推送 public async Task HandleAlarmAsync(AlarmEvent evt) { using (var db = new AlarmDbContext()) { db.AlarmLogs.Add(new AlarmLog { FaceId = evt.FaceId, Name = evt.Name, Similarity = evt.Similarity, CapturedAt = DateTimeOffset.FromUnixTimeSeconds(evt.Time), FaceImageUrl = evt.FaceImageUrl }); await db.SaveChangesAsync(); } await PushWebhookAsync(evt); }推送接口用HttpClient,序列化直接用Newtonsoft.Json,超时时间设5秒,推送失败要重试但不能阻塞设备回调。我在这个环节加了一个简单的内存队列做削峰,避免报警并发高时把业务系统打崩。
做这套东西这么久,我养成的习惯是每次换设备或改SDK版本时,先跑一遍“登录-抓拍-下发-布防-触发-撤防”的回归流程,日志全部过一遍再往上叠业务功能。人脸设备跟普通摄像头不一样,它有自己的底库和识别逻辑,很多问题是设备端行为导致的,不是代码问题。先确认设备本身工作正常,再怀疑自己的代码,能少走一半弯路。希望帮到你。
本文还有配套的精品资源,点击获取