This commit is contained in:
oneao committed 2025-10-15 17:33:02 +08:00
1 parent 63f1d4d1dc
commit 655581b217
17 files changed
+188 -146

No files matched your search

@@ -1,21 +1,12 @@
package com.link.im.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.api.util.DruidDataSourceManager;
import com.link.api.util.PaginationUtils;
import com.link.api.util.Result;
import com.link.api.util.UserContext;
import com.link.im.manager.UserChannelManager;
import com.link.im.utils.DbUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@CrossOrigin
@@ -29,15 +20,28 @@ public class CompanyController {
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql);
for (Map<String, Object> map : maps) {
String bId = map.get("b_id").toString();
return Result.ok("成功",maps);
} catch (Exception e) {
return Result.fail("公司列表查询异常:" + e.getMessage());
}
}
map.put("_online",UserChannelManager.isOnline(bId));
}
@GetMapping("/api/loadFriendList")
public Result<?> loadFriendList(){
try {
String orgId = UserContext.getOrgId();
String sql = "select b_id,b_name from b_user where b_canuse = '1' ";
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql);
return Result.ok("成功",maps);
} catch (Exception e) {
return Result.fail("公司列表查询异常:" + e.getMessage());
}
}
@GetMapping("/api/loadGroupList")
public Result<?> loadGroupList(){
return Result.ok("");
}
}
@@ -1,6 +1,5 @@
package com.link.im.handler;
import com.link.api.util.UserContext;
import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager;
import com.link.im.model.ImRequest;
@@ -9,7 +8,6 @@ import com.link.im.utils.CommonUtils;
import com.link.im.utils.DbUtils;
import io.netty.channel.ChannelHandlerContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.sql.SQLException;
import java.util.List;
@@ -38,35 +36,68 @@ public class ImMessageProcessor {
}
private void handlePing(ChannelHandlerContext ctx) {
WriteManager.writeToChannel(ctx.channel(), ImResponse.success("pong",null));
WriteManager.writeToChannel(ctx.channel(), ImResponse.success("pong",null,null));
}
private void handleLogin(ChannelHandlerContext ctx,ImRequest request){
String fromUserid = request.getFrom();
UserChannelManager.addUser(fromUserid,ctx.channel());
String orgId = request.getOrgId();
System.out.println(orgId);
String fromUserid = request.getFrom();
// 单个上线
UserChannelManager.addUser(fromUserid,ctx.channel());
// 群组上线
// 1.通知给好友
String sql1 = "select b_frienduser_Id from IM_Friends where b_user_id = '" + fromUserid +"'";
try {
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql1);
String friendSql = "select b_frienduser_Id from IM_Friends where b_user_id = '" + fromUserid +"'";
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, friendSql);
ImResponse response = ImResponse.success();
response.setType("online");
response.setFrom(fromUserid);
for (Map<String, Object> map : maps) {
String bFrienduserId = map.get("b_frienduser_Id").toString();
if (CommonUtils.isNotEmpty(bFrienduserId)) {
boolean friendOnline = UserChannelManager.isOnline(bFrienduserId);
if(friendOnline){
if (CommonUtils.isNotEmpty(bFrienduserId) && UserChannelManager.isOnline(bFrienduserId)) {
WriteManager.writeToUser(bFrienduserId,response);
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
try {
// 1. 查询用户所属群组
String sqlGroups = "SELECT b_group_id FROM IM_GroupMembers WHERE b_member_id = '" + fromUserid + "'";
List<Map<String, Object>> groupList = DbUtils.queryForList(orgId, sqlGroups);
for (Map<String, Object> g : groupList) {
String groupId = (String) g.get("b_group_id");
if (CommonUtils.isEmpty(groupId)) continue;
// 2. 查询群组成员
String sqlMembers = "SELECT b_member_id FROM IM_GroupMembers WHERE b_group_id = '" + groupId + "'";
List<Map<String, Object>> memberList = DbUtils.queryForList(orgId, sqlMembers);
// 3. 构造消息
ImResponse response = ImResponse.success();
response.setFrom(fromUserid);
response.setData(CommonUtils.mapOf("groupId", groupId));
// 4. 遍历群成员发送消息
for (Map<String, Object> member : memberList) {
String memberId = (String) member.get("b_member_id");
if (!fromUserid.equals(memberId)) {
WriteManager.writeToUser(memberId, response);
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 2.通知给群组
// 3.通知给全部(测试用,先留,后面会删)
}
}
@@ -1,10 +1,8 @@
package com.link.im.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.im.manager.GroupManager;
import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager;
import com.link.im.model.ImMessage;
import com.link.im.model.ImRequest;
import com.link.im.model.ImResponse;
import io.netty.channel.ChannelHandler;
@@ -31,9 +29,6 @@ public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocke
// 下线时通过 UserChannelManager 移除
UserChannelManager.removeUserByChannel(ctx.channel());
// 同时从所有群中移除
GroupManager.removeUserFromAllGroups(ctx.channel());
System.out.println("❌ 客户端断开: " + ctx.channel().id().asShortText());
}
@@ -1,71 +0,0 @@
package com.link.im.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class GroupManager {
private static final Logger logger = LoggerFactory.getLogger(GroupManager.class);
// groupId -> Set<userId>
private static final ConcurrentHashMap<String, Set<String>> GROUP_MAP = new ConcurrentHashMap<>();
/**
* 创建或获取群成员集合
*/
private static Set<String> getGroup(String groupId) {
return GROUP_MAP.computeIfAbsent(groupId, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
}
/**
* 添加成员到群
*/
public static void addUser(String groupId, String userId) {
if (groupId == null || userId == null) return;
getGroup(groupId).add(userId);
}
/**
* 从群中移除成员
*/
public static void removeUser(String groupId, String userId) {
Set<String> members = GROUP_MAP.get(groupId);
if (members != null) {
members.remove(userId);
}
}
/**
* 根据 Channel 移除该用户在所有群的成员记录
*/
public static void removeUserFromAllGroups(Channel channel) {
if (channel == null) return;
// 遍历所有群
GROUP_MAP.forEach((groupId, members) -> {
members.removeIf(userId -> {
Channel ch = UserChannelManager.getChannel(userId);
boolean match = ch != null && ch.equals(channel);
if (match) {
logger.info("用户从群移除: groupId={}, userId={}", groupId, userId);
}
return match;
});
});
}
/**
* 获取群成员列表(不可修改)
*/
public static Set<String> getMembers(String groupId) {
Set<String> members = GROUP_MAP.get(groupId);
if (members == null) return Collections.emptySet();
return Collections.unmodifiableSet(members);
}
}
@@ -1,13 +1,12 @@
package com.link.im.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.im.handler.ImWebSocketHandler;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.List;
public class WriteManager {
private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -15,8 +14,16 @@ public class WriteManager {
/**
* 发送消息 - 根据 userId
* 内部会自动判断用户是否在线
*/
public static void writeToUser(String userId, Object message) {
if (userId == null || message == null) return;
if (!UserChannelManager.isOnline(userId)) {
// 用户不在线,直接跳过
return;
}
Channel ch = UserChannelManager.getChannel(userId);
writeToChannel(ch, message);
}
@@ -36,22 +43,25 @@ public class WriteManager {
}
/**
* 向群内所有在线成员广播消息
* 只在异常时记录日志
* 广播消息给指定用户列表
* 内部会自动过滤离线用户
*/
public static void writeToGroup(String groupId, Object message) {
Set<String> members = GroupManager.getMembers(groupId);
for (String userId : members) {
try {
Channel ch = UserChannelManager.getChannel(userId);
if (ch != null && ch.isActive() && message != null) {
String json = message instanceof String ? (String) message : MAPPER.writeValueAsString(message);
public static void broadcastToUsers(List<String> userIds, Object message) {
if (message == null || userIds == null || userIds.isEmpty()) return;
ch.writeAndFlush(new TextWebSocketFrame(json));
}
} catch (Exception e) {
logger.error("❌ 群消息发送失败 groupId={} userId={}", groupId, userId, e);
}
for (String userId : userIds) {
writeToUser(userId, message); // 内部自动判断在线
}
}
/**
* 广播消息给指定 Channel 列表
*/
public static void broadcastToChannels(List<Channel> channels, Object message) {
if (message == null || channels == null || channels.isEmpty()) return;
for (Channel ch : channels) {
writeToChannel(ch, message);
}
}
}
@@ -5,37 +5,45 @@ public class ImResponse {
private int code;
private String message;
private long timestamp;
private String from;
private Object data;
// 无参构造
public ImResponse() {}
public ImResponse() {
}
// 全参构造
public ImResponse(String type, int code, String message, long timestamp, Object data) {
public ImResponse(String type, int code, String message, long timestamp, String from,Object data) {
this.type = type;
this.code = code;
this.message = message;
this.timestamp = timestamp;
this.from = from;
this.data = data;
}
public static ImResponse success(String type, Object data) {
return new ImResponse(type, 200, "success", System.currentTimeMillis(), data);
public static ImResponse success() {
ImResponse response = new ImResponse();
response.setCode(200);
response.setMessage("success");
response.setTimestamp( System.currentTimeMillis());
return response;
}
public static ImResponse success(String type, String message, Object data) {
return new ImResponse(type, 200, message, System.currentTimeMillis(), data);
public static ImResponse success(String type, String from,Object data) {
return new ImResponse(type, 200, "success", System.currentTimeMillis(), from,data);
}
public static ImResponse success(String type, String message,String from,Object data) {
return new ImResponse(type, 200, message, System.currentTimeMillis(), from,data);
}
public static ImResponse error(String type, String message) {
return new ImResponse(type, 500, message, System.currentTimeMillis(), null);
return new ImResponse(type, 500, message, System.currentTimeMillis(), null,null);
}
public static ImResponse error(String type, int code, String message) {
return new ImResponse(type, code, message, System.currentTimeMillis(), null);
return new ImResponse(type, code, message, System.currentTimeMillis(), null,null);
}
// Getter & Setter
public String getType() {
return type;
}
@@ -68,6 +76,14 @@ public class ImResponse {
this.timestamp = timestamp;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public Object getData() {
return data;
}
@@ -75,15 +91,4 @@ public class ImResponse {
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "ImResponse{" +
"type='" + type + '\'' +
", code=" + code +
", message='" + message + '\'' +
", timestamp=" + timestamp +
", data=" + data +
'}';
}
}
@@ -1,5 +1,8 @@
package com.link.im.utils;
import java.util.HashMap;
import java.util.Map;
public class CommonUtils {
/**
* 判断字符串是否为空(null 或 "")
@@ -18,4 +21,29 @@ public class CommonUtils {
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
/**
* 快速创建 Map,类似 JDK9 的 Map.of()
* 用法示例:
* MapUtils.of("groupId", 1, "userId", 2, "status", "online")
*/
public static <K, V> Map<K, V> mapOf(Object... keyValues) {
if (keyValues == null || keyValues.length == 0) {
return new HashMap<>();
}
if (keyValues.length % 2 != 0) {
throw new IllegalArgumentException("参数数量必须为偶数(键值成对出现)");
}
Map<K, V> map = new HashMap<>(keyValues.length / 2);
for (int i = 0; i < keyValues.length; i += 2) {
@SuppressWarnings("unchecked")
K key = (K) keyValues[i];
@SuppressWarnings("unchecked")
V value = (V) keyValues[i + 1];
map.put(key, value);
}
return map;
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { useWebSocket } from './stores/websocketStore'
const websocket = useWebSocket()
onBeforeUnmount(() => {
// websocket.disconnect()
websocket.disconnect()
})
</script>
@@ -31,7 +31,6 @@ onMounted(async () => {
</div>
<el-table :data="tableData" border stripe style="width: 50%;">
<el-table-column prop="b_name" label="用户名称" />
<el-table-column prop="_online" label="在线状态" />
<el-table-column fixed="right" label="操作">
<template #default="scope">
+44 -3
View File
@@ -1,7 +1,42 @@
<script setup>
import { Search } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import Avatar from '@/components/avatar.vue'
import Icon from '@/components/icon.vue'
import { axiosGet, axiosPost } from '@/utils/axios'
import { useUserStore } from '../../stores/userStore'
import { useWebSocket } from '../../stores/websocketStore'
const websocket = useWebSocket()
const userStore = useUserStore()
const friendList = ref([])
const groupList = ref([])
onMounted(async () => {
const userid = userStore.userInfo.userid
const friendSql = `
SELECT
f.*,
u.b_name,
m.b_content AS last_message,
m.b_SendTime AS last_message_time
FROM IM_Friends f
INNER JOIN b_user u
ON f.b_frienduser_id = u.b_id
LEFT JOIN IM_PrivateMessages m
ON f.b_last_message_id = m.b_id
WHERE f.b_user_id = '${userid}';
`
const friendRes = await axiosGet('/selectlistbysql', {
sql: friendSql,
})
if (friendRes.data.records) {
friendList.value = friendRes.data.records
}
})
</script>
<template>
@@ -26,10 +61,13 @@ import Icon from '@/components/icon.vue'
<Avatar name="User" />
<div class="im__user-info">
<div class="im__user-name">
用户A
{{ userStore.userInfo.username }}
</div>
<div class="im__user-status im__user-status--online">
<span>在线</span>
<div
class="im__user-status"
:class="websocket.isConnected.value ? 'im__user-status--online' : 'im__user-status--offline'"
>
<span>{{ websocket.isConnected.value ? '在线' : '离线' }}</span>
</div>
</div>
</div>
@@ -350,6 +388,9 @@ import Icon from '@/components/icon.vue'
.im__user-status--online {
color: #4caf50;
}
.im__user-status--offline {
color: #666;
}
}
}
}