POS软件是什么?你好意思吗,还在用老掉牙的Winform。

门店被淘汰的POS机
销售终端——POS(point of sale)是一种多功能终端,把它安装在信用卡的特约商户和受理网点中与计算机联成网络,就能实现电子资金自动转账,它具有支持消费、预授权、余额查询和转账等功能,使用起来安全、快捷、可靠。

前言
万事俱备只欠东风------一个USB摄像头和一个经过改造的人脸识别程序。

下载地址:
GitHub - ViewFaceCore/ViewFaceCore: C# 超简单的离线人脸识别库。( 基于 SeetaFace6 )
开始干活,动手改造。
- 程序要支持无人值守,程序启动时自动打开摄像头。超过设定的时间无移动鼠标和敲击键盘,程序自动关闭摄像头,进入“休眠”
- 识别人脸成功后记录当前时间作为考勤记录
- 人脸信息放在服务器端,桌面程序和服务器端同步人脸信息
- 关于不排班实现考勤的思考
- 取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗
1.检测超过设定的时间无移动鼠标和敲击键盘,判断是否无人使用。
- #region 获取键盘和鼠标没有操作的时间
- [StructLayout(LayoutKind.Sequential)]
- struct LASTINPUTINFO
- {
- [MarshalAs(UnmanagedType.U4)]
- public int cbSize;
- [MarshalAs(UnmanagedType.U4)]
- public uint dwTime;
- }
- [DllImport("user32.dll")]
- private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
- /// <summary>
- /// 获取键盘和鼠标没有操作的时间
- /// </summary>
- /// <returns></returns>
- private static long GetLastInputTime()
- {
- LASTINPUTINFO vLastInputInfo = new LASTINPUTINFO();
- vLastInputInfo.cbSize = Marshal.SizeOf(vLastInputInfo);
- if (!GetLastInputInfo(ref vLastInputInfo))
- return 0;
- else
- return Environment.TickCount - (long)vLastInputInfo.dwTime;//单位ms
- }
- #endregion
2.把人脸识别这个用途改成考勤
- /// <summary>
- /// 窗体加载时
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void Form_Load(object sender, EventArgs e)
- {
- #region 窗体初始化
- WindowState = FormWindowState.Maximized;
- // 隐藏摄像头画面控件
- VideoPlayer.Visible = false;
- //初始化VideoDevices
- 检测摄像头ToolStripMenuItem_Click(null, null);
- //默认禁用拍照按钮
- FormHelper.SetControlStatus(this.ButtonSave, false);
- Text = "WPOS人脸识别&考勤";
- #endregion
- #region TTS
- try
- {
- VoiceUtilHelper = new SpVoiceUtil();
- StartVoiceTaskJob();
- }
- catch (Exception ex)
- {
- byte[] zipfile = (byte[])Properties.Resources.ResourceManager.GetObject("TTSrepair");
- System.IO.File.WriteAllBytes("TTSrepair.zip", zipfile);
- Program.UnZip("TTSrepair.zip", "", "", true);
- #region 语音引擎修复安装
- try
- {
- MessageBox.Show("初始化语音引擎出错,错误描述:" + ex.Message + Environment.NewLine +
- "正在运行语音引擎安装程序,请点下一步执行安装!", Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
- string physicalRoot = AppDomain.CurrentDomain.BaseDirectory;
- string info1 = Program.Execute("TTSrepair.exe", 3);
- }
- finally
- {
- System.IO.File.Delete("TTSrepair.zip");
- Application.Restart();
- }
- #endregion
- }
- #endregion
- #region 自动打开摄像头
- Thread thread = new Thread(() =>
- {
- Thread.Sleep(5000);
- sc.Post(SystemInit, this);
- });
- thread.Start();
- #endregion
- #region Sync face data
- Thread SyncThread = new Thread(() =>
- {
- while (IsWorkEnd == false)
- {
- var theEmployeeList = SyncServerEmployeeInfomation().Where(r => r.EmpFacialFeature != null).ToList();
- if (theEmployeeList != null && theEmployeeList.Count > 0)
- {
- foreach (var emp in theEmployeeList)
- {
- poolExt.Post(emp);
- }
- }
- Thread.Sleep(5000);
- }
- });
- SyncThread.Start();
- #endregion
- #region 自动关闭摄像头线程
- Thread CameraCheckThread = new Thread(() =>
- {
- while (IsWorkEnd == false)
- {
- if (IsNeedAutoCheck)
- {
- long Auto_close_camera_interval = long.Parse(string.IsNullOrEmpty(config.AppSettings.Settings["Auto_close_camera_interval"].Value) ? "60000" : config.AppSettings.Settings["Auto_close_camera_interval"].Value);
- long ts = GetLastInputTime();
- if (ts > Auto_close_camera_interval)
- {
- IsNeedAutoCheck = false;
- sc.Post(CheckCameraStatus, this);
- }
- }
- Thread.Sleep(1000);
- }
- });
- CameraCheckThread.Start();
- btnSleep.Enabled = true;
- btnStopSleep.Enabled = true;
- #endregion
- }
修改识别人脸后做的事情:
- /// <summary>
- /// 持续检测一次人脸,直到停止。
- /// </summary>
- /// <param name="token">取消标记</param>
- private async void StartDetector(CancellationToken token)
- {
- List<double> fpsList = new List<double>();
- double fps = 0;
- Stopwatch stopwatchFPS = new Stopwatch();
- Stopwatch stopwatch = new Stopwatch();
- isDetecting = true;
- try
- {
- if (VideoPlayer == null)
- {
- return;
- }
- while (VideoPlayer.IsRunning && !token.IsCancellationRequested)
- {
- try
- {
- if (CheckBoxFPS.Checked)
- {
- stopwatch.Restart();
- if (!stopwatchFPS.IsRunning)
- { stopwatchFPS.Start(); }
- }
- Bitmap bitmap = VideoPlayer.GetCurrentVideoFrame(); // 获取摄像头画面
- if (bitmap == null)
- {
- await Task.Delay(10, token);
- FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
- continue;
- }
- if (!CheckBoxDetect.Checked)
- {
- await Task.Delay(1000 / 60, token);
- FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
- continue;
- }
- List<Models.FaceInfo> faceInfos = new List<Models.FaceInfo>();
- using (FaceImage faceImage = bitmap.ToFaceImage())
- {
- var infos = await faceFactory.Get<FaceTracker>().TrackAsync(faceImage);
- for (int i = 0; i < infos.Length; i++)
- {
- Models.FaceInfo faceInfo = new Models.FaceInfo
- {
- Pid = infos[i].Pid,
- Location = infos[i].Location
- };
- if (CheckBoxFaceMask.Checked || CheckBoxFaceProperty.Checked)
- {
- Model.FaceInfo info = infos[i].ToFaceInfo();
- if (CheckBoxFaceMask.Checked)
- {
- var maskStatus = await faceFactory.Get<MaskDetector>().PlotMaskAsync(faceImage, info);
- faceInfo.HasMask = maskStatus.Masked;
- }
- if (CheckBoxFaceProperty.Checked)
- {
- FaceRecognizer faceRecognizer = null;
- if (faceInfo.HasMask)
- {
- faceRecognizer = faceFactory.GetFaceRecognizerWithMask();
- }
- else
- {
- faceRecognizer = faceFactory.Get<FaceRecognizer>();
- }
- var points = await faceFactory.Get<FaceLandmarker>().MarkAsync(faceImage, info);
- float[] extractData = await faceRecognizer.ExtractAsync(faceImage, points);
- UserInfo userInfo = CacheManager.Instance.Get(faceRecognizer, extractData);
- if (userInfo != null)
- {
- faceInfo.Name = userInfo.Name;
- faceInfo.Age = userInfo.Age;
- switch (userInfo.Gender)
- {
- case GenderEnum.Male:
- faceInfo.Gender = Gender.Male;
- break;
- case GenderEnum.Female:
- faceInfo.Gender = Gender.Female;
- break;
- case GenderEnum.Unknown:
- faceInfo.Gender = Gender.Unknown;
- break;
- }
- pool.Post(userInfo);
- }
- else
- {
- faceInfo.Age = await faceFactory.Get<AgePredictor>().PredictAgeAsync(faceImage, points);
- faceInfo.Gender = await faceFactory.Get<GenderPredictor>().PredictGenderAsync(faceImage, points);
- }
- }
- }
- faceInfos.Add(faceInfo);
- }
- }
- using (Graphics g = Graphics.FromImage(bitmap))
- {
- #region 绘制当前时间
- StringFormat format = new StringFormat();
- format.Alignment = StringAlignment.Center;
- format.LineAlignment = StringAlignment.Center;
- g.DrawString($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}", new Font("微软雅黑", 32), Brushes.Green, new Rectangle(0, 0, Width - 32, 188), format);
- #endregion
- // 如果有人脸,在 bitmap 上绘制出人脸的位置信息
- if (faceInfos.Any())
- {
- g.DrawRectangles(new Pen(Color.Red, 4), faceInfos.Select(p => p.Rectangle).ToArray());
- if (CheckBoxDetect.Checked)
- {
- for (int i = 0; i < faceInfos.Count; i++)
- {
- StringBuilder builder = new StringBuilder();
- if (CheckBoxFaceProperty.Checked)
- {
- if (!string.IsNullOrEmpty(faceInfos[i].Name))
- {
- builder.Append(faceInfos[i].Name);
- }
- }
- if (builder.Length > 0)
- g.DrawString(builder.ToString(), new Font("微软雅黑", 32), Brushes.Green, new PointF(faceInfos[i].Location.X + faceInfos[i].Location.Width + 24, faceInfos[i].Location.Y));
- }
- }
- }
- if (CheckBoxFPS.Checked)
- {
- stopwatch.Stop();
- if (numericUpDownFPSTime.Value > 0)
- {
- fpsList.Add(1000f / stopwatch.ElapsedMilliseconds);
- if (stopwatchFPS.ElapsedMilliseconds >= numericUpDownFPSTime.Value)
- {
- fps = fpsList.Average();
- fpsList.Clear();
- stopwatchFPS.Reset();
- }
- }
- else
- {
- fps = 1000f / stopwatch.ElapsedMilliseconds;
- }
- g.DrawString($"{fps:#.#} FPS", new Font("微软雅黑", 24), Brushes.Green, new Point(10, 10));
- }
- }
- FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
- }
- catch (TaskCanceledException)
- {
- break;
- }
- catch { }
- }
- }
- finally
- {
- isDetecting = false;
- }
- }
- #endregion
3.把人脸信息放在服务器端,桌面程序和服务器端同步人脸信息
- /// <summary>
- /// 同步人员信息
- /// </summary>
- private List<PlatEmployeeDto> SyncServerEmployeeInfomation()
- {
- List<PlatEmployeeDto> list = new List<PlatEmployeeDto>();
- string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/POSSyncEmployeeInfomation";
- try
- {
- string rs = Program.HttpGetRequest(url);
- if (!string.IsNullOrEmpty(rs) && JObject.Parse(rs).Value<int>("code").Equals(200))
- {
- JObject jo = JObject.Parse(rs);
- list = JsonConvert.DeserializeObject<List<PlatEmployeeDto>>(jo["data"].ToString());
- }
- }
- catch (Exception ex)
- {
- if (ex.Message.Contains("无法连接到远程服务器"))
- {
- Thread.Sleep(100);
- ViewFaceCore.Controls.MessageTip.ShowError("无法连接到远程服务器" + Environment.NewLine + "Unable to connect to remote server", 300);
- }
- }
- return list;
- }
- private void btnSave_Click(object sender, EventArgs e)
- {
- try
- {
- SetUIStatus(false);
- UserInfo userInfo = BuildUserInfo();
- if (userInfo == null)
- {
- throw new Exception("获取用户基本信息失败!");
- }
- using (DefaultDbContext db = new DefaultDbContext())
- {
- db.UserInfo.Add(userInfo);
- if (db.SaveChanges() > 0)
- {
- CacheManager.Instance.Refesh();
- this.Close();
- _ = Task.Run(() =>
- {
- //确保关闭后弹窗
- Thread.Sleep(100);
- try
- {
- #region Post Data
- string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/PosNewEmployeeRegister";
- PlatEmployeeDto dto = new PlatEmployeeDto();
- dto.KeyId = Guid.NewGuid().ToString();
- dto.EmpNo = userInfo.EmpNo;
- dto.EmpName = userInfo.Name;
- dto.EmpSex = (int)userInfo.Gender.ToInt64();
- dto.Mobile = userInfo.Phone;
- dto.PositionValue = userInfo.JobPosition.ToString();
- dto.EmpFacialFeature = _globalUserInfo.Extract;
- dto.EmpMainPhoto = _globalUserInfo.Image;
- dto.CreateBy = "Client";
- dto.CreateTime = DateTime.Now;
- dto.IsAdmin = "N";
- dto.Status = 0;
- dto.FirstPositionLabel = cbxposition.Text;
- string jsondata = JsonConvert.SerializeObject(dto);
- string st = Program.PostJsonData(url, jsondata);
- #endregion
- if (!string.IsNullOrEmpty(st) && st.Contains("200"))
- {
- //MessageBox.Show("保存用户信息成功!同步到服务器成功,可到其他门店考勤。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
- DialogResult = DialogResult.OK;
- }
- }
- catch (Exception ex)
- {
- MessageBox.Show("本地保存用户信息成功!但同步到服务器出错,不能立即到其他门店考勤。" + ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }
- });
- }
- }
- }
- catch (Exception ex)
- {
- MessageBox.Show(ex.Message, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- }
- finally
- {
- SetUIStatus(false);
- }
- }
4.关于不排班实现考勤的思考
- /// <summary>
- /// 客户端添加attendance考勤明细
- /// </summary>
- /// <returns></returns>
- [HttpPost("AddAttendanceDetails")]
- //[ActionPermissionFilter(Permission = "business:erpattendancedetails:add")]
- [Log(Title = "attendance考勤明细", BusinessType = BusinessType.INSERT)]
- [AllowAnonymous]
- public IActionResult AddAttendanceDetails([FromBody] AttendanceDetailsDto parm)
- {
- var modal = parm.Adapt<AttendanceDetails>().ToCreate(HttpContext);
- if (!string.IsNullOrEmpty(parm.FkStore))
- {
- int storeId = -1;
- int.TryParse(parm.FkStore, out storeId);
- var store = _MerchantStoreService.GetFirst(s => s.Id == storeId);
- if (store == null)
- return BadRequest();
- modal.FkStore = store.KeyId;
- }
- else
- return BadRequest();
- if (!_AttendanceDetailsService.Any(r => r.AuditDate == parm.AuditDate && r.EmpNo == parm.EmpNo))
- {
- modal.Remark = "上班&clock in";
- var response = _AttendanceDetailsService.AddAttendanceDetails(modal);
- return SUCCESS(response);
- }
- else
- {
- var list = _AttendanceDetailsService.GetList(r => r.AuditDate == parm.AuditDate && r.EmpNo == parm.EmpNo);
- var time1 = list.Max(r => r.AttendanceDatetime);
- if (time1 != null)
- {
- var ts = DateTime.Now - DateTime.Parse(time1);
- if (ts.TotalMinutes < 61)
- {
- return Ok();
- }
- else
- {
- modal.Remark = "下班&clock out";
- var response = _AttendanceDetailsService.AddAttendanceDetails(modal);
- return SUCCESS(response);
- }
- }
- else
- {
- return BadRequest();
- }
- }
- }
5.取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗

这个需要感谢以前在园子里的一位博主的分享他写的控件名字叫"LayeredWindow",对外暴露的类叫“MessageTip”,不好意思已忘记作者。
如果你仔细阅读代码还会发现集成了TTS。反正做得有点像无人值守的一些商业机器。好了,收工了。今天只上半天班。
