wip: 临时提交
This commit is contained in:
@@ -3,10 +3,10 @@ package com.youlai.boot.config;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.youlai.boot.core.security.model.SysUserDetails;
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
import com.youlai.boot.core.security.token.TokenManager;
|
import com.youlai.boot.core.security.token.TokenManager;
|
||||||
import com.youlai.boot.system.event.UserConnectionEvent;
|
import com.youlai.boot.system.service.UserOnlineService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
@@ -29,23 +29,17 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
|||||||
/**
|
/**
|
||||||
* WebSocket配置
|
* WebSocket配置
|
||||||
*
|
*
|
||||||
* @author Ray.Hao
|
* @author You Lai
|
||||||
* @since 2.4.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
|
|
||||||
@EnableWebSocketMessageBroker
|
@EnableWebSocketMessageBroker
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
|
||||||
|
|
||||||
private final TokenManager tokenManager;
|
private final TokenManager tokenManager;
|
||||||
|
private final UserOnlineService userOnlineService;
|
||||||
public WebSocketConfig(ApplicationEventPublisher eventPublisher, TokenManager tokenManager) {
|
|
||||||
this.eventPublisher = eventPublisher;
|
|
||||||
this.tokenManager = tokenManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册一个端点,客户端通过这个端点进行连接
|
* 注册一个端点,客户端通过这个端点进行连接
|
||||||
@@ -56,7 +50,9 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
// 注册 /ws 的端点
|
// 注册 /ws 的端点
|
||||||
.addEndpoint("/ws")
|
.addEndpoint("/ws")
|
||||||
// 允许跨域
|
// 允许跨域
|
||||||
.setAllowedOriginPatterns("*");
|
.setAllowedOriginPatterns("*")
|
||||||
|
// 开启SockJS支持,用于不支持WebSocket的浏览器
|
||||||
|
.withSockJS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -135,8 +131,8 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解)
|
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解)
|
||||||
accessor.setUser(authentication);
|
accessor.setUser(authentication);
|
||||||
|
|
||||||
// 发布用户上线事件(示例:可用于更新在线用户列表)
|
// 记录用户上线状态
|
||||||
eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
|
userOnlineService.userConnected(username, accessor.getSessionId());
|
||||||
|
|
||||||
}
|
}
|
||||||
// 处理客户端断开请求
|
// 处理客户端断开请求
|
||||||
@@ -149,7 +145,9 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
|
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
|
||||||
log.info("WebSocket连接关闭:用户[{}]", username);
|
log.info("WebSocket连接关闭:用户[{}]", username);
|
||||||
eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
|
|
||||||
|
// 记录用户下线状态
|
||||||
|
userOnlineService.userDisconnected(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (AuthenticationException ex) {
|
} catch (AuthenticationException ex) {
|
||||||
@@ -166,5 +164,4 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.youlai.boot.system.model.form.DictForm;
|
|||||||
import com.youlai.boot.common.annotation.Log;
|
import com.youlai.boot.common.annotation.Log;
|
||||||
import com.youlai.boot.system.service.DictItemService;
|
import com.youlai.boot.system.service.DictItemService;
|
||||||
import com.youlai.boot.system.service.DictService;
|
import com.youlai.boot.system.service.DictService;
|
||||||
|
import com.youlai.boot.system.service.WebSocketMessageService;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -42,7 +43,7 @@ public class DictController {
|
|||||||
|
|
||||||
private final DictService dictService;
|
private final DictService dictService;
|
||||||
private final DictItemService dictItemService;
|
private final DictItemService dictItemService;
|
||||||
|
private final WebSocketMessageService webSocketMessageService;
|
||||||
|
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
// 字典相关接口
|
// 字典相关接口
|
||||||
@@ -80,6 +81,10 @@ public class DictController {
|
|||||||
@RepeatSubmit
|
@RepeatSubmit
|
||||||
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
|
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
|
||||||
boolean result = dictService.saveDict(formData);
|
boolean result = dictService.saveDict(formData);
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (result && formData.getCode() != null) {
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(formData.getCode());
|
||||||
|
}
|
||||||
return Result.judge(result);
|
return Result.judge(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +93,13 @@ public class DictController {
|
|||||||
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
|
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
|
||||||
public Result<?> updateDict(
|
public Result<?> updateDict(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@RequestBody DictForm DictForm
|
@RequestBody DictForm dictForm
|
||||||
) {
|
) {
|
||||||
boolean status = dictService.updateDict(id, DictForm);
|
boolean status = dictService.updateDict(id, dictForm);
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (status && dictForm.getCode() != null) {
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(dictForm.getCode());
|
||||||
|
}
|
||||||
return Result.judge(status);
|
return Result.judge(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +109,16 @@ public class DictController {
|
|||||||
public Result<?> deleteDictionaries(
|
public Result<?> deleteDictionaries(
|
||||||
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids
|
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids
|
||||||
) {
|
) {
|
||||||
|
// 获取字典编码列表,用于发送删除通知
|
||||||
|
List<String> dictCodes = dictService.getDictCodesByIds(Arrays.stream(ids.split(",")).toList());
|
||||||
|
|
||||||
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
|
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
|
||||||
|
|
||||||
|
// 发送字典删除通知
|
||||||
|
for (String dictCode : dictCodes) {
|
||||||
|
webSocketMessageService.sendDictDeletedEvent(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +156,12 @@ public class DictController {
|
|||||||
) {
|
) {
|
||||||
formData.setDictCode(dictCode);
|
formData.setDictCode(dictCode);
|
||||||
boolean result = dictItemService.saveDictItem(formData);
|
boolean result = dictItemService.saveDictItem(formData);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (result) {
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.judge(result);
|
return Result.judge(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +187,12 @@ public class DictController {
|
|||||||
formData.setId(itemId);
|
formData.setId(itemId);
|
||||||
formData.setDictCode(dictCode);
|
formData.setDictCode(dictCode);
|
||||||
boolean status = dictItemService.updateDictItem(formData);
|
boolean status = dictItemService.updateDictItem(formData);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
if (status) {
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(dictCode);
|
||||||
|
}
|
||||||
|
|
||||||
return Result.judge(status);
|
return Result.judge(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +200,14 @@ public class DictController {
|
|||||||
@DeleteMapping("/{dictCode}/items/{itemIds}")
|
@DeleteMapping("/{dictCode}/items/{itemIds}")
|
||||||
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
|
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
|
||||||
public Result<Void> deleteDictItems(
|
public Result<Void> deleteDictItems(
|
||||||
|
@PathVariable String dictCode,
|
||||||
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String itemIds
|
@Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String itemIds
|
||||||
) {
|
) {
|
||||||
dictItemService.deleteDictItemByIds(itemIds);
|
dictItemService.deleteDictItemByIds(itemIds);
|
||||||
|
|
||||||
|
// 发送字典更新通知
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(dictCode);
|
||||||
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.youlai.boot.system.controller;
|
||||||
|
|
||||||
|
import com.youlai.boot.common.result.Result;
|
||||||
|
import com.youlai.boot.system.service.UserOnlineService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户控制器
|
||||||
|
*
|
||||||
|
* @author You Lai
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "13.在线用户接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/users/online")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserOnlineController {
|
||||||
|
|
||||||
|
private final UserOnlineService userOnlineService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户列表
|
||||||
|
*
|
||||||
|
* @return 在线用户列表
|
||||||
|
*/
|
||||||
|
@Operation(summary = "获取在线用户列表")
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("@ss.hasPerm('sys:monitor:online')")
|
||||||
|
public Result<List<UserOnlineService.UserOnlineDTO>> getOnlineUsers() {
|
||||||
|
return Result.success(userOnlineService.getOnlineUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户统计信息
|
||||||
|
*
|
||||||
|
* @return 在线用户统计
|
||||||
|
*/
|
||||||
|
@Operation(summary = "获取在线用户统计")
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@PreAuthorize("@ss.hasPerm('sys:monitor:online')")
|
||||||
|
public Result<Map<String, Object>> getOnlineStats() {
|
||||||
|
return Result.success(Map.of(
|
||||||
|
"count", userOnlineService.getOnlineUserCount()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.youlai.boot.system.controller;
|
||||||
|
|
||||||
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
|
import com.youlai.boot.system.service.WebSocketMessageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.messaging.handler.annotation.Payload;
|
||||||
|
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket消息控制器
|
||||||
|
* 用于处理WebSocket客户端发送的消息
|
||||||
|
*
|
||||||
|
* @author You Lai
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class WebSocketMessageController {
|
||||||
|
|
||||||
|
private final WebSocketMessageService webSocketMessageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理发送到指定用户的消息
|
||||||
|
* 客户端发送消息到 /app/sendToUser/{username}
|
||||||
|
*
|
||||||
|
* @param message 消息内容
|
||||||
|
* @param headerAccessor 消息头访问器
|
||||||
|
* @param username 接收消息的用户名
|
||||||
|
*/
|
||||||
|
@MessageMapping("/sendToUser/{username}")
|
||||||
|
public void sendToUser(@Payload String message, SimpMessageHeaderAccessor headerAccessor, String username) {
|
||||||
|
Authentication authentication = (Authentication) headerAccessor.getUser();
|
||||||
|
if (authentication != null) {
|
||||||
|
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||||
|
String sender = userDetails.getUsername();
|
||||||
|
|
||||||
|
// 构建消息
|
||||||
|
Map<String, Object> messageData = Map.of(
|
||||||
|
"sender", sender,
|
||||||
|
"content", message,
|
||||||
|
"timestamp", System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送点对点消息
|
||||||
|
webSocketMessageService.sendPrivateMessage(username, messageData);
|
||||||
|
log.info("用户[{}]向用户[{}]发送消息: {}", sender, username, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理广播消息
|
||||||
|
* 客户端发送消息到 /app/broadcast
|
||||||
|
*
|
||||||
|
* @param message 消息内容
|
||||||
|
* @param headerAccessor 消息头访问器
|
||||||
|
*/
|
||||||
|
@MessageMapping("/broadcast")
|
||||||
|
public void broadcast(@Payload String message, SimpMessageHeaderAccessor headerAccessor) {
|
||||||
|
Authentication authentication = (Authentication) headerAccessor.getUser();
|
||||||
|
if (authentication != null) {
|
||||||
|
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||||
|
String sender = userDetails.getUsername();
|
||||||
|
|
||||||
|
// 构建消息
|
||||||
|
Map<String, Object> messageData = Map.of(
|
||||||
|
"sender", sender,
|
||||||
|
"content", message,
|
||||||
|
"timestamp", System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送广播消息
|
||||||
|
webSocketMessageService.broadcastMessage(messageData);
|
||||||
|
log.info("用户[{}]发送广播消息: {}", sender, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.youlai.boot.system.controller;
|
||||||
|
|
||||||
|
import com.youlai.boot.common.result.Result;
|
||||||
|
import com.youlai.boot.system.service.WebSocketMessageService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket测试控制器
|
||||||
|
*
|
||||||
|
* @author You Lai
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "12.WebSocket接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/websocket")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketTestController {
|
||||||
|
|
||||||
|
private final WebSocketMessageService webSocketMessageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送字典更新事件
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@Operation(summary = "发送字典更新事件")
|
||||||
|
@PostMapping("/dict/{dictCode}/updated")
|
||||||
|
public Result<Void> sendDictUpdatedEvent(
|
||||||
|
@Parameter(description = "字典编码") @PathVariable String dictCode
|
||||||
|
) {
|
||||||
|
webSocketMessageService.sendDictUpdatedEvent(dictCode);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送字典删除事件
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@Operation(summary = "发送字典删除事件")
|
||||||
|
@PostMapping("/dict/{dictCode}/deleted")
|
||||||
|
public Result<Void> sendDictDeletedEvent(
|
||||||
|
@Parameter(description = "字典编码") @PathVariable String dictCode
|
||||||
|
) {
|
||||||
|
webSocketMessageService.sendDictDeletedEvent(dictCode);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,11 @@ public interface DictService extends IService<Dict> {
|
|||||||
*/
|
*/
|
||||||
void deleteDictByIds(List<String> ids);
|
void deleteDictByIds(List<String> ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典ID列表获取字典编码列表
|
||||||
|
*
|
||||||
|
* @param ids 字典ID列表
|
||||||
|
* @return 字典编码列表
|
||||||
|
*/
|
||||||
|
List<String> getDictCodesByIds(List<String> ids);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.youlai.boot.system.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线状态服务
|
||||||
|
* 负责维护用户的在线状态和相关统计
|
||||||
|
*
|
||||||
|
* @author You Lai
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class UserOnlineService {
|
||||||
|
|
||||||
|
// 在线用户映射表,key为用户名,value为用户在线信息
|
||||||
|
private final Map<String, UserOnlineInfo> onlineUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户上线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param sessionId WebSocket会话ID
|
||||||
|
*/
|
||||||
|
public void userConnected(String username, String sessionId) {
|
||||||
|
UserOnlineInfo info = new UserOnlineInfo(username, sessionId, System.currentTimeMillis());
|
||||||
|
onlineUsers.put(username, info);
|
||||||
|
log.info("用户[{}]上线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户下线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
*/
|
||||||
|
public void userDisconnected(String username) {
|
||||||
|
onlineUsers.remove(username);
|
||||||
|
log.info("用户[{}]下线,当前在线用户数:{}", username, onlineUsers.size());
|
||||||
|
|
||||||
|
// 通知在线用户状态变更
|
||||||
|
notifyOnlineUsersChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户列表
|
||||||
|
*
|
||||||
|
* @return 在线用户名列表
|
||||||
|
*/
|
||||||
|
public List<UserOnlineDTO> getOnlineUsers() {
|
||||||
|
return onlineUsers.values().stream()
|
||||||
|
.map(info -> new UserOnlineDTO(info.getUsername(), info.getLoginTime()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户数量
|
||||||
|
*
|
||||||
|
* @return 在线用户数
|
||||||
|
*/
|
||||||
|
public int getOnlineUserCount() {
|
||||||
|
return onlineUsers.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否在线
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 是否在线
|
||||||
|
*/
|
||||||
|
public boolean isUserOnline(String username) {
|
||||||
|
return onlineUsers.containsKey(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有客户端在线用户变更
|
||||||
|
*/
|
||||||
|
private void notifyOnlineUsersChange() {
|
||||||
|
try {
|
||||||
|
OnlineUsersChangeEvent event = new OnlineUsersChangeEvent();
|
||||||
|
event.setType("ONLINE_USERS_CHANGE");
|
||||||
|
event.setCount(onlineUsers.size());
|
||||||
|
event.setUsers(getOnlineUsers());
|
||||||
|
event.setTimestamp(System.currentTimeMillis());
|
||||||
|
|
||||||
|
String message = objectMapper.writeValueAsString(event);
|
||||||
|
messagingTemplate.convertAndSend("/topic/online-users", message);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Failed to send online users change event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class UserOnlineInfo {
|
||||||
|
private final String username;
|
||||||
|
private final String sessionId;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在线DTO(用于返回给前端)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class UserOnlineDTO {
|
||||||
|
private final String username;
|
||||||
|
private final long loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户变更事件
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
private static class OnlineUsersChangeEvent {
|
||||||
|
private String type;
|
||||||
|
private int count;
|
||||||
|
private List<UserOnlineDTO> users;
|
||||||
|
private long timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.youlai.boot.system.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket消息服务
|
||||||
|
*
|
||||||
|
* @author Ray
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class WebSocketMessageService {
|
||||||
|
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典事件类型
|
||||||
|
*/
|
||||||
|
public enum DictEventType {
|
||||||
|
/**
|
||||||
|
* 字典更新
|
||||||
|
*/
|
||||||
|
DICT_UPDATED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典删除
|
||||||
|
*/
|
||||||
|
DICT_DELETED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典事件消息
|
||||||
|
*/
|
||||||
|
public static class DictEvent {
|
||||||
|
/**
|
||||||
|
* 事件类型
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
private String dictCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间戳
|
||||||
|
*/
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public DictEvent(DictEventType type, String dictCode) {
|
||||||
|
this.type = type.name();
|
||||||
|
this.dictCode = dictCode;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDictCode() {
|
||||||
|
return dictCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDictCode(String dictCode) {
|
||||||
|
this.dictCode = dictCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(long timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向所有客户端发送字典更新事件
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
public void sendDictUpdatedEvent(String dictCode) {
|
||||||
|
DictEvent event = new DictEvent(DictEventType.DICT_UPDATED, dictCode);
|
||||||
|
sendDictEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向所有客户端发送字典删除事件
|
||||||
|
*
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
public void sendDictDeletedEvent(String dictCode) {
|
||||||
|
DictEvent event = new DictEvent(DictEventType.DICT_DELETED, dictCode);
|
||||||
|
sendDictEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送字典事件消息
|
||||||
|
*
|
||||||
|
* @param event 字典事件
|
||||||
|
*/
|
||||||
|
private void sendDictEvent(DictEvent event) {
|
||||||
|
try {
|
||||||
|
String message = objectMapper.writeValueAsString(event);
|
||||||
|
messagingTemplate.convertAndSend("/topic/dict", message);
|
||||||
|
log.info("Sent dict event to clients: {}", message);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Failed to send dict event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import com.youlai.boot.system.model.entity.Dict;
|
|||||||
import com.youlai.boot.system.model.entity.DictItem;
|
import com.youlai.boot.system.model.entity.DictItem;
|
||||||
import com.youlai.boot.system.model.form.DictForm;
|
import com.youlai.boot.system.model.form.DictForm;
|
||||||
import com.youlai.boot.system.model.query.DictPageQuery;
|
import com.youlai.boot.system.model.query.DictPageQuery;
|
||||||
import com.youlai.boot.system.model.vo.DictItemOptionVO;
|
|
||||||
import com.youlai.boot.system.model.vo.DictPageVO;
|
import com.youlai.boot.system.model.vo.DictPageVO;
|
||||||
import com.youlai.boot.system.service.DictItemService;
|
import com.youlai.boot.system.service.DictItemService;
|
||||||
import com.youlai.boot.system.service.DictService;
|
import com.youlai.boot.system.service.DictService;
|
||||||
@@ -110,20 +109,23 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean updateDict(Long id, DictForm dictForm) {
|
public boolean updateDict(Long id, DictForm dictForm) {
|
||||||
// 更新字典
|
// 获取字典
|
||||||
Dict entity = dictConverter.toEntity(dictForm);
|
Dict entity = this.getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new BusinessException("字典不存在");
|
||||||
|
}
|
||||||
// 校验 code 是否唯一
|
// 校验 code 是否唯一
|
||||||
String dictCode = entity.getDictCode();
|
String dictCode = dictForm.getCode();
|
||||||
|
if (!entity.getDictCode().equals(dictCode)) {
|
||||||
long count = this.count(new LambdaQueryWrapper<Dict>()
|
long count = this.count(new LambdaQueryWrapper<Dict>()
|
||||||
.eq(Dict::getDictCode, dictCode)
|
.eq(Dict::getDictCode, dictCode)
|
||||||
.ne(Dict::getId, id)
|
|
||||||
);
|
);
|
||||||
if (count > 0) {
|
Assert.isTrue(count == 0, "字典编码已存在");
|
||||||
throw new BusinessException("字典编码已存在");
|
|
||||||
}
|
}
|
||||||
|
// 更新字典
|
||||||
return this.updateById(entity);
|
Dict dict = dictConverter.toEntity(dictForm);
|
||||||
|
dict.setId(id);
|
||||||
|
return this.updateById(dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,23 +133,32 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
|
|||||||
*
|
*
|
||||||
* @param ids 字典ID,多个以英文逗号(,)分割
|
* @param ids 字典ID,多个以英文逗号(,)分割
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@Override
|
||||||
public void deleteDictByIds(List<String> ids) {
|
public void deleteDictByIds(List<String> ids) {
|
||||||
for (String id : ids) {
|
// 删除字典
|
||||||
Dict dict = this.getById(id);
|
this.removeByIds(ids);
|
||||||
if (dict != null) {
|
|
||||||
boolean removeResult = this.removeById(id);
|
// 删除字典项
|
||||||
// 删除字典下的字典项
|
List<Dict> list = this.listByIds(ids);
|
||||||
if (removeResult) {
|
if (!list.isEmpty()) {
|
||||||
dictItemService.remove(
|
List<String> dictCodes = list.stream().map(Dict::getDictCode).toList();
|
||||||
new LambdaQueryWrapper<DictItem>()
|
dictItemService.remove(new LambdaQueryWrapper<DictItem>()
|
||||||
.eq(DictItem::getDictCode, dict.getDictCode())
|
.in(DictItem::getDictCode, dictCodes)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
}
|
* 根据字典ID列表获取字典编码列表
|
||||||
|
*
|
||||||
|
* @param ids 字典ID列表
|
||||||
|
* @return 字典编码列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> getDictCodesByIds(List<String> ids) {
|
||||||
|
List<Dict> dictList = this.listByIds(ids);
|
||||||
|
return dictList.stream().map(Dict::getDictCode).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user