From 87e58ca4aff0b04e5e072bf12760f7bfd0ca3586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=88=90?= <2847920761@qq.com> Date: Tue, 14 Oct 2025 16:02:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A7=BB=E5=8A=A8=E8=80=83?= =?UTF-8?q?=E5=8B=A4=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi-modules/ruoyi-system/pom.xml | 8 + .../vo/BusBillofquantitiesVersionsVo.java | 2 +- .../DeviceMessageSender.java | 163 ++++++ .../DeviceWebSocketServer.java | 472 ++++++++++++++++++ .../mobileAttendanceMachine/KqjEntity.java | 299 +++++++++++ .../controller/system/SysUserController.java | 1 + .../controller/TransferDataController.java | 1 + .../service/TransferDataService.java | 2 + 8 files changed, 947 insertions(+), 1 deletion(-) create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java create mode 100644 xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml index 5f707192..ce758e5f 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml @@ -18,6 +18,14 @@ + + + javax.websocket + javax.websocket-api + 1.1 + provided + + diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java index f9221e09..745914fd 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/design/domain/vo/BusBillofquantitiesVersionsVo.java @@ -17,7 +17,7 @@ import java.util.Date; /** - * 工程量清单版本视图对象 bus_billofquantities_versions + * 工程量清单版本视图对象 bus_billofquantities_versions * * @author Lion Li * @date 2025-08-11 diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java new file mode 100644 index 00000000..a96ecf02 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceMessageSender.java @@ -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); + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java new file mode 100644 index 00000000..fb2c00ea --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/DeviceWebSocketServer.java @@ -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 connectedDevices = new ConcurrentHashMap<>(); + // 2. 存储UUID对应的响应通道(key: UUID,value: 响应结果容器) + private static final Map> 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 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 firstMsgFuture = new java.util.concurrent.CompletableFuture<>(); + // 使用AtomicReference包装临时处理器,解决未初始化问题 + final java.util.concurrent.atomic.AtomicReference> tempHandlerRef = new java.util.concurrent.atomic.AtomicReference<>(); + + // 定义临时消息处理器 + MessageHandler.Whole 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 firstMsgFuture = new java.util.concurrent.CompletableFuture<>(); +// +// // 临时注册消息处理器,读取第一条消息后移除 +// MessageHandler.Whole 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 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 { + private final String sn; // 关联的设备SN + private final java.util.concurrent.CompletableFuture 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 getResultFuture() { + return resultFuture; + } + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java new file mode 100644 index 00000000..eaa3bf6d --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/mobileAttendanceMachine/KqjEntity.java @@ -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; + } + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java index 61ee644f..db8cd249 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java @@ -383,6 +383,7 @@ public class SysUserController extends BaseController { public TableDataInfo fbList(FbUserListDto dto, PageQuery pageQuery) { return userService.selectPageFbUserList(dto, pageQuery); } + /** * 获取分包单位列表 */ diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java index 81fa4dc9..9983c6be 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/controller/TransferDataController.java @@ -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; diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java index 9a4155d7..e3c6581e 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/transferData/service/TransferDataService.java @@ -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 {