服务类的设计

MR 人脸识别打卡系统将同一个业务场景下的不同功能交给某个服务(或者叫模块)进行统一管理。每一种服务都对应一个单独分服务类,所有服务类都放在 com.mr.clock.service 包下。项目中的服务类及其所提供的功能如表24.6所示。

image 2024 03 06 22 30 09 668
Figure 1. 表24.6 MR人脸识别打卡系统中的服务

摄像头服务

com.mr.clock.service 包下的 CameraService.java 为摄像头服务类,该服务类封装了所有关于摄像头的操作。

CameraService 类中的静态常量属性 WEBCAM 为当前计算机使用的默认摄像头。若计算机同时连接多个摄像头,则 WEBCAM 代表启用顺序排名第一的摄像头;若计算机没有连接任何摄像头,则 WEBCAM 为 null。静态常量属性 WEBCAM 的定义如下:

private static final Webcam WEBCAM = Webcam.getDefault();  // 摄像头对象

startCamera() 方法用于开启摄像头,这是捕获摄像头画面的前置操作,如果摄像头开启失败,方法会返回 false。startCamera() 方法的具体代码如下:

public static boolean startCamera() {
    if (WEBCAM == null) { // 如果计算机没有连接摄像头
        return false;
    }
    WEBCAM.setViewSize(new Dimension(640,480)); // 摄像头采用默认的 640 * 480
    return WEBCAM.open();  // 开启摄像头,返回开启是否成功
}

cameraIsOpen() 方法用于判断摄像头是否已经正常开启。因为从启动摄像头到摄像头可以捕捉到画面会有一段延迟时间,该方法可以通知其他程序摄像头是否已进入工作状态。cameraIsOpen() 方法的具体代码如下:

public static boolean cameralsOpen() {
    if (WEBCAM == null) {  // 如果计算机没有连接摄像头
        return false;
    }
    return WEBCAM.isOpen();
}

getCameraPanel() 方法用于返回一个 JPanel 面板,该面板会显示摄像头捕捉到的连续画面。该方法的具体代码如下:

public static JPanel getCameraPanel() {
    WebcamPanel panel = new WebcamPanel(WEBCAM); // 摄像头画面面板
    panel.setMirrored(true); // 开启镜像
    return panel;
}

getCameraFrame() 方法用于获取摄像头捕获的当前帧画面,返回值是 BufferedImage 类型的图像。该方法的具体代码如下:

public static BufferedImage getCameraFrame() {  // 获取当前帧画面
    return WEBCAM.getImage();
}

releaseCamera() 方法是释放摄像头的方法,当其他代码使用完摄像头之后,要及时调用该方法释放摄像头资源。该方法的具体代码如下:

/**
* 释放摄像头资源
*/
public static void releaseCamera() {
    if (WEBCAM != null) {
        WEBCAM.close();          // 关闭摄像头
    }
}

人脸识别服务

com.mr.clock.service 包下的 FaceEngineService.java 为人脸识别服务类,该服务类封装了所有关于人脸识别的功能。

人脸识别服务采用虹软科技提供的离线人脸识别SDK,读者在运行项目前需要到虹软科技视觉开发平台(ai.arcsoft.com.cn)注册账户,并申请试用离线人脸识别 SDK。在得到 app id 和 sdk key 两个激活码后,将其填写到 com.mr.clock.config 包下的 ArcFace.properties 文件中,并确保第一次运行项目时计算机可以连接互联网。

关于虹软科技人脸识别 SDK 的免费试用期限和商业版收费的标准,请以虹软科技官网内容为准。

FaceEngineService 类中有很多私有静态属性,appId 和 sdkKey 是从配置文件中读取的激活码。所有私有静态属性的定义如下:

private static String appId = null;
private static String sdkKey = null;
private static FaceEngine faceEngine = null;  // 人脸识别引擎
private static String ENGINE_PATH = "ArcFace/WIN64"; // 算法库文件夹地址
private static final String CONFIG_FILE = "src/com/mr/clock/config/ArcFace.properties"; // 配置文件地址

在使用人脸识别服务前,需要先对人脸识别引擎进行初始化操作。首先要从 com.mr.clock.config 包下的 ArcFace.properties 文件中读取读者填写的激活码,然后用激活码激活引擎,激活成功后再对引擎进行一系列功能性的配置,最后让引擎初始化。

这些操作都写在一个静态代码块中,具体代码如下:

static {

}

引擎初始化完成后,就可以使用人脸识别功能了。getFaceFeature() 方法可以从一张人脸图像中分析出此人的面部特征。参数img为被检测的图像:如果图像中没有人脸,方法会返回 null;如果图像中有人脸,则返回此人的面部特征对象。因为人脸识别引擎对图像有格式要求,所以在检测人脸之前,需要将传入的图像覆盖到一个标准 BRG 格式的临时图像之上,然后让人脸识别引擎分析临时图像。getFaceFeature() 方法的具体代码如下:

public static FaceFeature getFaceFeature(BufferedImage img) {

}

loadAllFaceFeature() 方法用于一次性加载所有员工的人脸特征,并将这些特征保存在 Session 类对象中。该方法会在项目启动时执行一次,这样项目启动完成之后就会形成一个公司内部的人脸特征库。loadAllFaceFeature() 方法的具体代码如下:

public static void loadAllFaceFeature() {

}

detectFace() 方法用于检测某个人脸特征是否可以在人脸特征库中找到匹配,参数 targetFaceFeature 为传入的人脸特征对象。detectFace() 方法会遍历人脸特征库,让传入人脸特征对象与每一个特征进行对比,记录最高对比得分,如果最高得分可以超过 90%,则认为匹配成功,该方法返回得分最高的员工特征码。detectFace() 方法的具体代码如下:

public static String detectFace(FaceFeature targetFaceFeature) {

}

dispost() 方法用于释放人脸识别引擎,该方法的具体代码如下:

/**
* 释放资源
*/
public static void dispost() {
    faceEngine.unInit();       // 卸载引擎
}

人事服务

本节使用的数据表:t_emp,t_lock_in_record,t_user,t_work_time

com.mr.clock.service 包下的 HRService.java 为人事服务类,该服务类封装了管理员登录、员工管理和计算考勤等所有与人事管理有关的功能。

HRService 类中有两类静态常量属性:一类是计算考勤报表用到的考勤标记,这些标记会记录员工的考勤状态;另一类是数据库接口对象。这些属性的定义如下:

loadAllEmp() 方法用于一次性从数据库读取所有员工的信息,并将这些员工信息保存在 Session 类对象中。该方法的具体代码如下:

public static void loadAllEmp() {
    Session.EMP_SET.clear();
    Session.EMP_SET.addAll(dao.getALLEmp());
}

userLogin() 方法用于检验管理员账号、密码是否正确,在该方法中,参数 username 为用户输入的管理员用户名,参数 password 为用户输入的密码。如果用户名和密码正确,则返回 true,表示登录成功并被保存在 Session 类对象中,否则返回 false。userLogin() 方法的具体代码如下:

public static boolean userLogin(String username, String password) {

}

addEmp() 方法用于添加新员工,在该方法中,参数 name 为新员工的姓名,参数 face 是为新员工拍的照片图像。该方法首先会通过 UUID 工具类为新员工生成一个随机的 32 位特征码,然后将新员工的数据插入数据库中,插入完毕后还要查询数据库为新员工分配的员工编号是多少,并将所有结果封装成一个新的员工对象,最后将新员工对象添加到 Session 类对象的全体员工集合中,并返回此新员工对象。addEmp() 方法的具体代码如下:

public static Employee addEmp(String name, BufferedImage face) {

}

deleteEmp() 方法用于删除员工,参数 id 为被删除员工的编号。彻底删除一个员工要分6步:第一步,在 Session 类对象的员工集合中删除该员工;第二步,在数据库的员工表中删除该员工;第三步,在数据库的打卡记录表中删除该员工的所有打卡记录;第四步,删除该员工的照片文件;第五步,在 Session 类对象的人脸特征库中删除该员工的人脸特征;第六步,在 Session 类对象的打卡记录集中删除该员工的所有记录。实现以上6步的具体代码如下:

public static void deleteEmp(int id) {

}

getEmp() 方法是一个经常被用到的方法,该方法用于获取用户对象,并有两种重载形式,分别是通过员工编号予以查找和通过员工特征码予以查找。这两个重载方法的具体代码如下:

public static Employee getEmp(int id) {

}

public static Employee getEmp(String code) {

}

addClockInRecord() 方法用于为某员工添加打卡记录,参数 e 为打卡的员工对象。该方法会以方法执行时间作为员工的打卡时间。该方法的具体代码如下:

public static void addClockInRecord(Employee e) {

}

loadAllClockInRecord() 方法用于一次性加载所有员工的打卡记录,并将打卡记录保存在 Session 类对象的打卡记录键值对中。该方法会在项目启动时执行一次。因为从数据库中读取的全部打卡记录是一个二维数组,所以需要对数据格式进行加工,对数组中员工编号进行归类,为每一位员工分配一个 HashSet 集合,将员工打卡时间字符串转换为 Date 对象,并添加到各自的集合中,这样就得到了一个以员工编号为键,以打卡日期集合为值的键值对结构。loadAllClockInRecord() 方法的具体代码如下:

public static void loadAllClockInRecord() {

}

loadWorkTime() 方法和 updateWorkTime() 方法用于加载和更新作息时间。更新操作会同时修改数据库和 Session 类对象中的作息时间。这两个方法的具体代码如下:

public static void loadWorkTime() {

}

public static void updateWorkTime(WorkTime time) {

}

getOneDayRecordData() 方法是获取某一天所有员工的考勤情况的方法,方法的3个参数分别是所要查询的年、月、日的数字,方法返回值是一个键值对,键为员工对象,值为员工的考勤标记。

考勤标记是由打卡标记(由类常亮属性提供)拼接而成的,可以如实地反映员工的打卡次数和每一次打卡所处的时间段。例如,某员工在某一天的考勤标记为“IEO”,表示该员工打了3次卡,分别是在上班时间打卡、在迟到的时间打卡和在下班时间打卡。系统需要根据实际情况对考勤标记进行分析,如果员工同时有正常上班打卡记录和迟到打卡记录,说明该员工在上班前已经到达公司,迟到记录可能是误打卡,不应算作考勤结果。

该方法中使用 Date 类提供的 before() 方法和 after() 方法来判断两个日期时间的先后顺序。如果 a 时间比 b 时间早,则 a.before(b) 返回 true,a.after(b) 返回 false。只有在 a 时间与 b 时间完全相同的情况下,a.equals(b) 才会返回 true。

该方法通过 5 个时间划分出了 4 种打卡时间段,如图24.6所示。上班时间和下班时间是由用户自己定义的,数据库默认的上班时间为 9:00:00,下班为 17:00:00。

image 2024 03 06 23 00 34 950
Figure 2. 图24.6 4种打卡时间段

如果员工一天的打卡次数少于两次,则认为员工漏打卡;如果员工一次打卡记录都没有,则认为员工缺席。getOneDayRecordData() 方法的具体代码如下:

private static Map<Employee,String> getOneDayRecordData(int year, int month, int day) {

}

getDayReport() 方法用来生成考勤日报字符串,方法的 3 个参数分别是要查询的年、月、日的数字。该方法首先会创建 3 个空的集合,分别用来存储迟到员工、早退员工和缺席员工;然后调用 getOneDayRecordData() 方法获取所有员工在这一天的考勤标记,分析这些标记,如果员工迟到了,就把该员工放进迟到集合里,以此类推,将打卡异常的员工放到对应的集合中;最后遍历这些集合,统计出多少人迟到、多少人早退、多少人缺席,并将这些人都列出来。getDayReport() 方法的具体代码如下:

public static String getDayReport(int year, int month, int day) {

}

getMonthReport() 方法用于获取考勤月报的数据。方法参数只有两个,分别是要查询的年和月的数字。方法返回值是一个二维数组,该数组第一列为员工姓名,从第二列至最后为该员工在该月份每一天的打卡情况。如果存在打卡异常情况,则会显示相应的文字提示;如果正常上下班打卡,则什么都不显示。getMonthReport() 方法的具体代码如下:

public static String[][] getMonthReport(int year, int month) {

}

图像文件服务

com.mr.clock.service 包下的 ImageService.java 为图像文件服务类,该服务类封装了所有关于员工照片文件的读写功能。

ImageService 类中有两个私有静态常量属性,FACE_DIR 表示存储员工照片文件的文件夹对象,SUFFIX 表示图像文件的默认格式。这两个属性的定义如下:

private static final File FACE_DIR = new File("src/faces");
private static final String SUFFIX = "png";

loadAllImage() 方法用于一次性加载所有员工照片,并将这些照片图像保存在 Session 类对象中。该方法会在项目启动时执行一次,这样项目启动完成后就会形成一个公司内部的照片库。loadAllImage() 方法的具体代码如下:

public static Map<String, BufferedImage> loadAllImage() {

}

saveFaceImage() 方法用于保存新员工的照片文件,该方法的参数 image 为新员工的照片图像,参数 code 为新员工的特征码。该方法会将新员工的特征码作为照片的文件名,将照片保存在 FACE_DIR 所指定的文件夹中,并采用 SUFFIX 所指定的格式。saveFaceImage() 方法的具体代码如下:

public static void saveFaceImage(BufferedImage image, String code) {

}

deleteFaceImage() 方法用于删除某员工的照片文件,参数 code 为该员工的特征码。该方法的具体代码如下:

/**
* 删除人脸图像文件
*/
public static void deleteFaceImage(String code) {

}