Merge branch 'main' into frx

This commit is contained in:
furongxin 2024-05-29 18:34:31 +08:00
commit e48e3ba95c
14 changed files with 416 additions and 16 deletions

View File

@ -2,11 +2,16 @@ package cn.iocoder.yudao.framework.websocket.core.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.framework.websocket.core.attendance.AttendanceConstants;
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
import com.github.yulichang.toolkit.SpringContentUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
@ -43,28 +48,79 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 1.1 空消息跳过
if (message.getPayloadLength() == 0) {
return;
}
// 1.2 ping 心跳消息直接返回 pong 消息
if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
session.sendMessage(new TextMessage("pong"));
return;
String payload = message.getPayload() ;
boolean isTypeJSON = JSONUtil.isTypeJSON(payload) ;
if( isTypeJSON ) {
JSONObject object = new JSONObject(payload);
String cmd = object.getStr("cmd") ; //设备请求方法
String sn = object.getStr("sn") ; //设备编号
// 此指令服务端不需要回复 客户端声明
if ( cmd !=null && cmd.equals("declare") ) {
log.info("[客户端声明]"+ "||"+ session.getId()+ "||"+ message.getPayload(), session.getId(), message.getPayload());
//此处需要将sessionId 与sn 绑定
WebSocketSessionManager webSocketSessionManager = SpringContentUtils.getBean(WebSocketSessionManager.class);
webSocketSessionManager.addSession(session,sn);
return;
}
// 1.2 ping 心跳消息直接返回 pong 消息
if ( cmd != null && cmd.equals("ping") ) {
log.info("[设备心跳]"+ "||"+ session.getId()+ "||"+ message.getPayload(), session.getId(), message.getPayload());
session.sendMessage(new TextMessage("{\"cmd\": \"pong\"}"));
//TODO 更新设备心跳请求时间
// 传入设备sn编码 调用相关方法更新对应sn的本次请求时间
// 并且将设备的状态更新成在线状态
return;
}
} else {
// 1.2 ping 心跳消息直接返回 pong 消息
if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
// log.info("[WEB心跳]"+ "||"+ session.getId()+ "||"+ message.getPayload(), session.getId(), message.getPayload());
session.sendMessage(new TextMessage("pong"));
return;
}
}
// 2.1 解析消息
try {
// 2.1 解析消息
JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
if (jsonMessage == null) {
if (message.getPayload() == null) {
log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
return;
}
if (StrUtil.isEmpty(jsonMessage.getType())) {
log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
JSONObject object = new JSONObject(message.getPayload());
String cmd = object.getStr("cmd") ;
if (StrUtil.isEmpty( cmd )) {
log.error("[handleTextMessage][session({}) message({}) 数据格错误]", session.getId(), message.getPayload());
return;
}
// 2.2 获得对应的 WebSocketMessageListener
if( cmd.equals(AttendanceConstants.CMD_TO_DEVICE) ) {
jsonMessage.setType("attendance-message-send") ;
//服务器下发数据到设备
//TODO 记录服务器下数据
}else if( cmd.equals(AttendanceConstants.CMD_TO_CLIENT) ) {
jsonMessage.setType("attendance-message-send") ;
//设备的响应数据返回服务器
//{"cmd":"to_client","form":"QT74824","from":"QT74824","to":"system","data":{"cmd":"addUserRet","code":0,"msg":"下发成功","user_id":"999999"},"extra":"null"}
String data = object.getStr("data") ;
JSONObject dataObject = new JSONObject(data);
String msg= dataObject.getStr("msg") ;
// TODO 记录响应
}
// 2.2 获得对应的 WebSocketMessageListener 根据Type找到对应的处理监听器
WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
if (messageListener == null) {
log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
@ -72,7 +128,11 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
}
// 2.3 处理消息
Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
JSONObject object1 = new JSONObject(message.getPayload());
Object data = object1.get("data");
object1.set("data",data.toString()) ;
String string = object1.toString();
Object messageObj = JsonUtils.parseObject(string, type);
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
} catch (Throwable ex) {

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.websocket.core.sender;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
@ -41,6 +43,36 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
send(sessionId, null, null, messageType, messageContent);
}
@Override
public void snSend(String sn, String messageType, String messageContent) {
SNSend(sn, messageType, messageContent);
}
/**
* 发送消息
*
* @param sn 设备编码 编号
* @param messageType 消息类型
* @param messageContent 消息内容
*/
public void SNSend(String sn, String messageType, String messageContent) {
// 1. 获得 Session 列表
List<WebSocketSession> sessions = Collections.emptyList();
System.out.println("===="+ sn) ;
if (StrUtil.isNotEmpty(sn)) {
WebSocketSession session = sessionManager.getSessionByDeviceNum(sn);
if (session != null) {
sessions = Collections.singletonList(session);
}
}
if (CollUtil.isEmpty(sessions)) {
log.info("[send][sn({}) messageType({}) messageContent({}) 未匹配到sn设备]",
sn, messageType, messageContent);
}
// 2. 执行发送
doSend(sessions, messageType, messageContent);
}
/**
* 发送消息
*
@ -80,7 +112,10 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
*/
public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
String payload = JsonUtils.toJsonString(message); // 关键使用 JSON 序列化
// JSONObject object = JSONUtil.parseObj(message.getContent()) ;
//消息内容中获取text 属性下的内容用户发送给指定的设备
String payload = messageContent;
sessions.forEach(session -> {
// 1. 各种校验保证 Session 可以被发送
if (session == null) {

View File

@ -37,6 +37,15 @@ public interface WebSocketMessageSender {
*/
void send(String sessionId, String messageType, String messageContent);
/**
* 发送消息给指定 Session
*
* @param sn 设备标号 编号
* @param messageType 消息类型
* @param messageContent 消息内容JSON 格式
*/
void snSend(String sn, String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
@ -49,4 +58,8 @@ public interface WebSocketMessageSender {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendSNObject(String sn, String messageType, Object messageContent) {
snSend(sn, messageType, JsonUtils.toJsonString(messageContent));
}
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.websocket.core.session;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
import java.util.Set;
/**
* {@link WebSocketSession} 管理器的接口
@ -50,4 +51,32 @@ public interface WebSocketSessionManager {
*/
Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
/**
* 添加 Session
*
* @param session Session
* @param sn 设备号
*/
void addSession(WebSocketSession session, String sn);
/**
* 获得指定设备编号的 Session
*
* @param sn 设备号
* @return Session session
*/
WebSocketSession getSessionByDeviceNum(String sn);
/**
* 获取设备SN的集合
* @return
*/
Collection<WebSocketSession> getSNSession();
/**
* 获取所有在线设备的SN
* @return
*/
Set<String> getOnlineSN() ;
}

View File

@ -6,10 +6,7 @@ import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
@ -21,6 +18,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
/**
* sn WebSocketSession 映射
*
* key设备编号
*/
private final ConcurrentMap<String, WebSocketSession> snSessions = new ConcurrentHashMap<>();
/**
* id WebSocketSession 映射
*
@ -39,6 +43,7 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
@Override
public void addSession(WebSocketSession session) {
System.out.println("add###############"+ session.getId());
// 添加到 idSessions
idSessions.put(session.getId(), session);
// 添加到 userSessions
@ -65,8 +70,10 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
@Override
public void removeSession(WebSocketSession session) {
System.out.println("remove###############"+ session.getId());
// 移除从 idSessions
idSessions.remove(session.getId());
snSessions.remove(session.getId());
// 移除从 idSessions
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
if (user == null) {
@ -122,4 +129,29 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
}
@Override
public void addSession(WebSocketSession session, String sn){
System.out.println("sn===="+ sn);
//添加之前先删除历史sn对应的snsession
snSessions.remove(sn) ;
// 添加到 idSessions
snSessions.put(sn, session);
}
@Override
public WebSocketSession getSessionByDeviceNum(String sn) {
return snSessions.get(sn);
}
@Override
public Collection<WebSocketSession> getSNSession(){
return this.snSessions.values();
}
@Override
public Set<String> getOnlineSN() {
Set<String> snSet = this.snSessions.keySet();
return snSet ;
}
}

View File

@ -71,4 +71,16 @@ public interface WebSocketSenderApi {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
/**
* 发送消息给指定 SN
*
* @param sn 设备 编号
* @param messageType 消息类型
* @param messageContent 消息内容JSON 格式
*/
default void sendSN(String sn, String messageType, String messageContent) {
send(new WebSocketSendReqDTO().setSn(sn)
.setMessageType(messageType).setMessageContent(messageContent)).checkError();
}
}

View File

@ -9,6 +9,9 @@ import javax.validation.constraints.NotEmpty;
@Data
public class WebSocketSendReqDTO {
@Schema(description = "sn 编号", example = "QT74824")
private String sn;
@Schema(description = "Session 编号", example = "abc")
private String sessionId;
@Schema(description = "用户编号", example = "1024")

View File

@ -20,7 +20,10 @@ public class WebSocketSenderApiImpl implements WebSocketSenderApi {
@Override
public CommonResult<Boolean> send(WebSocketSendReqDTO message) {
if (StrUtil.isNotEmpty(message.getSessionId())) {
if (StrUtil.isNotEmpty(message.getSn())) {
webSocketMessageSender.snSend(message.getSn(),
message.getMessageType(), message.getMessageContent());
}else if (StrUtil.isNotEmpty(message.getSessionId())) {
webSocketMessageSender.send(message.getSessionId(),
message.getMessageType(), message.getMessageContent());
} else if (message.getUserType() != null && message.getUserId() != null) {

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.infra.websocket;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
import cn.iocoder.yudao.module.infra.websocket.message.AttendanceReceiveMessage;
import cn.iocoder.yudao.module.infra.websocket.message.AttendanceSendMessage;
import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage;
import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.Resource;
/**
* 考勤仪设备 WebSocket 数据通信
*
*/
@Component
public class AttendanceWebSocketMessageListener implements WebSocketMessageListener<AttendanceSendMessage> {
@Resource
private WebSocketMessageSender webSocketMessageSender;
@Override
public void onMessage(WebSocketSession session, AttendanceSendMessage message) {
String sn = session.getId() ;
AttendanceReceiveMessage toMessage = new AttendanceReceiveMessage().setFrom("system")
.setData(message.getData());
webSocketMessageSender.sendSNObject(sn, "attendance-message-send", toMessage);
}
@Override
public String getType() {
return "attendance-message-send";
}
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.infra.websocket.message;
import lombok.Data;
/**
* 示例server -> client 同步消息
*
*/
@Data
public class AttendanceReceiveMessage {
/**
* 接收人的编号
*/
private String from;
/**
* 内容
*/
private String data;
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.infra.websocket.message;
import lombok.Data;
/**
* 示例client -> server 发送消息
*
*/
@Data
public class AttendanceSendMessage {
/**
* 发送给哪个设备
*/
private String to;
/**
* 内容
*/
private String data;
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.system.attendance;
import cn.hutool.json.JSONObject;
import lombok.Data;
/**
* 功能描述
*
* @author: yj
* @date: 2024年05月27日 10:48
*/
@Data
public class AttendanceBaseMessage {
/**
* 固定为 to_device
*/
public final String cmd = AttendanceConstants.CMD_BASE;
/**
* 这个字段对设备来说没有任何作用只是在响应指令时在 to 字段中附带回服务端
* 可以认为是 message id 的概念一般是让服务端区分设备响应的是具体的哪一条消息服务端没有用到的话可以为空字符
*/
public String from ;
/**
* 字段非必填这个字段和 from 一样对设备来说没有任何作用
* 只是在响应指令时原封不动的把 extra 的内容响应回去服务端没有用到可以不传
*/
public String extra ;
/**
* 目标的设备编号
*/
public String to ;
/**
* 具体指令中的数据格式每个指令都不一样
*/
public JSONObject data;
// public void setData(Object o) {
// this.data = JsonUtils.toJsonString(o);
// }
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.system.attendance;
/**
* 功能描述
*
* @author: yj
* @date: 2024年05月27日 11:40
*/
public class AttendanceConstants {
public static final String CMD_BASE = "to_device" ;
public static class OperateUserCMD {
/**
* 下发人员信息到设备
*/
public static final String ADD_USER_CMD = "addUser" ;
/**
* 修改人员信息
*/
public static final String EDIT_USER_CMD = "editUser" ;
/**
* 批量删除人员
*/
public static final String DEL_MULTI_USER_CMD = "delMultiUser" ;
/**
* 检测图片质量
*/
public static final String CHECK_USER_PHOTO_CMD = "verifyPhoto" ;
}
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.system.attendance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 功能描述
*
* @author: yj
* @date: 2024年05月27日 11:02
*/
@Schema(description="下发人员信息到设备")
@Data
public class SendUserToDeviceMessage {
@Schema(description = "指令名称", requiredMode = Schema.RequiredMode.REQUIRED ,example = "addUser")
@NotNull(message = "指定名称不能为空")
public String cmd = AttendanceConstants.OperateUserCMD.ADD_USER_CMD;
@Schema(description = "用户id注意 user_id 请不要使用 DL 开头", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "user_id不能为空")
public String user_id ;
@Schema(description = "用户姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
@NotNull(message = "用户姓名不能为空")
public String name;
// @Schema(description = "身份证号码,在刷身份证时会匹配这个号码是否存在", example = "362301111111111111")
// public String user_id_card ;
@Schema(description = "彩色照片,可以为两种格式。\n" +
"1.http 链接,例如 https://up.enterdesk.com/edpic/70/0e/33/700e3312f74e378fbcc2fb3819421e73.jpg\n" +
"2.直接传图片 点击查看【2.服务规范】中的照片编码规则 。设备【验证模式】为【人脸或卡】时可以不传照片,非【人脸或卡】模式这个字段为必传", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://xxx.xx.jpg")
@NotNull(message = "照片不能为空")
public String face_template ;
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
@NotNull(message = "手机号不能为空")
public String phone;
@Schema(description = "人员有效期人员在这个时间点后无法通行格式yyyy-MM-dd 或者 yyyy-MM-dd HH:mm为 “” 则为永久", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
public final String id_valid= "";
}