一、摘要
本文档将详细阐述如何使用RuoYi-Vue-Plus框架快速搭建一个基于SpringBoot和Vue3的前后端分离的考勤管理系统。本文档不仅涵盖了框架的基本使用和技术点介绍,还包括了详细的准备工作步骤、项目启动指南,以及如何连接mqtt服务器以实现实时考勤数据的传输和处理。更重要的是,本文将深入讲解如何对代码进行自定义和扩展,以满足企业实际的考勤管理需求。
二、技术点和准备工作
1.后端技术点
- Java17
- Mysql 8.0 / mongo数据库 (安装教程自行百度)
- MyBatis Plus:作为持久层框架,实现了数据库的CRUD操作,简化了与数据库的交互
- Redis : 作为缓存中间件,提高了系统性能和响应速度
- Spring Boot :作为核心框架,提供了一系列开箱即用的功能,如数据访问、消息传递、任务调度等
- Sa Token: 提供了强大的安全认证和授权功能
- Easy Excel: 快速、简洁、解决大文件内存溢出的Excel处理工具
2.前端技术点
- vue3
- Element Plus
- vite
3.软件和开发环境
jdk17 |
---|
Mysql 8 / mongo 7.0.2 |
Redis5 |
Maven3.9.6 |
Node 20.10.0 |
开发工具的话这里选择idea开发,以及我们项目需要使用docker拉取镜像,大家百度自行下载即可。
三、拉取项目并启动项目
1.拉取项目
下载方法有很多,
- 是本地安装了git 直接在目录输入以下命令
git clone https://gitee.com/dromara/RuoYi-Vue-Plus.git
2.是打开idea,记得登录gitee,直接克隆项目
https://gitee.com/dromara/RuoYi-Vue-Plus.git
- 是直接点击下载ZIP
下载方式就仁者见仁智者见智,大家挑喜欢的就好。
2.启动项目
动手能力强的可以参考文档地址: plus-doc 一步到位!
第一步我们先创建数据库;然后导入Sql,修改yml参数。修改mysql和redis连接信息即可
搞定这些我们就可以启动项目了!
至于前端部分,我们直接访问前端项目地址: 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
看到下图就启动成功了!
接下来我们输入 http://127.0.0.1:9000/ 他会直接 转发到MinIO Console;登录用户名和密码是admin/admin123。先创建桶,在创建key。记得不要把桶改成私密哦!然后我们去RuoYi-Vue-Plus 系统管理->文件管理->配置管理,填号对应的数据就行了!
然后自行上传图片测试一下即可!
四、MQTT服务器部署
在开始之前可能就有很多小伙伴不理解,mqtt是什么?为什么要部署这个呢?
mqtt简介
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种轻量级的、基于发布/订阅模式的消息传输协议,专为低带宽、高延迟或不可靠网络环境下的设备通信设计。MQTT由IBM于1999年开发,现已成为ISO/IEC 20922国际标准,广泛应用于物联网(IoT)、小型设备、移动应用等领域。
MQTT的核心特点包括:
- 轻量级:MQTT协议非常轻便,适用于带宽有限和资源受限的环境。
- 异步通信:发布者(Publisher)和订阅者(Subscriber)之间通过消息代理(Broker)进行异步通信,解耦了消息发送方与接收方,提升了系统的扩展性。
- 低功耗:MQTT协议设计简洁,消息传输开销小,有助于降低设备的能耗。
- 可靠性:通过不同的服务质量(QoS)级别,确保消息传递的可靠性。
- 灵活性:支持持久化会话、遗嘱消息等功能,适应不同需求。
MQTT的核心组件包括:
- 发布者(Publisher):负责将消息发布到特定的主题(Topic)。
- 订阅者(Subscriber):订阅特定的主题,接收并处理与该主题相关的消息。
- 消息代理(Broker):位于发布者和订阅者之间,负责接收来自客户端的消息并将其分发给相应的订阅者。
看到这里还不了解的小伙伴可以去百度或者哔哩哔哩大学了解一下!
mqtt部署
-
访问EMQX官网
-
获取 Docker 镜像
docker pull emqx/emqx:5.8.5
-
启动 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模块鼠标右键,新建->新模块->新建模块。
这里要注意父项不要选择错了!注意父项不要选择错了!注意父项不要选择错了!重要的事情说三遍
然后注意 ruoyi-modules 的 pom.xml、父项目的 pom.xml、以及ruoyi-admin的 pom.xml是否一致!
最后我们只需要导入我们需要的包就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 # 超时时间,单位:秒,默认:5秒
reconnect: 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了!
到此为止我们就成功接入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服务器
这里我是图深考勤机的演示,因为没有其他考勤机。连接方法都大同小异,大家百度一下的都应该可以解决。
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));
}
}
}
这时候我们打卡就能在页面上看见考勤记录了!!!
到这里我们的考勤系统就先告一段落了,后续会补充和填充计算考勤明细和考勤统计的代码。
需要源代码的同学可以添加我vx:chenbai0511
有什么不懂的也可以加微交流哦qwq