Merge remote-tracking branch 'origin/dev_2.0.0' into dev_2.0.0

# Conflicts:
#	virtual-patient-web/src/main/java/com/supervision/controller/UserController.java
dev_2.0.0
xueqingkun 1 year ago
commit 158196e119

@ -43,6 +43,7 @@
<freemarker.version>2.3.31</freemarker.version>
<mysql-connector-java.version>8.0.26</mysql-connector-java.version>
<io-swagger.version>1.5.22</io-swagger.version>
<lock4j.version>2.2.5</lock4j.version>
</properties>
<dependencyManagement>

@ -38,6 +38,12 @@
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--redis分布式锁 https://gitee.com/baomidou/lock4j -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
<version>${lock4j.version}</version>
</dependency>
<!-- 其他依赖 -->
<dependency>

@ -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,

@ -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";
}

@ -58,6 +58,7 @@
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>

@ -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) {

@ -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<String, Session> 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);

@ -18,7 +18,6 @@ public class AskPhysicalResultReqVO {
private String locationCode;
@ApiModelProperty("初步诊断ID")
@NotBlank(message = "初步诊断ID不能为空")
private String primaryId;
@NotBlank(message = "流程ID不能为空")

@ -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<String,String> 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();
}
}

@ -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);
}

@ -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<String, String> 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());
}
}

@ -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;
}
Loading…
Cancel
Save