wip: 临时提交

This commit is contained in:
Ray.Hao
2025-04-22 20:49:49 +08:00
parent f06fe3ee01
commit ecba46e020
9 changed files with 554 additions and 51 deletions

View File

@@ -16,6 +16,7 @@ import com.youlai.boot.system.model.form.DictForm;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.service.DictItemService;
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.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -42,7 +43,7 @@ public class DictController {
private final DictService dictService;
private final DictItemService dictItemService;
private final WebSocketMessageService webSocketMessageService;
//---------------------------------------------------
// 字典相关接口
@@ -80,6 +81,10 @@ public class DictController {
@RepeatSubmit
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
boolean result = dictService.saveDict(formData);
// 发送字典更新通知
if (result && formData.getCode() != null) {
webSocketMessageService.sendDictUpdatedEvent(formData.getCode());
}
return Result.judge(result);
}
@@ -88,9 +93,13 @@ public class DictController {
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
public Result<?> updateDict(
@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);
}
@@ -100,7 +109,16 @@ public class DictController {
public Result<?> deleteDictionaries(
@Parameter(description = "字典ID多个以英文逗号(,)拼接") @PathVariable String ids
) {
// 获取字典编码列表,用于发送删除通知
List<String> dictCodes = dictService.getDictCodesByIds(Arrays.stream(ids.split(",")).toList());
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
// 发送字典删除通知
for (String dictCode : dictCodes) {
webSocketMessageService.sendDictDeletedEvent(dictCode);
}
return Result.success();
}
@@ -138,6 +156,12 @@ public class DictController {
) {
formData.setDictCode(dictCode);
boolean result = dictItemService.saveDictItem(formData);
// 发送字典更新通知
if (result) {
webSocketMessageService.sendDictUpdatedEvent(dictCode);
}
return Result.judge(result);
}
@@ -163,6 +187,12 @@ public class DictController {
formData.setId(itemId);
formData.setDictCode(dictCode);
boolean status = dictItemService.updateDictItem(formData);
// 发送字典更新通知
if (status) {
webSocketMessageService.sendDictUpdatedEvent(dictCode);
}
return Result.judge(status);
}
@@ -170,9 +200,14 @@ public class DictController {
@DeleteMapping("/{dictCode}/items/{itemIds}")
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
public Result<Void> deleteDictItems(
@PathVariable String dictCode,
@Parameter(description = "字典ID多个以英文逗号(,)拼接") @PathVariable String itemIds
) {
dictItemService.deleteDictItemByIds(itemIds);
// 发送字典更新通知
webSocketMessageService.sendDictUpdatedEvent(dictCode);
return Result.success();
}

View File

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

View File

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

View File

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

View File

@@ -66,6 +66,11 @@ public interface DictService extends IService<Dict> {
*/
void deleteDictByIds(List<String> ids);
/**
* 根据字典ID列表获取字典编码列表
*
* @param ids 字典ID列表
* @return 字典编码列表
*/
List<String> getDictCodesByIds(List<String> ids);
}

View File

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

View File

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

View File

@@ -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.form.DictForm;
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.service.DictItemService;
import com.youlai.boot.system.service.DictService;
@@ -110,20 +109,23 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
*/
@Override
public boolean updateDict(Long id, DictForm dictForm) {
// 更新字典
Dict entity = dictConverter.toEntity(dictForm);
// 校验 code 是否唯一
String dictCode = entity.getDictCode();
long count = this.count(new LambdaQueryWrapper<Dict>()
.eq(Dict::getDictCode, dictCode)
.ne(Dict::getId, id)
);
if (count > 0) {
throw new BusinessException("字典编码已存在");
// 获取字典
Dict entity = this.getById(id);
if (entity == null) {
throw new BusinessException("字典不存在");
}
return this.updateById(entity);
// 校验 code 是否唯一
String dictCode = dictForm.getCode();
if (!entity.getDictCode().equals(dictCode)) {
long count = this.count(new LambdaQueryWrapper<Dict>()
.eq(Dict::getDictCode, dictCode)
);
Assert.isTrue(count == 0, "字典编码已存在");
}
// 更新字典
Dict dict = dictConverter.toEntity(dictForm);
dict.setId(id);
return this.updateById(dict);
}
/**
@@ -131,25 +133,34 @@ public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements Di
*
* @param ids 字典ID多个以英文逗号(,)分割
*/
@Override
@Transactional
@Override
public void deleteDictByIds(List<String> ids) {
for (String id : ids) {
Dict dict = this.getById(id);
if (dict != null) {
boolean removeResult = this.removeById(id);
// 删除字典下的字典项
if (removeResult) {
dictItemService.remove(
new LambdaQueryWrapper<DictItem>()
.eq(DictItem::getDictCode, dict.getDictCode())
);
}
// 删除字典
this.removeByIds(ids);
}
// 删除字典项
List<Dict> list = this.listByIds(ids);
if (!list.isEmpty()) {
List<String> dictCodes = list.stream().map(Dict::getDictCode).toList();
dictItemService.remove(new LambdaQueryWrapper<DictItem>()
.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();
}
}