diff --git a/pom.xml b/pom.xml
index a4a59a2e..48b22f29 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,7 @@
2.3.31
8.0.26
1.5.22
+ 2.2.5
diff --git a/virtual-patient-common/pom.xml b/virtual-patient-common/pom.xml
index 39517bdf..4eb50d36 100644
--- a/virtual-patient-common/pom.xml
+++ b/virtual-patient-common/pom.xml
@@ -38,6 +38,12 @@
redis.clients
jedis
+
+
+ com.baomidou
+ lock4j-redis-template-spring-boot-starter
+ ${lock4j.version}
+
diff --git a/virtual-patient-common/src/main/java/com/supervision/config/JwtInterceptor.java b/virtual-patient-common/src/main/java/com/supervision/config/JwtInterceptor.java
index 8513504d..6dfb5735 100644
--- a/virtual-patient-common/src/main/java/com/supervision/config/JwtInterceptor.java
+++ b/virtual-patient-common/src/main/java/com/supervision/config/JwtInterceptor.java
@@ -58,27 +58,6 @@ public class JwtInterceptor implements HandlerInterceptor {
return true;
}
- public void checkSingleLogin(String userId, JWT currentJwt) {
-
- if (Boolean.FALSE.equals(redisTemplate.hasKey(UserTokenConstant.TOKEN_CACHE + userId))) {
- throw new BusinessException("用户已被踢下线或超时,请重新登录", 505);
- }
- String value = redisTemplate.opsForValue().get(UserTokenConstant.TOKEN_CACHE + userId);
-
- long redisCacheTime = Long.parseLong(String.valueOf(value));
- Object currentJwtIssueTimeObject = currentJwt.getPayload("issueTime");
- long currentJwtIssueTime = Long.parseLong(String.valueOf(currentJwtIssueTimeObject));
- if (redisCacheTime == currentJwtIssueTime) {
- // 如果相等,说明这个token就是最新的,直接放行
- return;
- } else if (currentJwtIssueTime > redisCacheTime) {
- // 如果当前请求时间,大于Redis缓存时间,说明重新登录了,这个时候要把最新的放到缓存中
- redisTemplate.opsForValue().set(UserTokenConstant.TOKEN_CACHE + userId, String.valueOf(System.currentTimeMillis()), 1000 * 5L, TimeUnit.MILLISECONDS);
- } else {
- // 走到这里,说明redisCacheTime是最新的,说明当前用户请求了一个新的token,那么原来的用户就踢掉
- throw new BusinessException("当前用户已在其他地方登录!", 505);
- }
- }
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
diff --git a/virtual-patient-common/src/main/java/com/supervision/constant/UserTokenConstant.java b/virtual-patient-common/src/main/java/com/supervision/constant/UserTokenConstant.java
index 29466321..a45b5bd2 100644
--- a/virtual-patient-common/src/main/java/com/supervision/constant/UserTokenConstant.java
+++ b/virtual-patient-common/src/main/java/com/supervision/constant/UserTokenConstant.java
@@ -1,10 +1,8 @@
package com.supervision.constant;
public interface UserTokenConstant {
-
- String TOKEN_CACHE = "USER:LOGIN:TOKEN:";
-
- String USER_ID_CACHE = "USER:ID:CACHE";
+ String USER_WEBSOCKET_CACHE = "USER:ID:CACHE";
String KICK_CHANNEL = "USER:KICK:CHANNEL";
+
}
diff --git a/virtual-patient-web/pom.xml b/virtual-patient-web/pom.xml
index 34155a28..4830043f 100644
--- a/virtual-patient-web/pom.xml
+++ b/virtual-patient-web/pom.xml
@@ -58,6 +58,7 @@
spring-boot-starter-websocket
+
diff --git a/virtual-patient-web/src/main/java/com/supervision/controller/UserController.java b/virtual-patient-web/src/main/java/com/supervision/controller/UserController.java
index 2aebb0a0..36d6ec88 100644
--- a/virtual-patient-web/src/main/java/com/supervision/controller/UserController.java
+++ b/virtual-patient-web/src/main/java/com/supervision/controller/UserController.java
@@ -50,14 +50,6 @@ public class UserController {
return loginResVO;
}
- @ApiOperation("token心跳")
- @PostMapping("keepaliveToken")
- public void keepaliveToken() {
- User user = UserUtil.getUser();
- // 每次心跳都设置为5分钟之后
- redisTemplate.expire(UserTokenConstant.TOKEN_CACHE + user.getId(), 1000 * 5L, TimeUnit.MILLISECONDS);
- }
-
@ApiOperation("踢用户下线")
@GetMapping("kickUser")
public void kickUser(String userId) {
diff --git a/virtual-patient-web/src/main/java/com/supervision/controller/WebSocketServer.java b/virtual-patient-web/src/main/java/com/supervision/controller/WebSocketServer.java
index 1a122a26..0aa39942 100644
--- a/virtual-patient-web/src/main/java/com/supervision/controller/WebSocketServer.java
+++ b/virtual-patient-web/src/main/java/com/supervision/controller/WebSocketServer.java
@@ -1,10 +1,12 @@
package com.supervision.controller;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
import com.supervision.constant.UserTokenConstant;
-import lombok.RequiredArgsConstructor;
+import com.supervision.usermanage.UserResourceCheck;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@@ -25,14 +27,19 @@ public class WebSocketServer {
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static final ConcurrentHashMap SESSION_POOL = new ConcurrentHashMap<>();
+ @Value("${human.resourceMaxNumber}")
+ private String resourceNumber;
+
+ @Autowired
+ private UserResourceCheck userResourceCheck;
+
/**
* 有客户端连接成功
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "uid") String uid) {
- log.info("用户:{}登录,缓存到Redis", uid);
+ userResourceCheck.achieveDiagnoseResourceAndOpenConnection(uid, session);
SESSION_POOL.put(uid, session);
- redisTemplate.opsForSet().add(UserTokenConstant.USER_ID_CACHE, uid);
}
/**
@@ -40,9 +47,9 @@ public class WebSocketServer {
*/
@OnClose
public void onClose(Session session, @PathParam(value = "uid") String uid) {
- redisTemplate.opsForSet().remove(UserTokenConstant.USER_ID_CACHE, uid);
+ redisTemplate.opsForHash().delete(UserTokenConstant.USER_WEBSOCKET_CACHE, uid, session.getId());
SESSION_POOL.remove(uid);
- log.info("用户:{}关闭,从Redis中移除,当前连接数为:{}", uid, redisTemplate.opsForSet().size(UserTokenConstant.USER_ID_CACHE));
+ log.info("用户:{}关闭,从Redis中移除,当前连接数为:{}", uid, redisTemplate.opsForHash().size(UserTokenConstant.USER_WEBSOCKET_CACHE));
}
/**
@@ -50,18 +57,20 @@ public class WebSocketServer {
*/
@OnError
public void onError(Session session, @PathParam(value = "uid") String uid, Throwable throwable) {
- redisTemplate.opsForSet().remove(UserTokenConstant.USER_ID_CACHE, uid);
+ redisTemplate.opsForHash().delete(UserTokenConstant.USER_WEBSOCKET_CACHE, uid, session.getId());
SESSION_POOL.remove(uid);
- log.error("用户:{}发生错误,从Redis中移除,当前连接数为:{}", uid, redisTemplate.opsForSet().size(UserTokenConstant.USER_ID_CACHE), throwable);
+ log.error("用户:{}发生错误,从Redis中移除,当前连接数为:{}", uid, redisTemplate.opsForHash().size(UserTokenConstant.USER_WEBSOCKET_CACHE), throwable);
}
// 实现一个方法用于踢下线用户,走的是Redis的消息队列
- public void kickUser(String userId) throws IOException {
+ public void kickUser(String userId, String ignoreSessionId) throws IOException {
log.info("尝试主动踢用户:{}下线", userId);
Session session = SESSION_POOL.get(userId);
- if (ObjectUtil.isNotEmpty(session)) {
+ // 只有不是忽略剔除的sessionId才可以踢下线
+ if (ObjectUtil.isNotEmpty(session) && !StrUtil.equals(ignoreSessionId, session.getId())) {
session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "用户被踢下线"));
- log.info("主动踢用户:{}下线成功", userId);
+ SESSION_POOL.remove(userId);
+ log.info("主动踢用户:{},sessionId:{} 下线成功", userId, session.getId());
return;
}
log.info("主动踢用户:{}下线,未找到用户,踢下线失败", userId);
diff --git a/virtual-patient-web/src/main/java/com/supervision/pojo/vo/AskPhysicalResultReqVO.java b/virtual-patient-web/src/main/java/com/supervision/pojo/vo/AskPhysicalResultReqVO.java
index d30311b1..8174b7b7 100644
--- a/virtual-patient-web/src/main/java/com/supervision/pojo/vo/AskPhysicalResultReqVO.java
+++ b/virtual-patient-web/src/main/java/com/supervision/pojo/vo/AskPhysicalResultReqVO.java
@@ -18,7 +18,6 @@ public class AskPhysicalResultReqVO {
private String locationCode;
@ApiModelProperty("初步诊断ID")
- @NotBlank(message = "初步诊断ID不能为空")
private String primaryId;
@NotBlank(message = "流程ID不能为空")
diff --git a/virtual-patient-web/src/main/java/com/supervision/service/impl/DiagnoseHallServiceImpl.java b/virtual-patient-web/src/main/java/com/supervision/service/impl/DiagnoseHallServiceImpl.java
index 0996558b..9bbcf208 100644
--- a/virtual-patient-web/src/main/java/com/supervision/service/impl/DiagnoseHallServiceImpl.java
+++ b/virtual-patient-web/src/main/java/com/supervision/service/impl/DiagnoseHallServiceImpl.java
@@ -1,8 +1,9 @@
package com.supervision.service.impl;
-import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.lock.annotation.Lock4j;
import com.supervision.constant.UserTokenConstant;
import com.supervision.service.DiagnoseHallService;
+import com.supervision.usermanage.UserResourceCheck;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -14,20 +15,13 @@ import org.springframework.stereotype.Service;
@Slf4j
public class DiagnoseHallServiceImpl implements DiagnoseHallService {
- private final RedisTemplate redisTemplate;
-
- @Value("${human.resourceMaxNumber}")
- private String resourceNumber;
+ private final UserResourceCheck userResourceCheck;
+ @Lock4j(name = "achieveDiagnoseResource")
@Override
public boolean achieveDiagnoseResource() {
- long humanMaxNumber = Long.parseLong(resourceNumber);
- Long currentUserNum = redisTemplate.opsForSet().size(UserTokenConstant.USER_ID_CACHE);
// 如果小于数字人最大连接数,则可以连接
- if (null == currentUserNum){
- return true;
- }
- return currentUserNum <= humanMaxNumber;
+ return userResourceCheck.achieveDiagnoseResource();
}
}
diff --git a/virtual-patient-web/src/main/java/com/supervision/usermanage/KickUserListener.java b/virtual-patient-web/src/main/java/com/supervision/usermanage/KickUserListener.java
index b2fc923e..c1311808 100644
--- a/virtual-patient-web/src/main/java/com/supervision/usermanage/KickUserListener.java
+++ b/virtual-patient-web/src/main/java/com/supervision/usermanage/KickUserListener.java
@@ -1,5 +1,6 @@
package com.supervision.usermanage;
+import cn.hutool.json.JSONUtil;
import com.supervision.constant.UserTokenConstant;
import com.supervision.controller.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
@@ -20,10 +21,11 @@ public class KickUserListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
- String userId = message.toString();
- log.info("Redis的Channel:{}收到踢用户下线消息:{}", UserTokenConstant.KICK_CHANNEL, userId);
+ String messageString = message.toString();
+ UserWebSocketDTO user = JSONUtil.toBean(messageString, UserWebSocketDTO.class);
+ log.info("Redis的Channel:{}收到踢用户{}下线消息", UserTokenConstant.KICK_CHANNEL, user.getUserId());
try {
- webSocketServer.kickUser(userId);
+ webSocketServer.kickUser(user.getUserId(), user.getIgnoreSessionId());
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/virtual-patient-web/src/main/java/com/supervision/usermanage/UserResourceCheck.java b/virtual-patient-web/src/main/java/com/supervision/usermanage/UserResourceCheck.java
new file mode 100644
index 00000000..4d18a7ea
--- /dev/null
+++ b/virtual-patient-web/src/main/java/com/supervision/usermanage/UserResourceCheck.java
@@ -0,0 +1,45 @@
+package com.supervision.usermanage;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.lock.annotation.Lock4j;
+import com.supervision.constant.UserTokenConstant;
+import com.supervision.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+
+import javax.websocket.*;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class UserResourceCheck {
+
+ @Value("${human.resourceMaxNumber}")
+ private String resourceNumber;
+
+ private final RedisTemplate redisTemplate;
+
+ @Lock4j(name = "achieveDiagnoseResource")
+ public boolean achieveDiagnoseResource() {
+ long humanMaxNumber = Long.parseLong(resourceNumber);
+ long currentSize = redisTemplate.opsForHash().size(UserTokenConstant.USER_WEBSOCKET_CACHE);
+ // 如果小于数字人最大连接数,则可以连接
+ return currentSize < humanMaxNumber;
+ }
+
+ @Lock4j(name = "achieveDiagnoseResourceAndOpenConnection")
+ public void achieveDiagnoseResourceAndOpenConnection(String uid, Session session){
+ // 如果小于数字人最大连接数,则可以连接
+ if (!achieveDiagnoseResource()) {
+ throw new BusinessException("暂时没有资源,建立连接失败");
+ }
+ log.info("用户:{}登录,缓存到Redis", uid);
+ // 链接之前先把之前的用户踢下线(ignoreSessionId防止把当前用户踢下线)
+ // 注意,这里如果用户没有进到问诊页面,只是在问诊大厅时,是不会被踢掉的.(因为这时没有建立websocket连接)
+ redisTemplate.convertAndSend(UserTokenConstant.KICK_CHANNEL, JSONUtil.toJsonStr(new UserWebSocketDTO(uid, session.getId())));
+ redisTemplate.opsForHash().put(UserTokenConstant.USER_WEBSOCKET_CACHE, uid, session.getId());
+ }
+}
diff --git a/virtual-patient-web/src/main/java/com/supervision/usermanage/UserWebSocketDTO.java b/virtual-patient-web/src/main/java/com/supervision/usermanage/UserWebSocketDTO.java
new file mode 100644
index 00000000..063401f1
--- /dev/null
+++ b/virtual-patient-web/src/main/java/com/supervision/usermanage/UserWebSocketDTO.java
@@ -0,0 +1,17 @@
+package com.supervision.usermanage;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class UserWebSocketDTO {
+
+ private String userId;
+
+ /**
+ * 忽略提除的ID
+ */
+ private String ignoreSessionId;
+
+}