电子线束ERP - HR人事考勤模块详细功能介绍|C/S开发框架

作者:jonny  发布日期:2026/10/28 15:29:19

电子线束ERP - HR人事考勤模块详细功能介绍|C/S开发框架

考勤模块是 ERP 里最容易"看起来简单、做起来失控"的模块之一。一旦业务变成多班次接力、按拉线错峰吃饭、直接与间接人工口径不同,原来那套"读打卡表、算差值"的写法很快就会崩——每一处硬编码都会在下一个客户现场变成 Bug。

本文以电子线束行业的 HR 人事考勤模块为例,介绍基于 CSFrameworkV6 平台落地这类模块时的功能划分、关键实现思路、技术栈选型与外部设备接入方式,供做同类业务系统的开发者参考。

一、先定数据流,再谈功能

模块的功能清单很长,但骨架只有一条数据流。把它定清楚,后面的功能才有位置可放:

环节职责主要表 / 入口
采集设备同步、Excel 导入、HR 补卡tb_AttendTime
归档匹配班次与节假日,逐日判定迟到/早退/缺卡tb_AttendArchive(HRCoreDAL_AttendComputing)
报表按归档逐日重算工时,汇总到人/部门HRCoreDAL_AttendReport
薪资异常与工时进入工资核算考勤工资调整、薪资调整

关键决策是在原始打卡与薪资之间强制插入一层归档。好处有三个:计算结果可审计(每笔工资能追溯到具体日期、班次、卡)、口径可对比(改造前后跑同月归档逐字比对,等价性可证明)、数据可封账(薪资结完不再变动)。代价是要多写一个计算引擎,但这笔账值得。

二、功能清单

1. 人力资源

  • 员工档案(含身份证阅读器录入)、员工合同、岗位调整记录、薪资调整记录
  • 员工异动日志:调岗/辞职/自离/辞退/劝离/急辞/开除/停薪留职 8 类异动统一落日志表,记录异动前后部门、岗位、状态与生效日期
  • 恢复在职:误操作可回退,同时补写异动日志,避免直接改库造成履历断档
  • 考勤工资调整:按人按月调整考勤工资项

2. 考勤配置

  • 考勤参数配置(14 项算法参数 + 特殊考勤日两个页签,同一 DAL)
  • 班次管理、排班管理(部门/拉线/人员三级适用范围)、节假日管理
  • 考勤设备管理(IP/MAC/端口/SDK 登录测试)、考勤设备绑定

3. 考勤处理

  • 原始考勤记录、导入考勤数据、考勤计算(归档)、补卡管理、请假管理

4. 报表

  • 个人考勤报表(含横向版)、部门考勤报表、原始考勤记录表

三、三个关键实现

1. 班次槽位解耦:把字面量换成语义

最初的实现里,取卡算法到处写着 "A1"、"B1"、"C1"——上午、下午、加班三个班次用字符串字面量判断。业务提出"要支持 A2/B2/C2 错峰班次"时,这套写法立刻变成灾难:十几处硬编码要同时改,漏一处就判错。

解法是把字面量抽象成槽位:班次编号首字母定槽位(A→上午、B→下午、C→加班、X→特殊),数字只表示错峰分组。槽位用只读计算属性从班次编号现算,因此不可能与字段不同步:

public static class AttendSlot
{
    public const int ShangWu = 1;   // 上午
    public const int XiaWu   = 2;   // 下午
    public const int JiaBan  = 3;   // 晚上加班
    public const int TeShu   = 9;   // 特殊班

    /// <summary>班次编号首字母定槽位,数字只表示错峰分组(A1/A2 同槽位)</summary>
    public static int GetSlot(string shiftId)
    {
        if (string.IsNullOrWhiteSpace(shiftId)) return TeShu;
        switch (shiftId.Trim().Substring(0, 1).ToUpper())
        {
            case "A": return ShangWu;
            case "B": return XiaWu;
            case "C": return JiaBan;
            default:  return TeShu;
        }
    }
}

另一个细节:"下一段班"不能用"槽位数值 + 1"去推,而应按排班单明细的 SortID 排序取 ±1。实测中存在一张单是 X1 + X2(首字母相同),按槽位推会取错,按排序取才对。

2. 适用范围解析收口到一个静态类

排班单原本只能按部门,后来扩到"部门 / 拉线 / 人员",优先级 人员 > 拉线 > 部门,命中即整单替换。麻烦的是,排班单在三个地方被消费:考勤计算(封账)、考勤报表、员工排班查询。三处各写一遍匹配逻辑,口径必然发散。

做法是抽出一个统一解析器,三处全部改调它:

public static class AttendScheduleScopeResolver
{
    public const string ScopeDept = "部门";
    public const string ScopeLine = "拉线";
    public const string ScopeEmp  = "人员";
    public const string UnrestrictedFlag = "*";

    /// <summary>按 人员 > 拉线 > 部门 优先级解析员工命中的排班单</summary>
    public static List<string> FilterDocNos(List<ScheduleDoc> docs, Employee emp)
    {
        // 1) 人员级:InfoIDs 包含该员工工号
        var byEmp = docs.Where(d => NormalizeScope(d.ScopeType) == ScopeEmp
                                 && MatchMultiValue(d.InfoIDs, emp.InfoID)).ToList();
        if (byEmp.Any()) return byEmp.Select(d => d.DocNo).ToList();

        // 2) 拉线级
        var byLine = docs.Where(d => NormalizeScope(d.ScopeType) == ScopeLine
                                  && MatchMultiValue(d.LineNums, emp.LineNum)).ToList();
        if (byLine.Any()) return byLine.Select(d => d.DocNo).ToList();

        // 3) 部门级(ScopeType 为空/NULL 一律按部门,老数据零影响)
        return docs.Where(d => NormalizeScope(d.ScopeType) == ScopeDept
                            && d.CenterID == emp.DeptCode)
                   .Select(d => d.DocNo).ToList();
    }
}

两个约束值得记下来:范围值留空表示"不限"(等价于部门级,保证老数据行为不变);多值匹配必须精确切分(按半角/全角逗号、分号切分后 Trim 再比),不能用 Contains 做子串匹配,否则部门码 / 拉线码互为子串时会误命中。

3. 参数走反射缓存,界面改完即生效

14 项考勤参数如果做成方法参数层层传递,调用链会立刻失控。这里的做法是把参数表反射成强类型对象,进程内单例缓存:

// 1) 参数表 tb_AttendOptions:ParamCode 必须等于 AttendOptions 模型的属性名
// 2) 首次访问时反射加载,按 ParamCode 赋值到对象属性
AttendOptions options = HRCoreDAL.GetAttendOptionsCache();

// 3) 使用处直接读强类型属性,编译期即可发现拼写错误
var window = options.ValidTimeRangeMinutes;   // 打卡取值窗口半径
var grace  = options.GracePeriodMinutes;      // 宽松窗口

// 4) 界面保存后强制重载,立即生效(无需重启客户端)
HRCoreBLL.LoadAttendOptions();

这套机制的隐含契约是:ParamCode 必须与模型属性名完全一致,拼错不会报错、只会静默不生效。因此新增参数必须三处同步——表里插记录、模型加同名属性、使用处读属性。

四、平台技术栈

层次选型说明
开发语言C#全栈统一,客户端与服务端共用模型
运行时.NET 8(net8.0-windows)客户端 WinForms,服务端 API 同版本
界面框架WinForms + DevExpress 22.1.13Grid、编辑控件、打印与导出
数据访问EF Core 8.0.6 + SQL Server ProviderDatabase First,LINQ 查询,多账套切换
数据库SQL Server单库多账套,账套间数据隔离
报表FastReport 2023.1.8个人/部门考勤报表、导出 Excel/PDF
架构C/S(客户端 + 服务端 API)客户端自动升级、统一登录、在线授权
权限模块 + 功能 + 数据权限数据权限支持部门/角色/人员范围,敏感列按权限隐藏
审计数据日志 + 业务日志敏感字段变更留痕,异动有履历

为什么坚持 C/S:考勤机 SDK 是原生动态库(P/Invoke 调用),只有在客户端进程内才能稳定工作;车间与门卫室的网络环境普遍不稳,客户端本地缓存 + 断点重传比浏览器更可靠。而 DevExpress 的数据网格在处理"一天三班、一月三十天、上千人"的考勤明细时,分页与虚拟滚动是刚需。

五、考勤设备接入

1. 设备选型与两条通道

主用设备是海康威视人脸识别门禁考勤一体机,接入方式两条并行:

通道实现适用场景
HCNetSDKC# P/Invoke 封装(约两万行声明)设备登录、网络参数读取、考勤事件订阅与查询、员工与人脸照片下发
ISAPIHTTP + JSON(报文模型强类型化)人员分页查询、全量人员拉取、人脸照片设置/获取、事件按条件检索

两条通道各有取舍:SDK 功能全、性能好,但需要随程序分发原生 DLL(位数必须与进程一致);ISAPI 无依赖、易调试,但大批量分页拉取时要注意设备的返回缓冲与分页上限。

2. 封装分层

// 独立类库分层,界面层不碰 SDK 细节
// HikHelper           设备登录/登出/网络参数
// HikAttendHelper     考勤事件查询(支持多工号)
// HikEmployeeHelper   员工登记/查询/删除/全量分页拉取
// HikFaceHelper       人脸照片 设置/获取/删除/现场拍照
// CHCNetSDK.cs        SDK P/Invoke 声明
// Class/              ISAPI 报文模型(强类型)

界面层只调用 HikHelper.LoginDevice()、HikAttendHelper.Query() 这类高层方法,设备型号差异、SDK 返回值判断、超时重试都收敛在类库内。这样换设备型号或补一个新接口,不会牵动业务窗体。

3. 兜底方案

对异地办公、外派驻厂或尚未布机的场景,模块支持导入钉钉考勤 Excel(按模板映射工号、时间、方向),导入前预览原始记录并给出异常行提示。导入数据与设备采集进入同一张原始打卡表,后续归档口径完全一致——业务上不会出现"两套考勤"。

六、实施与验证建议

  1. 先配参数再算数据:参数、班次、排班、节假日缺一不可,否则算出来的数字没有意义
  2. 用"逐字等价"做改造验收:重构取卡算法这类高风险改动,最可靠的方式是改造前后各跑一次同月归档,逐人逐日比对迟到/早退/有效类型,差异必须为零
  3. 数据级验证优先于界面验证:先比对 SQL 结果集,再点界面。界面"看起来对了"是假象,数字对了才算改完
  4. 新增字段先建表再发客户端:模型加了字段而库里没列,EF 查询会直接报"列名无效",整条考勤链路会挂
  5. 双轨运行至少两个月:新系统与手工台账并行,差异归零后再封账

七、小结

这个模块真正难的不是"连考勤机、读打卡"——那是两天的活。难的是把行业里那些默认成规的规矩(三段班接力、拉线错峰、直接/间接人工不同口径、请假与加班交叉)翻译成一套可配置、可回归、可解释的算法,并且在业务不断加需求时还能守住等价性。

实践下来有效的三条经验:把业务字面量抽象成语义(槽位)、把重复的匹配逻辑收口到一个解析器、把参数做成强类型缓存并在保存时强制重载。这三件事做完,后面加错峰班次、加按人员排班,都只是配置层的事,算法不用再动。


C/S框架网|原创精神.创造价值.打造精品