增加移动考勤机
This commit is contained in:
@ -18,6 +18,14 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Java WebSocket 标准API -->
|
||||
<dependency>
|
||||
<groupId>javax.websocket</groupId>
|
||||
<artifactId>javax.websocket-api</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.drewnoakes</groupId>-->
|
||||
<!-- <artifactId>metadata-extractor</artifactId>-->
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
package org.dromara.mobileAttendanceMachine;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @Author 铁憨憨
|
||||
* @Date 2025/10/14 15:48
|
||||
* @Version 1.0
|
||||
*
|
||||
* 考勤设备消息发送工具类(对应Golang的ws包工具方法)
|
||||
*/
|
||||
|
||||
@Service
|
||||
@Log4j2
|
||||
public class DeviceMessageSender {
|
||||
/**
|
||||
* 组装下发人员信息(对应Golang的TheSenderInformationOfTheAssemblyPersonnel)
|
||||
* @param sn 设备编号
|
||||
* @param userId 用户ID
|
||||
* @param name 用户名
|
||||
* @param face 人脸模板地址(HTTP链接)
|
||||
* @return 异常信息(无异常则返回null)
|
||||
*/
|
||||
public static Exception sendPersonnelInformation(String sn, String userId, String name, String face) {
|
||||
try {
|
||||
// 生成UUID
|
||||
String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits();
|
||||
|
||||
// 构建人员信息消息体
|
||||
KqjEntity.PeopleInformation people = new KqjEntity().new PeopleInformation();
|
||||
people.setCmd("to_device");
|
||||
people.setFrom(sUuid);
|
||||
people.setTo(sn);
|
||||
|
||||
// 构建消息数据
|
||||
KqjEntity.PeopleInData data = new KqjEntity().new PeopleInData();
|
||||
data.setCmd("addUser");
|
||||
data.setUserId(userId);
|
||||
data.setName(name);
|
||||
data.setFaceTemplate(face);
|
||||
data.setIdValid(""); // 永久有效
|
||||
people.setData(data);
|
||||
|
||||
// 发送请求并等待响应(忽略响应结果,只关注是否成功)
|
||||
DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, people);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("下发人员信息失败,SN: {}, UserID: {}", sn, userId, e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取打卡设备所有人员(对应Golang的SelectUserAll)
|
||||
* @param sn 设备编号
|
||||
* @return 响应结果(包含人员信息)
|
||||
* @throws Exception 发送或接收过程中的异常
|
||||
*/
|
||||
public static KqjEntity.CommonResponse getAllUsers(String sn) throws Exception {
|
||||
// 生成UUID
|
||||
String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits();
|
||||
|
||||
// 构建获取人员信息请求
|
||||
KqjEntity.PersonnelInformationAcquisition request = new KqjEntity().new PersonnelInformationAcquisition();
|
||||
request.setCmd("to_device");
|
||||
request.setFrom(sUuid);
|
||||
request.setTo(sn);
|
||||
|
||||
// 构建请求数据
|
||||
KqjEntity.PersonnelInformationAcquisitionTwo data = new KqjEntity().new PersonnelInformationAcquisitionTwo();
|
||||
data.setCmd("getUserInfo");
|
||||
data.setValue(1); // 1表示获取所有人员
|
||||
request.setData(data);
|
||||
|
||||
// 发送请求并返回响应
|
||||
return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定人员(对应Golang的DelByUserId)
|
||||
* @param sn 设备编号
|
||||
* @param userId 要删除的用户ID
|
||||
* @return 响应结果
|
||||
* @throws Exception 发送或接收过程中的异常
|
||||
*/
|
||||
public static KqjEntity.CommonResponse deleteUser(String sn, String userId) throws Exception {
|
||||
// 生成UUID
|
||||
String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits();
|
||||
|
||||
// 构建删除人员请求
|
||||
KqjEntity.DeletionOfPersonnel request = new KqjEntity().new DeletionOfPersonnel();
|
||||
request.setCmd("to_device");
|
||||
request.setFrom(sUuid);
|
||||
request.setTo(sn);
|
||||
|
||||
// 构建请求数据
|
||||
KqjEntity.DeletionOfPersonnelData data = new KqjEntity().new DeletionOfPersonnelData();
|
||||
data.setCmd("delUser");
|
||||
data.setUserId(userId);
|
||||
data.setUserType(0); // 0表示人脸接口下发的数据
|
||||
request.setData(data);
|
||||
|
||||
// 发送请求并返回响应
|
||||
return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除指定人员(对应Golang的BatchDelete)
|
||||
* @param sn 设备编号
|
||||
* @param userIds 要删除的用户ID列表
|
||||
* @return 响应结果
|
||||
* @throws Exception 发送或接收过程中的异常
|
||||
*/
|
||||
public static KqjEntity.CommonResponse batchDeleteUsers(String sn, String[] userIds) throws Exception {
|
||||
// 生成UUID
|
||||
String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits();
|
||||
|
||||
// 构建批量删除请求
|
||||
KqjEntity.BatchDeletion request = new KqjEntity().new BatchDeletion();
|
||||
request.setCmd("to_device");
|
||||
request.setFrom(sUuid);
|
||||
request.setTo(sn);
|
||||
|
||||
// 构建请求数据
|
||||
KqjEntity.BatchDeletionData data = new KqjEntity().new BatchDeletionData();
|
||||
data.setCmd("delMultiUser");
|
||||
data.setUserIds(userIds);
|
||||
data.setUserType(0); // 0表示人脸接口下发的数据
|
||||
request.setData(data);
|
||||
|
||||
// 发送请求并返回响应
|
||||
return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定考勤机全部人员(对应Golang的DelAll)
|
||||
* @param sn 设备编号
|
||||
* @return 响应结果
|
||||
* @throws Exception 发送或接收过程中的异常
|
||||
*/
|
||||
public static KqjEntity.CommonResponse deleteAllUsers(String sn) throws Exception {
|
||||
// 生成UUID
|
||||
String sUuid = DeviceWebSocketServer.generateUUIDWithSixRandomDigits();
|
||||
|
||||
// 构建删除全部人员请求
|
||||
KqjEntity.DeletionALlOfPersonnel request = new KqjEntity().new DeletionALlOfPersonnel();
|
||||
request.setCmd("to_device");
|
||||
request.setFrom(sUuid);
|
||||
request.setTo(sn);
|
||||
|
||||
// 构建请求数据
|
||||
KqjEntity.DeletionALlOfPersonnelData data = new KqjEntity().new DeletionALlOfPersonnelData();
|
||||
data.setCmd("delAllUser");
|
||||
data.setUserType(0); // 0表示人脸接口下发的数据
|
||||
request.setData(data);
|
||||
|
||||
// 发送请求并返回响应
|
||||
return DeviceWebSocketServer.sendRequestAndWaitResponse(sn, sUuid, request);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,472 @@
|
||||
package org.dromara.mobileAttendanceMachine;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.websocket.*;
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.http.WebSocket;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* WebSocket服务端实现(对应Golang的HandleWebSocket逻辑)
|
||||
* ServerEndpoint注解指定WebSocket连接路径
|
||||
*/
|
||||
@ServerEndpoint("/ws/device")
|
||||
@Log4j2
|
||||
public class DeviceWebSocketServer {
|
||||
|
||||
// ------------------------------ 常量定义(对应Golang的const) ------------------------------
|
||||
public static class Constants {
|
||||
public static final String DECLARE = "declare"; // 设备初上线
|
||||
public static final String PING = "ping"; // 心跳
|
||||
public static final String TO_CLIENT = "to_client"; // 服务器消息下发到客户端的响应
|
||||
}
|
||||
|
||||
// JSON序列化/反序列化工具(单例)
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 1. 存储所有连接的设备信息(key: 设备SN,value: 设备信息)
|
||||
private static final Map<String, KqjEntity.DeviceInfo> connectedDevices = new ConcurrentHashMap<>();
|
||||
// 2. 存储UUID对应的响应通道(key: UUID,value: 响应结果容器)
|
||||
private static final Map<String, ResponseHolder<KqjEntity.CommonResponse>> responseChannels = new ConcurrentHashMap<>();
|
||||
|
||||
// 当前连接的WebSocket会话
|
||||
private Session session;
|
||||
// 当前连接的设备SN(连接建立后从DECLARE消息中提取)
|
||||
private String currentDeviceSn;
|
||||
|
||||
|
||||
/**
|
||||
* 连接建立时触发(对应Golang中upgrader.Upgrade后的初始化逻辑)
|
||||
*/
|
||||
@OnOpen
|
||||
public void onOpen(Session session) {
|
||||
this.session = session;
|
||||
log.info("新的WebSocket连接建立,会话ID: {}", session.getId());
|
||||
|
||||
try {
|
||||
// 读取设备第一条消息(DECLARE消息),完成设备注册
|
||||
KqjEntity.DeclareMessage declareMsg = registerDevice();
|
||||
this.currentDeviceSn = declareMsg.getSn();
|
||||
log.info("设备注册成功,SN: {}, 设备信息: {}", currentDeviceSn, declareMsg);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("设备注册失败,关闭连接", e);
|
||||
try {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.PROTOCOL_ERROR, "设备注册失败"));
|
||||
} catch (IOException ex) {
|
||||
log.error("关闭异常连接失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 接收客户端消息时触发(对应Golang的for循环读取消息逻辑)
|
||||
*/
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
if (message == null || message.isEmpty()) {
|
||||
log.warn("收到空消息,忽略处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先解析通用消息的CMD字段(对应Golang的GenericMessage)
|
||||
KqjEntity.GenericMessage genericMsg = objectMapper.readValue(message, KqjEntity.GenericMessage.class);
|
||||
String cmd = genericMsg.getCmd();
|
||||
if (cmd == null) {
|
||||
log.warn("收到无CMD字段的消息,忽略: {}", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据CMD类型处理不同逻辑(对应Golang的switch case)
|
||||
switch (cmd) {
|
||||
case Constants.DECLARE:
|
||||
log.info("设备在线心跳(DECLARE),SN: {}", currentDeviceSn);
|
||||
break;
|
||||
|
||||
case Constants.PING:
|
||||
handlePing(message);
|
||||
break;
|
||||
|
||||
case Constants.TO_CLIENT:
|
||||
handleToClientResponse(message);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.warn("收到未知CMD消息,类型: {}, 内容: {}", cmd, message);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理消息失败,消息内容: {}", message, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 连接关闭时触发(对应Golang的defer conn.Close()和资源清理逻辑)
|
||||
*/
|
||||
@OnClose
|
||||
public void onClose(Session session, CloseReason closeReason) {
|
||||
log.info("WebSocket连接关闭,会话ID: {}, 原因: {}", session.getId(), closeReason);
|
||||
|
||||
// 1. 移除设备连接信息
|
||||
if (currentDeviceSn != null) {
|
||||
connectedDevices.remove(currentDeviceSn);
|
||||
// 更新设备状态为离线(对应Golang的service.BusAttendanceMachine().Change)
|
||||
updateDeviceStatus(currentDeviceSn, "0");
|
||||
}
|
||||
|
||||
// 2. 清理当前设备对应的响应通道(避免内存泄漏)
|
||||
responseChannels.entrySet().removeIf(entry -> {
|
||||
if (entry.getValue().getSn().equals(currentDeviceSn)) {
|
||||
entry.getValue().getResultFuture().completeExceptionally(
|
||||
new Exception("设备连接已关闭,响应通道清理")
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
this.session = null;
|
||||
this.currentDeviceSn = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 连接异常时触发
|
||||
*/
|
||||
@OnError
|
||||
public void onError(Session session, Throwable throwable) {
|
||||
log.error("WebSocket连接异常,会话ID: {}", session.getId(), throwable);
|
||||
// 异常时主动关闭连接
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "连接异常"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("关闭异常连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------ 核心业务方法 ------------------------------
|
||||
|
||||
/**
|
||||
* 设备注册(对应Golang的addDevice函数)
|
||||
* 读取DECLARE消息,解析设备信息并存储
|
||||
*/
|
||||
private KqjEntity.DeclareMessage registerDevice() throws Exception {
|
||||
// 阻塞读取第一条消息(DECLARE消息)
|
||||
String firstMessage = readFirstMessage();
|
||||
KqjEntity.DeclareMessage declareMsg = objectMapper.readValue(firstMessage, KqjEntity.DeclareMessage.class);
|
||||
|
||||
// 校验SN合法性
|
||||
String sn = declareMsg.getSn();
|
||||
if (sn == null || sn.isEmpty()) {
|
||||
throw new IllegalArgumentException("设备SN为空,注册失败");
|
||||
}
|
||||
|
||||
// 解析客户端IP和端口(对应Golang的parseRemoteAddr)
|
||||
InetSocketAddress remoteAddr = (InetSocketAddress) session.getUserProperties().get("javax.websocket.endpoint.remoteAddress");
|
||||
String ip = remoteAddr.getAddress().getHostAddress();
|
||||
String port = String.valueOf(remoteAddr.getPort());
|
||||
|
||||
// 存储设备信息
|
||||
KqjEntity.DeviceInfo deviceInfo = new KqjEntity.DeviceInfo();
|
||||
deviceInfo.setIp(ip);
|
||||
deviceInfo.setPort(port);
|
||||
|
||||
// ======================== 添加类型检查 ========================
|
||||
// 检查session是否能转换为WebSocket,避免强转失败
|
||||
if (!(session instanceof WebSocket)) {
|
||||
throw new IllegalStateException("WebSocket容器不支持Session转WebSocket,无法注册设备,SN: " + sn);
|
||||
}
|
||||
// 强转并赋值
|
||||
deviceInfo.setConn((WebSocket) session); // 用Session替换Golang的*websocket.Conn
|
||||
// ============================================================
|
||||
|
||||
|
||||
connectedDevices.put(sn, deviceInfo);
|
||||
|
||||
// 调用业务服务注册设备(对应Golang的service.BusAttendanceMachine().Register)
|
||||
registerDeviceToService(sn);
|
||||
|
||||
return declareMsg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理PING消息(对应Golang的handlePing函数)
|
||||
*/
|
||||
private void handlePing(String message) throws Exception {
|
||||
KqjEntity.DeclareMessage pingMsg = objectMapper.readValue(message, KqjEntity.DeclareMessage.class);
|
||||
String sn = pingMsg.getSn();
|
||||
|
||||
// 1. 回复PONG消息
|
||||
KqjEntity.PongMessage pongMsg = new KqjEntity.PongMessage();
|
||||
pongMsg.setCmd(Constants.PING);
|
||||
pongMsg.setFrom("server");
|
||||
pongMsg.setTo(sn);
|
||||
|
||||
KqjEntity.PongMessageData pongData = new KqjEntity.PongMessageData();
|
||||
pongData.setCmd("pong");
|
||||
pongMsg.setData(pongData);
|
||||
|
||||
// 发送PONG消息
|
||||
session.getBasicRemote().sendText(objectMapper.writeValueAsString(pongMsg));
|
||||
log.info("发送PONG消息给设备,SN: {}", sn);
|
||||
|
||||
// 2. 更新设备连接状态(对应Golang的connectedDevices[sn]重设)
|
||||
if (connectedDevices.containsKey(sn)) {
|
||||
KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn);
|
||||
deviceInfo.setConn((WebSocket) session);
|
||||
connectedDevices.put(sn, deviceInfo);
|
||||
}
|
||||
|
||||
// 3. 调用业务服务更新设备状态(对应Golang的service.BusAttendanceMachine().Register)
|
||||
registerDeviceToService(sn);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理TO_CLIENT响应消息(对应Golang的requestResponse函数)
|
||||
*/
|
||||
private void handleToClientResponse(String message) throws Exception {
|
||||
log.info("收到TO_CLIENT响应消息: {}", message);
|
||||
KqjEntity.CommonResponse commonResp = objectMapper.readValue(message, KqjEntity.CommonResponse.class);
|
||||
|
||||
// 根据UUID查找响应通道,传递响应结果
|
||||
String uuid = commonResp.getTo();
|
||||
ResponseHolder<KqjEntity.CommonResponse> holder = responseChannels.get(uuid);
|
||||
if (holder != null) {
|
||||
holder.getResultFuture().complete(commonResp);
|
||||
responseChannels.remove(uuid); // 移除已完成的通道
|
||||
log.info("响应已分发到UUID: {}, 响应内容: {}", uuid, commonResp);
|
||||
} else {
|
||||
log.warn("未找到UUID: {}对应的响应通道,响应丢弃", uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------ 工具方法 ------------------------------
|
||||
|
||||
/**
|
||||
* 读取设备第一条消息(阻塞直到收到消息)
|
||||
*/
|
||||
private String readFirstMessage() throws Exception {
|
||||
// 使用CompletableFuture等待消息
|
||||
final java.util.concurrent.CompletableFuture<String> firstMsgFuture = new java.util.concurrent.CompletableFuture<>();
|
||||
// 使用AtomicReference包装临时处理器,解决未初始化问题
|
||||
final java.util.concurrent.atomic.AtomicReference<MessageHandler.Whole<String>> tempHandlerRef = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
|
||||
// 定义临时消息处理器
|
||||
MessageHandler.Whole<String> tempHandler = msg -> {
|
||||
if (!firstMsgFuture.isDone()) {
|
||||
firstMsgFuture.complete(msg);
|
||||
// 从引用中获取处理器并移除
|
||||
session.removeMessageHandler(tempHandlerRef.get());
|
||||
}
|
||||
};
|
||||
// 将处理器存入引用
|
||||
tempHandlerRef.set(tempHandler);
|
||||
// 注册处理器
|
||||
session.addMessageHandler(tempHandler);
|
||||
|
||||
// 等待消息,超时10秒
|
||||
return firstMsgFuture.get(10, TimeUnit.SECONDS);
|
||||
// // 使用Java并发工具等待消息(模拟Golang的阻塞读取)
|
||||
// final java.util.concurrent.CompletableFuture<String> firstMsgFuture = new java.util.concurrent.CompletableFuture<>();
|
||||
//
|
||||
// // 临时注册消息处理器,读取第一条消息后移除
|
||||
// MessageHandler.Whole<String> tempHandler = msg -> {
|
||||
// if (!firstMsgFuture.isDone()) {
|
||||
// firstMsgFuture.complete(msg);
|
||||
// // 移除临时处理器(避免重复处理)
|
||||
// session.removeMessageHandler(tempHandler);
|
||||
// }
|
||||
// };
|
||||
// session.addMessageHandler(tempHandler);
|
||||
//
|
||||
// // 等待消息,超时10秒(防止设备一直不发消息)
|
||||
// return firstMsgFuture.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送消息给指定设备(对应Golang的sendMessageToDevice函数)
|
||||
*/
|
||||
public static boolean sendMessageToDevice(String sn, String uuid, Object message) {
|
||||
// 1. 检查设备是否在线
|
||||
KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn);
|
||||
if (deviceInfo == null) {
|
||||
log.warn("设备不存在,SN: {}", sn);
|
||||
responseChannels.remove(uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 将WebSocket转回Session(因为状态由Session管理)
|
||||
WebSocket webSocket = deviceInfo.getConn();
|
||||
if (!(webSocket instanceof Session)) {
|
||||
log.warn("设备连接类型错误,无法判断状态,SN: {}", sn);
|
||||
responseChannels.remove(uuid);
|
||||
return false;
|
||||
}
|
||||
Session session = (Session) webSocket;
|
||||
|
||||
// 3. 检查连接是否打开
|
||||
if (!session.isOpen()) {
|
||||
log.warn("设备连接已关闭,SN: {}", sn);
|
||||
connectedDevices.remove(sn); // 移除已关闭的设备
|
||||
responseChannels.remove(uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 4. 序列化消息并发送
|
||||
String msgJson = objectMapper.writeValueAsString(message);
|
||||
session.getBasicRemote().sendText(msgJson); // 通过Session发送消息
|
||||
log.info("发送消息给设备,SN: {}, UUID: {}, 消息: {}", sn, uuid, msgJson);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送消息失败,SN: {}, UUID: {}", sn, uuid, e);
|
||||
// 发送失败时移除设备(可能连接已异常)
|
||||
connectedDevices.remove(sn);
|
||||
responseChannels.remove(uuid);
|
||||
return false;
|
||||
}
|
||||
// // 1. 检查设备是否在线
|
||||
// KqjEntity.DeviceInfo deviceInfo = connectedDevices.get(sn);
|
||||
// if (deviceInfo == null || !deviceInfo.getSession().isOpen()) {
|
||||
// log.warn("设备不在线,SN: {}", sn);
|
||||
// // 清理无效的响应通道
|
||||
// responseChannels.remove(uuid);
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// // 2. 序列化消息并发送
|
||||
// String msgJson = objectMapper.writeValueAsString(message);
|
||||
// deviceInfo.getSession().getBasicRemote().sendText(msgJson);
|
||||
// log.info("发送消息给设备,SN: {}, UUID: {}, 消息: {}", sn, uuid, msgJson);
|
||||
// return true;
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// log.error("发送消息失败,SN: {}, UUID: {}", sn, uuid, e);
|
||||
// // 清理异常的响应通道
|
||||
// responseChannels.remove(uuid);
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送请求并等待响应(对应Golang的SendRequestAndWaitResponse函数)
|
||||
*/
|
||||
public static KqjEntity.CommonResponse sendRequestAndWaitResponse(String sn, String uuid, Object payload) throws Exception {
|
||||
// 1. 创建响应结果容器
|
||||
ResponseHolder<KqjEntity.CommonResponse> responseHolder = new ResponseHolder<>(sn);
|
||||
responseChannels.put(uuid, responseHolder);
|
||||
|
||||
try {
|
||||
// 2. 发送请求
|
||||
boolean sendSuccess = sendMessageToDevice(sn, uuid, payload);
|
||||
if (!sendSuccess) {
|
||||
throw new Exception("发送请求失败,设备不在线或发送异常,SN: " + sn);
|
||||
}
|
||||
|
||||
// 3. 等待响应,超时10秒
|
||||
return responseHolder.getResultFuture().get(10, TimeUnit.SECONDS);
|
||||
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
log.error("等待响应超时,SN: {}, UUID: {}", sn, uuid);
|
||||
responseChannels.remove(uuid);
|
||||
throw new Exception("等待响应超时(10秒)", e);
|
||||
|
||||
} finally {
|
||||
// 清理响应通道(防止内存泄漏)
|
||||
responseChannels.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成带6位随机数的UUID(对应Golang的GenerateUUIDWithSixRandomDigits函数)
|
||||
*/
|
||||
public static String generateUUIDWithSixRandomDigits() {
|
||||
// 生成标准UUID
|
||||
String uuidStr = UUID.randomUUID().toString().replace("-", "");
|
||||
// 生成6位随机数(100000-999999)
|
||||
Random random = new Random();
|
||||
int randomNum = random.nextInt(900000) + 100000;
|
||||
// 拼接返回
|
||||
return uuidStr + "-" + randomNum;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------ 业务服务调用(模拟Golang的service层) ------------------------------
|
||||
|
||||
/**
|
||||
* 注册设备到业务服务(对应Golang的service.BusAttendanceMachine().Register)
|
||||
* 实际项目中替换为真实的Service调用
|
||||
*/
|
||||
private void registerDeviceToService(String sn) {
|
||||
try {
|
||||
// 模拟业务服务调用(如更新数据库设备状态为在线)
|
||||
log.info("调用业务服务注册设备,SN: {}", sn);
|
||||
// TODO: 替换为真实的Service代码(如Spring Bean调用)
|
||||
} catch (Exception e) {
|
||||
log.error("业务服务注册设备失败,SN: {}", sn, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新设备状态(对应Golang的service.BusAttendanceMachine().Change)
|
||||
* 实际项目中替换为真实的Service调用
|
||||
*/
|
||||
private void updateDeviceStatus(String sn, String status) {
|
||||
try {
|
||||
log.info("调用业务服务更新设备状态,SN: {}, 状态: {}", sn, status);
|
||||
// TODO: 替换为真实的Service代码(如Spring Bean调用)
|
||||
} catch (Exception e) {
|
||||
log.error("业务服务更新设备状态失败,SN: {}", sn, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------ 内部辅助类 ------------------------------
|
||||
|
||||
/**
|
||||
* 响应结果容器(对应Golang的chan CommonResponse)
|
||||
* 用CompletableFuture实现异步结果等待
|
||||
*/
|
||||
private static class ResponseHolder<T> {
|
||||
private final String sn; // 关联的设备SN
|
||||
private final java.util.concurrent.CompletableFuture<T> resultFuture;
|
||||
|
||||
public ResponseHolder(String sn) {
|
||||
this.sn = sn;
|
||||
this.resultFuture = new java.util.concurrent.CompletableFuture<>();
|
||||
}
|
||||
|
||||
public String getSn() {
|
||||
return sn;
|
||||
}
|
||||
|
||||
public java.util.concurrent.CompletableFuture<T> getResultFuture() {
|
||||
return resultFuture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,299 @@
|
||||
package org.dromara.mobileAttendanceMachine;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.net.http.WebSocket;
|
||||
|
||||
/**
|
||||
* @Author 铁憨憨
|
||||
* @Date 2025/10/14 14:47
|
||||
* @Version 1.0
|
||||
*/
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class KqjEntity {
|
||||
|
||||
@Data
|
||||
public static class DeviceInfo {
|
||||
@JsonProperty("ip")
|
||||
private String ip;
|
||||
|
||||
@JsonProperty("port")
|
||||
private String port;
|
||||
|
||||
@JsonProperty("conn")
|
||||
private WebSocket conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用消息结构,用于解析初始的 cmd 字段
|
||||
*/
|
||||
@Data
|
||||
public class GenericMessage {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共响应结构
|
||||
*/
|
||||
@Data
|
||||
public class CommonResponse {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from;
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to;
|
||||
|
||||
@JsonProperty("data")
|
||||
private CommonResponseData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class CommonResponseData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("userIds")
|
||||
private String[] userIds; // 用户IDS
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("code")
|
||||
private int code;
|
||||
|
||||
@JsonProperty("msg")
|
||||
private String msg;
|
||||
|
||||
@JsonProperty("delFailed")
|
||||
private DelMultiUserData[] delFailed;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class DelMultiUserData {
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("code")
|
||||
private int code;
|
||||
|
||||
@JsonProperty("msg")
|
||||
private String msg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设备上线消息
|
||||
*/
|
||||
@Data
|
||||
public class DeclareMessage {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("sn")
|
||||
private String sn;
|
||||
|
||||
@JsonProperty("version_code")
|
||||
private String versionCode;
|
||||
|
||||
@JsonProperty("version_name")
|
||||
private String versionName;
|
||||
|
||||
@JsonProperty("token")
|
||||
private String token;
|
||||
|
||||
@JsonProperty("ip")
|
||||
private String ip;
|
||||
|
||||
@JsonProperty("timestamp")
|
||||
private double timestamp;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PongMessage {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from;
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to;
|
||||
|
||||
@JsonProperty("data")
|
||||
private PongMessageData data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 心跳回复消息结构
|
||||
*/
|
||||
@Data
|
||||
public static class PongMessageData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 人员信息下发
|
||||
*/
|
||||
@Data
|
||||
public class PeopleInformation {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd; // 该接口固定为to_device
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from; // 可不填写,填写uuid来做为发送请求或响应的标识
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to; // 设备号(请查看公共设置中的设备号)
|
||||
|
||||
@JsonProperty("data")
|
||||
private PeopleInData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class PeopleInData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("face_template")
|
||||
private String faceTemplate; // http 链接图
|
||||
|
||||
@JsonProperty("id_valid")
|
||||
private String idValid; // 人员有效期(人员在这个时间点后,无法通行)格式:yyyy-MM-dd 或者 yyyy-MM-dd HH:mm,为 "" 则为永久
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人员
|
||||
*/
|
||||
@Data
|
||||
public class DeletionOfPersonnel {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from;
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to;
|
||||
|
||||
@JsonProperty("data")
|
||||
private DeletionOfPersonnelData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class DeletionOfPersonnelData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("user_type")
|
||||
private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
@Data
|
||||
public class BatchDeletion {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from;
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to;
|
||||
|
||||
@JsonProperty("data")
|
||||
private BatchDeletionData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class BatchDeletionData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("user_ids")
|
||||
private String[] userIds;
|
||||
|
||||
@JsonProperty("user_type")
|
||||
private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部人员
|
||||
*/
|
||||
@Data
|
||||
public class DeletionALlOfPersonnel {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from;
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to;
|
||||
|
||||
@JsonProperty("data")
|
||||
private DeletionALlOfPersonnelData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class DeletionALlOfPersonnelData {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("user_type")
|
||||
private int userType; // 删除的用户类型:0-人脸接口下发的数据 1-人证比对接口下发的数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 人员信息获取
|
||||
*/
|
||||
@Data
|
||||
public class PersonnelInformationAcquisition {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd; // 该接口固定为to_device
|
||||
|
||||
@JsonProperty("from")
|
||||
private String from; // 可不填写,填写uuid来做为发送请求或响应的标识
|
||||
|
||||
@JsonProperty("to")
|
||||
private String to; // 设备号(请查看公共设置中的设备号)
|
||||
|
||||
@JsonProperty("data")
|
||||
private PersonnelInformationAcquisitionTwo data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class PersonnelInformationAcquisitionTwo {
|
||||
@JsonProperty("cmd")
|
||||
private String cmd;
|
||||
|
||||
@JsonProperty("value")
|
||||
private int value;
|
||||
}
|
||||
|
||||
}
|
||||
@ -383,6 +383,7 @@ public class SysUserController extends BaseController {
|
||||
public TableDataInfo<SysUserVo> fbList(FbUserListDto dto, PageQuery pageQuery) {
|
||||
return userService.selectPageFbUserList(dto, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分包单位列表
|
||||
*/
|
||||
|
||||
@ -3,6 +3,7 @@ package org.dromara.transferData.controller;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.github.xiaoymin.knife4j.annotations.Ignore;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.contractor.domain.SubConstructionUser;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.dromara.transferData.service;
|
||||
|
||||
import cn.dev33.satoken.secure.BCrypt;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@ -566,6 +567,7 @@ public class TransferDataService {
|
||||
sysUser.setNickName(user.getUserName());
|
||||
sysUser.setPhonenumber(phone);
|
||||
sysUser.setSex(user.getSex());
|
||||
sysUser.setPassword(BCrypt.hashpw("123456"));
|
||||
sysUser.setUserType(UserType.APP_USER.getUserType());
|
||||
sysUserId = userService.save(sysUser);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user