手把手教你打造基于RuoYi-Vue-Plus框架的考勤管理系统

175 阅读11分钟

一、摘要

本文档将详细阐述如何使用RuoYi-Vue-Plus框架快速搭建一个基于SpringBoot和Vue3的前后端分离的考勤管理系统。本文档不仅涵盖了框架的基本使用和技术点介绍,还包括了详细的准备工作步骤、项目启动指南,以及如何连接mqtt服务器以实现实时考勤数据的传输和处理。更重要的是,本文将深入讲解如何对代码进行自定义和扩展,以满足企业实际的考勤管理需求。

二、技术点和准备工作

1.后端技术点

  1. Java17
  2. Mysql 8.0 / mongo数据库 (安装教程自行百度)
  3. MyBatis Plus:作为持久层框架,实现了数据库的CRUD操作,简化了与数据库的交互
  4. Redis : 作为缓存中间件,提高了系统性能和响应速度
  5. Spring Boot :作为核心框架,提供了一系列开箱即用的功能,如数据访问、消息传递、任务调度等
  6. Sa Token: 提供了强大的安全认证和授权功能
  7. Easy Excel: 快速、简洁、解决大文件内存溢出的Excel处理工具

2.前端技术点

  1. vue3
  2. Element Plus
  3. vite

3.软件和开发环境

jdk17
Mysql 8 / mongo 7.0.2
Redis5
Maven3.9.6
Node 20.10.0

开发工具的话这里选择idea开发,以及我们项目需要使用docker拉取镜像,大家百度自行下载即可。

三、拉取项目并启动项目

1.拉取项目

访问 RuoYi-Vue-Plus 仓库地址

下载方法有很多,

  1. 是本地安装了git 直接在目录输入以下命令
git clone https://gitee.com/dromara/RuoYi-Vue-Plus.git

2.是打开idea,记得登录gitee,直接克隆项目

https://gitee.com/dromara/RuoYi-Vue-Plus.git
  1. 是直接点击下载ZIP

下载方式就仁者见仁智者见智,大家挑喜欢的就好。

2.启动项目

动手能力强的可以参考文档地址: plus-doc 一步到位!

第一步我们先创建数据库;然后导入Sql,修改yml参数。修改mysql和redis连接信息即可 image-20250304100639065.png

image.png

image.png

搞定这些我们就可以启动项目了!

至于前端部分,我们直接访问前端项目地址: plus-ui;克隆下来即可。

# 克隆项目
git clone https://gitee.com/JavaLionLi/plus-ui.git

# 安装依赖
npm install --registry=https://registry.npmmirror.com

# 启动服务
npm run dev

# 构建生产环境
npm run build:prod

# 前端访问地址 http://localhost:80

3.完善文件上传,minio方式

#ps: 博主是直接在win安装的docker所以用的命令是win的

第一步拉取镜像

docker pull minio/minio

第二步创建文件夹,我是直接创建在d盘

# 直接进入命令行,D:进入d盘
D:
# 创建minio文件夹
mkdir minio
# 进入minio文件夹
cd minio
# 创建data文件夹
mkdir data    
# 创建config文件夹
mkdir config

第三步直接执行启动命令

docker run --name minio -p 9000:9000 -p 9999:9999 -d --restart=always -e "MINIO_ROOT_USER=admin" -e "MINIO_ROOT_PASSWORD=admin123" -v D:\minio\data:/data -v D:\minio\config:/root/.minio minio/minio server /data --console-address 0.0.0.0:9999

看到下图就启动成功了!

image-20250304103353376.png

接下来我们输入 http://127.0.0.1:9000/ 他会直接 转发到MinIO Console;登录用户名和密码是admin/admin123。先创建桶,在创建key。记得不要把桶改成私密哦!然后我们去RuoYi-Vue-Plus 系统管理->文件管理->配置管理,填号对应的数据就行了!

image-20250304103948564.png

然后自行上传图片测试一下即可!

四、MQTT服务器部署

在开始之前可能就有很多小伙伴不理解,mqtt是什么?为什么要部署这个呢?

mqtt简介

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种轻量级的、基于发布/订阅模式的消息传输协议,专为低带宽、高延迟或不可靠网络环境下的设备通信设计。MQTT由IBM于1999年开发,现已成为ISO/IEC 20922国际标准,广泛应用于物联网(IoT)、小型设备、移动应用等领域。

MQTT的核心特点包括:

  1. 轻量级:MQTT协议非常轻便,适用于带宽有限和资源受限的环境。
  2. 异步通信:发布者(Publisher)和订阅者(Subscriber)之间通过消息代理(Broker)进行异步通信,解耦了消息发送方与接收方,提升了系统的扩展性。
  3. 低功耗:MQTT协议设计简洁,消息传输开销小,有助于降低设备的能耗。
  4. 可靠性:通过不同的服务质量(QoS)级别,确保消息传递的可靠性。
  5. 灵活性:支持持久化会话、遗嘱消息等功能,适应不同需求。

MQTT的核心组件包括:

  1. 发布者(Publisher):负责将消息发布到特定的主题(Topic)。
  2. 订阅者(Subscriber):订阅特定的主题,接收并处理与该主题相关的消息。
  3. 消息代理(Broker):位于发布者和订阅者之间,负责接收来自客户端的消息并将其分发给相应的订阅者。

看到这里还不了解的小伙伴可以去百度或者哔哩哔哩大学了解一下!

mqtt部署

  1. 访问EMQX官网

  2. 获取 Docker 镜像

    docker pull emqx/emqx:5.8.5
    
  3. 启动 Docker 容器

    docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx:5.8.5
    

我们可以通过浏览器访问 http://localhost:18083/ 以访问 EMQX Dashboard管理控制台,默认用户名及密码:admin/public。

到这里我们的mqtt服务器就部署好了,是不是很简单!!!

五、接入MQTT

1.新建mqtt模块

动手能力强的直接看文档!!!

在ruoyi-modules模块鼠标右键,新建->新模块->新建模块。 image-20250304110103088.png

image-20250304110226199.png

这里要注意父项不要选择错了!注意父项不要选择错了!注意父项不要选择错了!重要的事情说三遍

然后注意 ruoyi-modules 的 pom.xml、父项目的 pom.xml、以及ruoyi-admin的 pom.xml是否一致!

image-20250304111238112.png

image-20250304111353059.png

image-20250304111437290.png

最后我们只需要导入我们需要的包就OK了!

2.接入mqtt

第一步先导入maven坐标

        <dependency>
            <groupId>net.dreamlu</groupId>
            <artifactId>mica-mqtt-client-spring-boot-starter</artifactId>
            <version>2.2.10</version>
        </dependency>

第二步在yml文件里面填写mqtt连接信息

--- # mqtt配置信息
mqtt:
  client:
    enabled: true               # 是否开启客户端,默认:true
    ip: 192.168.158.12   # 连接的服务端 ip ,默认:broker.emqx.io
    port: 1883                  # 端口:默认:1883
    name: Mica-Mqtt-Client      # 名称,默认:Mica-Mqtt-Client
    client-id: orange      # 客户端Id(非常重要,一般为设备 sn,不可重复)
    user-name: orange-test    # 认证的用户名
    password: mqtt          # 认证的密码
    global-subscribe:           # 全局订阅的 topic,可被全局监听到,保留 session 停机重启,依然可以接受到消息。(2.2.9开始支持)
    timeout: 5                  # 超时时间,单位:秒,默认:5reconnect: false             # 是否重连,默认:true
    re-interval: 5000           # 重连时间,默认 5000 毫秒
    version: mqtt_3_1_1         # mqtt 协议版本,可选 MQTT_3_1、mqtt_3_1_1、mqtt_5,默认:mqtt_3_1_1
    read-buffer-size: 100KB       # 接收数据的 buffer size,默认:8k
    max-bytes-in-message: 10MB  # 消息解析最大 bytes 长度,默认:10M
    buffer-allocator: heap      # 堆内存和堆外内存,默认:堆内存
    keep-alive-secs: 60         # keep-alive 时间,单位:秒
    clean-session: true         # mqtt clean session,默认:true
    ssl:
      enabled: false            # 是否开启 ssl 认证,2.1.0 开始支持双向认证
      keystore-path:            # 可选参数:ssl 双向认证 keystore 目录,支持 classpath:/ 路径。
      keystore-pass:            # 可选参数:ssl 双向认证 keystore 密码
      truststore-path:          # 可选参数:ssl 双向认证 truststore 目录,支持 classpath:/ 路径。
      truststore-pass:          # 可选参数:ssl 双向认证 truststore 密码

第三步我们重启项目,就能连接上EMQX了!

image-20250304142223014.png 到此为止我们就成功接入MQTT!!!

六、创建考勤机表以及设备连接MQTT服务器

1.创建考勤机表

建表SQL,大家参考自己的业务修改。


CREATE TABLE `你的数据库名称`.`你的表名`  (
  `attendance_machine_id` bigint NOT NULL COMMENT '考勤机id',
  `tenant_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '000000' COMMENT '项目编号',
  `device_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '设备名称',
  `sn` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '' COMMENT '设备编号',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '0' COMMENT '状态(0正常 1离线)',
  `lock_in_out_status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '' COMMENT '入出状态(0=入,1=出)',
  `model` char(2) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '0' COMMENT '设备型号(1=图深人脸机,2=宇泛人脸机)',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '备注',
  `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`attendance_machine_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '考勤设备表' ROW_FORMAT = Dynamic;

之后就没什么难的,代码生成就行了,前后端代码。

2.设备连接MQTT服务器

这里我是图深考勤机的演示,因为没有其他考勤机。连接方法都大同小异,大家百度一下的都应该可以解决。

image-20250304151503706.png

mqtt地址、端口就是MQTT服务器ip、端口,mqtt用户名最好用考勤机sn码,密码自定义即可,要注意mqtt推送路径和订阅路径,需是device/tushen/设备sn码/Upstream、device/tushen/设备sn码/Downstream,记得勾上上报考勤和图片!!!

七、完善考勤机页面

在第六步我们代码生成了考勤机的前后端代码,在菜单添加上权限即可。我们要做的是第一步工作是添加上考勤机然后在检测它的状态。EMQX自带REST-API可以查询考勤机状态。关于EMQX的REST-API部分,大家自行参考官网。

刷新考勤机状态java代码

    /**
     * 刷新考勤机状态
     *
     * @return
     */
    @SaCheckPermission("mqtt:devices:refresh")
    @GetMapping("/refresh")
    public R<Void> refreshAttendanceMachineStatus() {
        return toAjax(attendanceDevicesService.refreshAttendanceMachineStatus());
    }

    /**
     * 刷新考勤机状态
     *
     * @return
     */
    @Override
    public Boolean refreshAttendanceMachineStatus() {
        List<AttendanceDevices> devices = baseMapper.selectList();
        if (devices.isEmpty()) {
            return false;
        }
        String host = sysConfigServiceApi.getConfigKey("mqtt.host");
        String apiKey = sysConfigServiceApi.getConfigKey("mqtt.apiKey");
        String secretKey = sysConfigServiceApi.getConfigKey("mqtt.secretKey");
        String url = String.format(MqttApiConstants.GET_ALL_MQTT_CLIENTS_URL, host, 1, devices.size() * 10);
        Object result = exchange(url, apiKey, secretKey);
        Map<String, DeviceStatusInfo> statusInfoMap = parseDeviceStatusInfo(result);
        if (statusInfoMap.isEmpty()) {
            return false;
        }
        devices.stream()
            .forEach(device -> {
                device.setStatus("1");
                DeviceStatusInfo info = statusInfoMap.get(device.getSn());
                if (info != null && info.getConnected()) {
                    device.setStatus("0");
                }
            });
        return baseMapper.updateBatchById(devices);
    }

    /**
     * 解析MQTT响应,将结果转换为DeviceStatusInfo列表
     *
     * @param result
     * @return
     */
    private Map<String, DeviceStatusInfo> parseDeviceStatusInfo(Object result) {
        Map<String, DeviceStatusInfo> statusInfoMap = new HashMap<>();
        if (result != null) {
            try {
                if (result instanceof JSONObject) {
                    JSONObject object = (JSONObject) result;
                    // 从JSON对象中提取数据并解析为DeviceStatusInfo对象列表
                    List<DeviceStatusInfo> list = JSONArray.parseArray(object.getString("data"), DeviceStatusInfo.class);
                    for (DeviceStatusInfo info : list) {
                        statusInfoMap.put(info.getClientid(), info);
                    }
                } else if (result instanceof JSONArray) {
                    JSONArray array = (JSONArray) result;
                    // 将JSONArray直接转换为DeviceStatusInfo对象列表
                    List<DeviceStatusInfo> list = array.toJavaList(DeviceStatusInfo.class);
                    for (DeviceStatusInfo info : list) {
                        statusInfoMap.put(info.getClientid(), info);
                    }
                }
            } catch (Exception e) {
                log.error("解析MQTT响应出错: {}", e);
            }
        }
        return statusInfoMap;
    }

    /**
     * 交换数据
     *
     * @param url
     * @param apiKey
     * @param secretKey
     * @return
     */
    private Object exchange(String url, String apiKey, String secretKey) {
        HttpResponse response = HttpRequest.get(url)
            .basicAuth(apiKey, secretKey)
            .header("Accept", "application/json")
            .timeout(10000)
            .execute();
        if (response.getStatus() == 200 && response.body() != null) {
            String body = response.body();
            if (body.startsWith("{")) { // JSON 对象
                return JSONObject.parseObject(body);
            } else if (body.startsWith("[")) { // JSON 数组
                return JSONArray.parseArray(body);
            }
        }
        return null;
    }

这里前端代码过于简单就不展示了QWQ

到这里我们的考勤机页面就算完工了!!!

八、考勤机推送消息给服务端

我这里使用的是图深的人脸机进行测试,图深在线文档地址 大家可以自行观看文档进行定制化开发。

这里我直接提供代码大家粘贴即可!!!

1.MqttClientConnectListener代码

/**
 * 监听MQTT客户端连接和断开连接的事件
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@RequiredArgsConstructor
@Service
public class MqttClientConnectListener {

    private static final Logger logger = LoggerFactory.getLogger(MqttClientConnectListener.class);

    @Value("${mqtt.client.client-id}")
    private String clientId;

    @Value("${mqtt.client.user-name}")
    private String userName;

    @Value("${mqtt.client.password}")
    private String password;

    /**
     * MQTT客户端创建器注入
     */
    private final MqttClientCreator mqttClientCreator;

    /**
     * 监听MQTT连接事件
     *
     * @param event
     */
    @EventListener
    public void onConnected(MqttConnectedEvent event) {
        // 打印MqttConnectedEvent信息
        logger.info("MqttConnectedEvent:{}", event);
    }

    /**
     * 监听MQTT断开连接事件
     * @param event
     */
    @EventListener
    public void onDisconnect(MqttDisconnectEvent event) {
        // 离线时更新重连时的密码,适用于类似阿里云 mqtt clientId 连接带时间戳的方式
        logger.info("MqttDisconnectEvent:{}", event);
        // 在断线时更新 clientId、username、password
        mqttClientCreator.clientId(clientId + System.currentTimeMillis())
            .username(userName)
            .password(password);
    }
}

2.MqttClientSubscribeListener 代码

/**
 * 启动时订阅MQTT主题
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@Component
@Slf4j
public class MqttClientSubscribeListener implements ApplicationRunner {

    @Autowired
    private MqttClientTemplate client;

    @Autowired
    private IAttendanceDevicesService attendanceDevicesService;

    @Autowired
    private IDepthMqttMessageService depthMqttMessageService;

    /**
     * ApplicationRunner 接口的核心方法
     * 应用启动时会自动调用该方法
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 查询考勤设备列表
        List<AttendanceDevicesVo> attendanceDevicesVos = attendanceDevicesService.queryList(null);
        for (AttendanceDevicesVo vo : attendanceDevicesVos) {
            // 图深
            if (AttendanceDevicesModel.TUSHEN.equals(vo.getModel())) {
                // 上行主题
                String upstreamTopic = "device/tushen/" + vo.getSn() + "/Upstream";
                // 下行主题
                String downstreamTopic = "device/tushen/" + vo.getSn() + "/Downstream";
                // 去除主题中的非法字符
                upstreamTopic = upstreamTopic.replaceAll("[^a-zA-Z0-9_/]", "");
                downstreamTopic = upstreamTopic.replaceAll("[^a-zA-Z0-9_/]", "");
                // 订阅上行主题
                client.subQos0(upstreamTopic, (context, topic, message, payload) -> {
                    // 将payload转换为字符串
                    String data = new String(payload, StandardCharsets.UTF_8);
                    // 将字符串转换为DepthMqttMessage对象
                    DepthMqttMessage mqttMessage = JSONObject.parseObject(data, DepthMqttMessage.class);
                    // 处理DepthMqttMessage对象
                    depthMqttMessageService.handle(mqttMessage, topic);
                });
                // 订阅下行主题
                client.subQos0(downstreamTopic, (context, topic, message, payload) -> {
                    // 打印下行主题和payload
                    log.info(topic + '\t' + new String(payload, StandardCharsets.UTF_8));
                });
            }
        }
    }
}

3.DepthMqttMessage代码

/**
 * 图深mqtt消息
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@Data
public class DepthMqttMessage {
    /**
     * 会话 ID
     */
    private String msgId;

    /**
     * 指令
     */
    private String cmd;

    /**
     * 设备ID
     */
    private String devID;

    /**
     * 具体请求参数
     */
    private Object param;

    /**
     * 状态码 0成功,其他失败
     */
    private String code;

    /**
     * ok或失败时错误信息
     */
    private String msg;

    /**
     * 返回的消息
     */
    private Object data;
}

4.IDepthMqttMessageService代码

/**
 * 图深处理回复 客户端消息
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
public interface IDepthMqttMessageService {

    /**
     * 处理mqtt消息
     *
     * @param mqttMessage mqtt 实体参数
     * @param topic       订阅的主题
     */
    void handle(DepthMqttMessage mqttMessage, String topic);
}

5.DepthMqttMessageServiceImpl代码

/**
 * 图深回复客户端接口实现类
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@Service
@Slf4j
public class DepthMqttMessageServiceImpl implements IDepthMqttMessageService {
    @Override
    public void handle(DepthMqttMessage mqttMessage, String topic) {
        String cmd = mqttMessage.getCmd();
        log.info("收到图深设备消息:{}", cmd);
    }
}

重启项目我们就能收到设备给我们发送的mqtt消息了!!!

相信仔细看代码的小伙伴就知道我们在6.2为什么注意mqtt推送路径和订阅路径路径了。

九、存储考勤信息到mongo数据库

我们之前的配置已经能够成功接收设备发送的MQTT消息。接下来,为了实现考勤功能,设备将主动推送考勤消息到我们的Java后端。我们只需接收这些消息并将其存储起来即可。在这里,我们选择了MongoDB作为存储数据库,当然,根据业务需求,大家也可以选择MySQL等其他数据库。对于MongoDB的CRUD操作,我们决定使用Mongo Plus这款工具来简化开发过程。

动手能力强的可以直接看文档:Mongo Plus文档地址

1.maven导入mongo plus

<!-- springboot -->
<dependency>
    <groupId>com.mongoplus</groupId>
    <artifactId>mongo-plus-boot-starter</artifactId>
    <version>2.1.7</version>
</dependency>
<!-- solon -->
<dependency>
    <groupId>com.mongoplus</groupId>
    <artifactId>mongo-plus-solon-plugin</artifactId>
    <version>2.1.7</version>
</dependency>

2.yml配置mongo plus连接信息

# mongo-plus
mongo-plus:
  data:
    mongodb:
      host: 127.0.0.1  #ip
      port: 27017   #端口
      database: ry-vue-plus    #数据库名
      username: ry-vue-plus    #用户名,没有可不填(若账号中出现@,!等等符号,不需要再进行转码!!!)
      password: ry-vue-plus    #密码,同上(若密码中出现@,!等等符号,不需要再进行转码!!!)
      authenticationDatabase: ry-vue-plus     #验证数据库
      connectTimeoutMS: 50000   #在超时之前等待连接打开的最长时间(以毫秒为单位)
  log: true
  pretty: true
  configuration:
    collection:
      mappingStrategy: CLASS_NAME
      block-attack-inner: true

3.存储考勤信息

mongo plus 无需手动建立集合,建好实体就行了。这里我也是直接粘贴代码给大家看,大家依次建立好就行了。

(1).Record代码


/**
 * 识别记录表
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@Data
@CollectionName("record")
public class Record {

    /**
     * id
     */
    @ID(type = IdTypeEnum.ASSIGN_UUID)
    private String id;

    /**
     * 项目编号
     */
    @NotNull(message = "项目编号不能为空")
    private String tenantId;

    /**
     * 出/入
     */
    private String lockInOutStatus;


    /**
     * 人脸机设备sn
     */
    @NotNull(message = "考勤机编号不能为空")
    private String sn;

    /**
     * 识别记录 ID
     */
    private String recordId;

    /**
     * 人员 ID
     */
    private String personId;

    /**
     * 人员姓名
     */
    private String name;

    /**
     * 图片长度
     */
    private String imgLength;

    /**
     * 图片内容(base64)
     */
    private String imgData;

    /**
     * 卡号
     * 刷卡进入
     */
    private String cardId;

    /**
     * 识别方式
     * 刷脸认证:0
     * 人卡合一认证:1
     * 人证比对:2
     * 刷卡认证:3
     * 按钮开门:4
     * 远程开门:5
     * 密码开门:6
     * 人 + 密码开门:7
     */
    private String type;

    /**
     * 创建时间
     */
    private Long createdAt;
}

(2).RecordController代码

/**
 * 考勤表控制器
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/mqtt/record")
public class RecordController {
    private final IRecordServiceApi recordServiceApi;

    /**
     * 分页查询考勤记录
     * @param bo
     * @param pageQuery
     * @return
     */
    @SaCheckPermission("mqtt:record:list")
    @GetMapping("/list")
    public TableDataInfo<Record> list(RecordBo bo, PageQuery pageQuery) {
        return recordServiceApi.queryPageList(bo, pageQuery);
    }
}

(3).IRecordServiceApi代码

/**
 * 考勤表
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
public interface IRecordServiceApi {

    /**
     * 分页查询考勤记录
     * @param bo
     * @param pageQuery
     * @return
     */
    TableDataInfo<Record> queryPageList(RecordBo bo, PageQuery pageQuery);
}

(4).RecordServiceApiImpl代码

/**
 * 考勤表接口实现类
 *
 * @Author: 陈江灿
 * @CreateTime: 2025-03-05
 */
@RequiredArgsConstructor
@Service
public class RecordServiceApiImpl  extends ServiceImpl<Record> implements IRecordServiceApi {
    /**
     * 分页查询考勤记录
     * @param bo
     * @param pageQuery
     * @return
     */
    @Override
    public TableDataInfo<Record> queryPageList(RecordBo bo, PageQuery pageQuery) {
        LambdaQueryChainWrapper<Record> lwq = this.lambdaQuery();
        lwq.eq(StringUtils.isNotEmpty(bo.getPersonId()), Record::getPersonId, bo.getPersonId());
        lwq.like(StringUtils.isNotEmpty(bo.getName()), Record::getName, bo.getName());
        lwq.like(StringUtils.isNotEmpty(bo.getSn()), Record::getSn, bo.getSn());
        String tenantId = LoginHelper.getTenantId();
        lwq.eq(StringUtils.isNotEmpty(tenantId), Record::getTenantId, tenantId);
        if (bo.getBeginCreateTime() != null && bo.getEndCreateTime() != null) {
            lwq.between(true, Record::getCreatedAt, bo.getBeginCreateTime(), bo.getEndCreateTime(), false);
        }
        lwq.orderByDesc(Record::getCreatedAt);
        PageResult<Record> page = this.page(lwq, pageQuery.getPageNum(), pageQuery.getPageSize());
        TableDataInfo<Record> tableDataInfo = new TableDataInfo<>();
        tableDataInfo.setTotal(page.getTotalSize());
        tableDataInfo.setRows(page.getContentData());
        tableDataInfo.setMsg("查询成功");
        tableDataInfo.setCode(HttpStatus.HTTP_OK);
        return tableDataInfo;
    }
}

(5).vue页面代码

<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
                :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="search" v-show="showSearch">
        <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="90px">
          <el-form-item label="人员 ID" prop="personId">
            <el-input v-model="queryParams.personId" placeholder="请输入人员 ID" clearable
                      style="width: 240px" @keyup.enter="handleQuery"/>
          </el-form-item>
          <el-form-item label="人员姓名" prop="name">
            <el-input v-model="queryParams.name" placeholder="请输入人员姓名" clearable
                      style="width: 240px" @keyup.enter="handleQuery"/>
          </el-form-item>
          <el-form-item label="创建时间" style="width: 308px">
            <el-date-picker
              v-model="dateRangeCreateTime"
              value-format="YYYY-MM-DD HH:mm:ss"
              type="daterange"
              range-separator="-"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
            />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
    </transition>

    <el-card shadow="never">
      <template #header>
        <el-row :gutter="10" class="mb8">
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
        </el-row>
      </template>

      <el-table v-loading="loading" :data="list" border>
        <el-table-column label="人员ID" align="center" prop="personId" width="100" fixed/>
        <el-table-column label="人员姓名" align="center" prop="name"  width="160" fixed/>
        <el-table-column label="sn" align="center" prop="sn"  width="160" fixed/>
        <el-table-column label="打卡时间" align="center" prop="createdAt" min-width="120">
          <template #default="scope">
            <span>{{ timeFormatDate(scope.row.createdAt) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="识别方式" align="center">
          人脸打卡
        </el-table-column>
        <el-table-column label="入出状态" align="center" prop="lockInOutStatus">
          <template #default="scope">
            <dict-tag :options="lock_in_out_status" :value="scope.row.lockInOutStatus"/>
          </template>
        </el-table-column>
        <el-table-column label="打卡图片" align="center" prop="imgData" width="120">
          <template #default="scope">
            <el-image
              style="width: 50px; height: 50px;"
              :src="'data:image/png;base64,' + scope.row.imgData"
              :preview-src-list="['data:image/png;base64,' + scope.row.imgData]"
              :hide-on-click-modal="true"
              :index="999"
              :initial-index="0"
              :preview-teleported="true"
              fit="cover"
            ></el-image>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script setup lang="ts" name="Records">
import {onMounted, ref} from 'vue';
import {listRecord} from '@/api/mqtt/record';
import {RecordVo, RecordQuery} from '@/api/mqtt/record/types';

const {proxy} = getCurrentInstance() as ComponentInternalInstance;
const {lock_in_out_status} = toRefs<any>(proxy?.useDict('lock_in_out_status'));

const loading = ref<boolean>(true);
const list = ref<RecordVo[]>();
const total = ref<number>(0);
const showSearch = ref<boolean>(true);
const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);

const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<RecordQuery>({
  pageNum: 1,
  pageSize: 10,
  sn: '',
  personId: '',
  name: '',
  cardId: '',
  type: '',
  createdAt: 0,
  beginCreateTime: undefined,
  endCreateTime: undefined
});

/**
 * 时间格式化为 YYYY-MM-DD hh:mm:ss
 */
const timeFormatDate = (dateString: string) => {
  if (!dateString) {
    return '未获取到时间';
  }
  const date = new Date(dateString);
  const year = date.getFullYear();
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
  const day = date.getDate().toString().padStart(2, '0');
  const hours = date.getHours().toString().padStart(2, '0');
  const minutes = date.getMinutes().toString().padStart(2, '0');
  const seconds = date.getSeconds().toString().padStart(2, '0');
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};

/** 格式化时间 */
const formatDate = (date: Date): string => {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const hours = String(date.getHours()).padStart(2, '0');
  const minutes = String(date.getMinutes()).padStart(2, '0');
  const seconds = String(date.getSeconds()).padStart(2, '0');
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};

/** 计算默认日期范围 */
const calculateDefaultDateRange = () => {
  const now = new Date();
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
  const startDate = new Date(startOfMonth.setHours(0, 0, 0, 0));
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const endDate = new Date(today.setHours(23, 59, 59, 0));
  dateRangeCreateTime.value = [formatDate(startDate), formatDate(endDate)];
};

/** 时间参数获取 */
const getDate = () => {
  calculateDefaultDateRange();
  if (dateRangeCreateTime.value[0] != '' && dateRangeCreateTime.value[0] != null) {
    const [startDate, endDate] = dateRangeCreateTime.value;
    if (startDate && endDate) {
      if (typeof startDate === 'string' && typeof endDate === 'string') {
        const formattedStartDate = new Date(startDate.replace(' ', 'T'));
        const formattedEndDate = new Date(endDate.replace(' ', 'T'));
        queryParams.value.beginCreateTime = formattedStartDate.getTime();
        queryParams.value.endCreateTime = formattedEndDate.getTime();
      }
    }
  }
}

/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  dateRangeCreateTime.value = ['', ''];
  queryParams.value.beginCreateTime = undefined;
  queryParams.value.endCreateTime = undefined;
  handleQuery();
};

/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
};

const getList = async () => {
  loading.value = true;
  await getDate();
  const res = await listRecord(queryParams.value);
  list.value = res.rows;
  total.value = res.total;
  loading.value = false;
}

onMounted(async () => {
  await nextTick();
  await getList();
});
</script>

到这里我们就可以看见我们的考勤记录列表了!!!

4.打卡信息存储mongo数据库

老规矩展示代码!!!

   @Override
    public void handle(DepthMqttMessage mqttMessage, String topic) {
        String cmd = mqttMessage.getCmd();
        log.info("收到图深设备消息:{}", cmd);
        // 识别信息自动推送
        if ("reportVisitRecord".equals(cmd)) {
            String param = String.valueOf(mqttMessage.getParam());
            JSONObject jsonObject = parseObject(param);
            if (ObjectUtil.isNotNull(jsonObject)) {
                LambdaQueryWrapper<AttendanceDevices> lwq = Wrappers.lambdaQuery();
                lwq.eq(AttendanceDevices::getSn, mqttMessage.getDevID());
                AttendanceDevices attendanceDevicesVo = attendanceDevicesMapper.selectOne(lwq);
                Record record = new Record();
                record.setLockInOutStatus(attendanceDevicesVo.getLockInOutStatus());
                record.setTenantId(attendanceDevicesVo.getTenantId());
                record.setName(jsonObject.getString("UserName"));
                record.setImgData(jsonObject.getString("FaceImgBase64"));
                record.setSn(mqttMessage.getDevID());
                record.setCreatedAt(jsonObject.getLong("TimeStamp"));
                record.setPersonId(jsonObject.getString("CustomID"));
                record.setRecordId(mqttMessage.getMsgId());
                record.setType("0");
                recordServiceApi.addRecord(record);
                DepthMqttMessage message = new DepthMqttMessage();
                message.setMsgId(UUID.randomUUID().toString());
                message.setCmd("reportVisitRecord");
                message.setDevID(mqttMessage.getDevID());
                message.setMsg("ok");
                message.setCode("0");
                String topicDownstream = "device/Uface/" + mqttMessage.getDevID() + "/Downstream";
                client.publish(topicDownstream, JSONObject.toJSONString(message).getBytes(StandardCharsets.UTF_8));
            }
        }
    }

这时候我们打卡就能在页面上看见考勤记录了!!!

image-20250305145816756.png

到这里我们的考勤系统就先告一段落了,后续会补充和填充计算考勤明细和考勤统计的代码。

需要源代码的同学可以添加我vx:chenbai0511

有什么不懂的也可以加微交流哦qwq