diff --git a/code/link/link-backend/.gitignore b/code/link/link-backend/.gitignore new file mode 100644 index 00000000..82f3e9d1 --- /dev/null +++ b/code/link/link-backend/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/code/link/link-backend/.idea/.gitignore b/code/link/link-backend/.idea/.gitignore deleted file mode 100644 index 73f69e09..00000000 --- a/code/link/link-backend/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/code/link/link-backend/.idea/compiler.xml b/code/link/link-backend/.idea/compiler.xml deleted file mode 100644 index 37f12852..00000000 --- a/code/link/link-backend/.idea/compiler.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/.idea/encodings.xml b/code/link/link-backend/.idea/encodings.xml deleted file mode 100644 index 63e90019..00000000 --- a/code/link/link-backend/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/.idea/jarRepositories.xml b/code/link/link-backend/.idea/jarRepositories.xml deleted file mode 100644 index aa4ad020..00000000 --- a/code/link/link-backend/.idea/jarRepositories.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/.idea/misc.xml b/code/link/link-backend/.idea/misc.xml deleted file mode 100644 index b784bbc8..00000000 --- a/code/link/link-backend/.idea/misc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/.idea/runConfigurations.xml b/code/link/link-backend/.idea/runConfigurations.xml deleted file mode 100644 index 797acea5..00000000 --- a/code/link/link-backend/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/.idea/vcs.xml b/code/link/link-backend/.idea/vcs.xml deleted file mode 100644 index ba43f699..00000000 --- a/code/link/link-backend/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/code/link/link-backend/pom.xml b/code/link/link-backend/pom.xml index 3e384b0f..ce7dbeb2 100644 --- a/code/link/link-backend/pom.xml +++ b/code/link/link-backend/pom.xml @@ -31,7 +31,7 @@ com.alibaba druid - 1.2.18 + 1.2.18 com.microsoft.sqlserver diff --git a/code/link/link-backend/src/main/java/com/link/api/util/BatchColumn.java b/code/link/link-backend/src/main/java/com/link/api/util/BatchColumn.java index 1064f75e..1b1d096e 100644 --- a/code/link/link-backend/src/main/java/com/link/api/util/BatchColumn.java +++ b/code/link/link-backend/src/main/java/com/link/api/util/BatchColumn.java @@ -123,4 +123,6 @@ public class BatchColumn { } } } + + } diff --git a/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java b/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java index 76a96f9b..c4ac7024 100644 --- a/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java +++ b/code/link/link-backend/src/main/java/com/link/api/util/PaginationUtils.java @@ -85,13 +85,11 @@ public class PaginationUtils { result.put("records", records); return mapper.writeValueAsString(Result.ok("分页查询成功", result)); - } catch (Exception e) { return mapper.writeValueAsString(Result.fail("分页查询失败:" + e.getMessage())); } } - public static String detailSqlServer2008( String orgCode, String tableName, diff --git a/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java b/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java deleted file mode 100644 index d363f3b8..00000000 --- a/code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.link.im.controller; - -import com.link.api.util.Result; -import com.link.api.util.UserContext; -import com.link.im.utils.DbUtils; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -@RestController -@CrossOrigin -public class CompanyController { - - @GetMapping("/api/loadCompanyList") - public Result getCompanyList(){ - try { - String orgId = UserContext.getOrgId(); - String sql = "select b_id,b_name from b_user where b_canuse = '1' "; - - List> maps = DbUtils.queryForList(orgId, sql); - - return Result.ok("成功",maps); - } catch (Exception e) { - return Result.fail("公司列表查询异常:" + e.getMessage()); - } - } - - @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> 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(""); - } -} diff --git a/code/link/link-backend/src/main/java/com/link/im/controller/ImController.java b/code/link/link-backend/src/main/java/com/link/im/controller/ImController.java new file mode 100644 index 00000000..e3e7f6dd --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/controller/ImController.java @@ -0,0 +1,409 @@ +package com.link.im.controller; + +import com.link.api.util.Result; +import com.link.api.util.UserContext; +import com.link.im.manager.UserChannelManager; +import com.link.im.model.PageResult; +import com.link.im.service.ImService; +import com.link.im.utils.CommonUtils; +import com.link.im.utils.DbUtils; +import com.link.im.utils.SnowflakeId; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping("/api") +public class ImController { + + @Resource + private ImService imService; + + /** + * 获取好友列表(分页,可按名字搜索) + */ + @GetMapping("/friends") + public Result getFriendList( + @RequestParam("userId") String userId, + @RequestParam("page") int page, + @RequestParam("pageSize") int pageSize, + @RequestParam(required = false) String searchName, + @RequestParam(required = false) String friendId + ) { + try { + String orgId = UserContext.getOrgId(); + + String sql = "SELECT f.*, " + + " u.b_name AS v_frienduser_name, " + + " m.b_content AS v_last_message, " + + " m.b_send_time AS v_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 + "'"; + + if (CommonUtils.isNotEmpty(searchName)) { + sql += " AND u.b_name LIKE '%" + searchName + "%'"; + } + + // 分页查询 + PageResult pageResult = DbUtils.queryForPage(orgId, sql, "v_last_message_time DESC", page, pageSize); + List> records = pageResult.getRecords(); + + // 判断是否在线 + for (Map item : records) { + String friendIdValue = item.get("b_frienduser_id").toString(); + item.put("is_online", CommonUtils.isNotEmpty(friendIdValue) && UserChannelManager.isOnline(orgId, friendIdValue) ? 1 : 0); + } + + // 如果传了 friendId 并且不在分页里 + if (CommonUtils.isNotEmpty(friendId)) { + boolean exists = records.stream().anyMatch(r -> friendId.equals(r.get("b_frienduser_id").toString())); + if (!exists) { + String friendSql = "SELECT f.*, u.b_name AS v_frienduser_name, m.b_content AS v_last_message, m.b_send_time AS v_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 + "' AND f.b_frienduser_id = '" + friendId + "'"; + + Map friendRecord = DbUtils.querySingle(orgId, friendSql); // 单条查询 + if (friendRecord != null && !friendRecord.isEmpty()) { + String friendIdValue = friendRecord.get("b_frienduser_id").toString(); + friendRecord.put("is_online", CommonUtils.isNotEmpty(friendIdValue) && UserChannelManager.isOnline(orgId, friendIdValue) ? 1 : 0); + + // 插入顶部 + records.add(0, friendRecord); + + // 保持总条数不超过 pageSize:去掉最后一条 + if (records.size() > pageSize) { + records.remove(records.size() - 1); + } + + pageResult.setRecords(records); + } + } + } + + return Result.ok("成功", pageResult); + } catch (Exception e) { + return Result.fail("好友列表查询异常:" + e.getMessage()); + } + } + + /** + * 获取私聊消息(基于时间戳 + 主键 ID 游标分页) + */ + @GetMapping("/privateMessages") + public Result getPrivateMessages( + @RequestParam("userId") String userId, + @RequestParam("friendId") String friendId, + @RequestParam(value = "beforeTimestamp", required = false) Long beforeTimestamp, + @RequestParam(value = "beforeBId", required = false) Long beforeBId, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize + ) { + try { + String orgId = UserContext.getOrgId(); + + StringBuilder sql = new StringBuilder(); + sql.append("SELECT TOP ").append(pageSize).append(" * FROM IM_PrivateMessages ") + .append("WHERE ((b_sender_id = '").append(userId).append("' AND b_receiver_id = '").append(friendId).append("') ") + .append(" OR (b_sender_id = '").append(friendId).append("' AND b_receiver_id = '").append(userId).append("')) "); + + // 双重分页条件 + if (beforeTimestamp != null && beforeBId != null) { + sql.append("AND (b_send_time < ").append(beforeTimestamp) + .append(" OR (b_send_time = ").append(beforeTimestamp) + .append(" AND b_id < ").append(beforeBId).append(")) "); + } + + sql.append("ORDER BY b_send_time DESC, b_id DESC"); + + List> records = DbUtils.queryForList(orgId, sql.toString()); + + // 反转顺序,让最新消息在列表底部 +// Collections.reverse(records); + + return Result.ok("成功", records); + } catch (Exception e) { + return Result.fail("私聊消息查询异常:" + e.getMessage()); + } + } + + @PostMapping("/ensureFriend") + public Result ensureFriend(@RequestBody Map body) { + try { + String orgId = UserContext.getOrgId(); + + CommonUtils.requireFields(body, "userId", "friendId"); + + String userId = body.get("userId").toString(); + String friendId = body.get("friendId").toString(); + + imService.ensureFriend(orgId, userId, friendId); + + return Result.ok("好友校验成功"); + } catch (Exception e) { + return Result.fail("好友校验/添加异常:" + e.getMessage()); + } + } + + @PostMapping("/updatePrivateUnread") + public Result updatePrivateUnread(@RequestBody Map body){ + try { + String orgId = UserContext.getOrgId(); + + CommonUtils.requireFields(body, "b_id", "b_unread_count"); + + String bId = body.get("b_id").toString(); + String bUnreadCount = body.get("b_unread_count").toString(); + + Map updateMap = new HashMap<>(); + updateMap.put("b_id",bId); + updateMap.put("b_unread_count",bUnreadCount); + + DbUtils.updateOne(orgId,"IM_Friends",updateMap,"b_id"); + + return Result.ok("更新未读消息成功"); + } catch (Exception e) { + return Result.fail("更新未读消息失败:" + e.getMessage()); + } + } + + @PostMapping("/updateGroupUnread") + public Result updateGroupUnread(@RequestBody Map body){ + try { + String orgId = UserContext.getOrgId(); + + CommonUtils.requireFields(body, "b_id", "b_unread_count"); + + String bId = body.get("b_id").toString(); + String bUnreadCount = body.get("b_unread_count").toString(); + + Map updateMap = new HashMap<>(); + updateMap.put("b_id",bId); + updateMap.put("b_unread_count",bUnreadCount); + + DbUtils.updateOne(orgId,"IM_GroupMembers",updateMap,"b_id"); + + return Result.ok("更新未读消息成功"); + } catch (Exception e) { + return Result.fail("更新未读消息失败:" + e.getMessage()); + } + } + + @PostMapping("/addGroup") + public Result addGroup (@RequestBody Map body){ + try { + String orgId = UserContext.getOrgId(); + CommonUtils.requireFields(body, "b_groupname"); + + Map insertMap = new HashMap<>(); + insertMap.put("b_id", SnowflakeId.nextId()); + insertMap.put("b_groupname", body.get("b_groupname").toString()); + insertMap.put("b_description", body.get("b_description").toString()); + insertMap.put("b_create_id", body.get("b_create_id").toString()); + insertMap.put("b_max_members", body.getOrDefault("b_max_members",1000)); + insertMap.put("b_create_time", CommonUtils.currentDateTime()); + + DbUtils.insertOne(orgId,"IM_Groups",insertMap,"b_id"); + + return Result.ok("新增群组成功"); + } catch (Exception e) { + return Result.fail("新增群组失败:" + e.getMessage()); + } + } + @GetMapping("/groups") + public Result getGroupList( + @RequestParam("userId") String userId, + @RequestParam("page") int page, + @RequestParam("pageSize") int pageSize, + @RequestParam(required = false) String searchName + ) { + try { + String orgId = UserContext.getOrgId(); + + String sql = "SELECT \n" + + " g.*,\n" + + " m.b_id AS v_group_member_id,\n" + + " gm.b_id AS v_last_message_id,\n" + + " gm.b_content AS v_last_message,\n" + + " gm.b_send_time AS v_last_message_time,\n" + + " u.b_name AS v_last_message_sender_name,\n" + + " m.b_unread_count AS v_unread_count\n" + + "FROM IM_Groups g\n" + + "INNER JOIN IM_GroupMembers m\n" + + " ON g.b_id = m.b_group_id\n" + + "LEFT JOIN IM_GroupMessages gm\n" + + " ON gm.b_id = g.b_last_message_id\n" + + "LEFT JOIN b_user u\n" + + " ON u.b_id = gm.b_sender_id\n" + + "WHERE m.b_member_id = '" + userId + "'"; + // 可选搜索群名 + if (searchName != null && !searchName.isEmpty()) { + sql += " AND g.b_groupname LIKE '%" + searchName + "%'"; + } + + // 分页查询 + PageResult pageResult = DbUtils.queryForPage(orgId, sql, "v_last_message_time DESC", page, pageSize); + + return Result.ok("查询群组成功",pageResult); + }catch (Exception e){ + return Result.fail("查询群组失败:" + e.getMessage()); + } + + } + + @GetMapping("/groupMembers") + public Result getGroupMembers(@RequestParam("groupId") Long groupId) { + try { + String orgId = UserContext.getOrgId(); + + String sql = "SELECT \n" + + " m.b_member_id,\n" + + " u.b_name AS v_member_name,\n" + + " m.b_role,\n" + + " m.b_join_time\n" + + "FROM IM_GroupMembers m\n" + + "LEFT JOIN b_user u\n" + + " ON u.b_id = m.b_member_id\n" + + "WHERE m.b_group_id = " + groupId + "\n" + + "ORDER BY m.b_join_time ASC;"; + + List> maps = DbUtils.queryForList(orgId, sql); + + // 添加在线状态 + maps.forEach(item -> { + String bMemberId = item.get("b_member_id").toString(); + boolean online = UserChannelManager.isOnline(orgId, bMemberId); + item.put("is_online", online); + }); + + // ✅ 按在线状态排序(在线的在上面) + maps.sort((a, b) -> { + boolean aOnline = (boolean) a.get("is_online"); + boolean bOnline = (boolean) b.get("is_online"); + return Boolean.compare(bOnline, aOnline); // true 在前 + }); + + return Result.ok("查询成功", maps); + } catch (Exception e) { + return Result.fail("查询群组成员失败:" + e.getMessage()); + } + } + + /** + * 获取群聊消息(基于时间戳 + 主键 ID 游标分页) + */ + @GetMapping("/groupMessages") + public Result getGroupMessages( + @RequestParam("groupId") String groupId, + @RequestParam(value = "beforeTimestamp", required = false) Long beforeTimestamp, + @RequestParam(value = "beforeBId", required = false) Long beforeBId, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize + ) { + try { + String orgId = UserContext.getOrgId(); + + StringBuilder sql = new StringBuilder(); + sql.append("SELECT TOP ").append(pageSize) + .append(" g.*, u.b_name AS v_sender_name ") + .append("FROM IM_GroupMessages g ") + .append("LEFT JOIN b_user u ON g.b_sender_id = u.b_id ") + .append("WHERE g.b_group_id = '").append(groupId).append("' "); + + // 游标分页条件(按时间戳 + ID 倒序分页) + if (beforeTimestamp != null && beforeBId != null) { + sql.append("AND (g.b_send_time < ").append(beforeTimestamp) + .append(" OR (g.b_send_time = ").append(beforeTimestamp) + .append(" AND g.b_id < ").append(beforeBId).append(")) "); + } + + sql.append("ORDER BY g.b_send_time DESC, g.b_id DESC;"); + + List> records = DbUtils.queryForList(orgId, sql.toString()); + + // 反转列表,让最新消息显示在底部(聊天常规) + Collections.reverse(records); + + return Result.ok("成功", records); + } catch (Exception e) { + return Result.fail("群聊消息查询异常:" + e.getMessage()); + } + } + + @GetMapping("/contacts") + public Result getContacts( + @RequestParam("userId") String userId, + @RequestParam("page") int page, + @RequestParam("pageSize") int pageSize, + @RequestParam(required = false) String searchName + ) { + try { + String orgId = UserContext.getOrgId(); + + // 搜索条件 + String searchCondition = ""; + if (CommonUtils.isNotEmpty(searchName)) { + searchCondition = " AND (u.b_name LIKE '%" + searchName + "%' OR g.b_groupname LIKE '%" + searchName + "%')"; + } + + // 拼接 SQL + String sql = "SELECT * FROM (\n" + + " SELECT \n" + + " f.b_id AS b_id,\n" + + " u.b_id AS v_user_id,\n" + + " 'friend' AS v_type,\n" + + " u.b_name AS v_name,\n" + + " m.b_content AS v_last_message,\n" + + " f.b_unread_count AS v_unread_count,\n" + + " m.b_send_time AS v_last_message_time,\n" + + " NULL AS v_last_message_sender_name\n" + + " FROM IM_Friends f\n" + + " INNER JOIN b_user u ON f.b_frienduser_id = u.b_id\n" + + " LEFT JOIN IM_PrivateMessages m ON f.b_last_message_id = m.b_id\n" + + " WHERE f.b_user_id = '" + userId + "'" + + (searchCondition.contains("u.b_name") ? " AND u.b_name LIKE '%" + searchName + "%'" : "") + "\n" + + "\n" + + " UNION ALL\n" + + "\n" + + " SELECT \n" + + " g.b_id AS b_id,\n" + + " NULL AS v_user_id,\n" + + " 'group' AS v_type,\n" + + " g.b_groupname AS v_name,\n" + + " gm.b_content AS v_last_message,\n" + + " m.b_unread_count AS v_unread_count,\n" + + " gm.b_send_time AS v_last_message_time,\n" + + " sender.b_name AS v_last_message_sender_name\n" + + " FROM IM_Groups g\n" + + " INNER JOIN IM_GroupMembers m ON g.b_id = m.b_group_id\n" + + " LEFT JOIN IM_GroupMessages gm ON gm.b_id = g.b_last_message_id\n" + + " LEFT JOIN b_user sender ON gm.b_sender_id = sender.b_id\n" + + " WHERE m.b_member_id = '" + userId + "'" + + (searchCondition.contains("g.b_groupname") ? " AND g.b_groupname LIKE '%" + searchName + "%'" : "") + "\n" + + ") AS combined"; + + // 查询数据 + PageResult pageResult = DbUtils.queryForPage(orgId, sql, "v_last_message_time desc", page, pageSize); + List> records = pageResult.getRecords(); + records.forEach(item -> { + if(item.get("v_type").equals("friend")){ + item.put("is_online", UserChannelManager.isOnline(orgId, item.get("v_user_id").toString())); + } else { + item.put("is_online", false); + } + }); + + return Result.ok("查询联系人成功", pageResult); + } catch (Exception e) { + e.printStackTrace(); + return Result.fail("查询失败:" + e.getMessage()); + } + } + +} diff --git a/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java b/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java index 5c1942cb..6474afeb 100644 --- a/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java +++ b/code/link/link-backend/src/main/java/com/link/im/handler/ImMessageProcessor.java @@ -1,33 +1,50 @@ package com.link.im.handler; +import com.link.im.ImServer; import com.link.im.manager.UserChannelManager; import com.link.im.manager.WriteManager; import com.link.im.model.ImRequest; import com.link.im.model.ImResponse; +import com.link.im.service.ImService; import com.link.im.utils.CommonUtils; import com.link.im.utils.DbUtils; +import com.link.im.utils.SnowflakeId; import io.netty.channel.ChannelHandlerContext; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @Component public class ImMessageProcessor { + private final ImService imService = new ImService(); + public void process(ChannelHandlerContext ctx, ImRequest request) { String type = request.getType(); if (type == null) { WriteManager.writeToChannel(ctx.channel(), "消息类型不能为空"); return; } - + System.out.println(request); switch (type) { case "ping": handlePing(ctx); break; - case "login": - handleLogin(ctx, request); + case "online": + handleUserStatus(ctx,request,"online"); + break; + case "offline": + handleUserStatus(ctx,request,"offline"); + break; + case "private": + handleChatPrivate(ctx,request); + break; + case "group": + handleChatGroup(ctx,request); break; default: WriteManager.writeToChannel(ctx.channel(), "未知的消息类型: " + type); @@ -39,65 +56,317 @@ public class ImMessageProcessor { WriteManager.writeToChannel(ctx.channel(), ImResponse.success("pong",null,null)); } - private void handleLogin(ChannelHandlerContext ctx,ImRequest request){ + private void handleUserStatus(ChannelHandlerContext ctx, ImRequest request, String statusType) { String orgId = request.getOrgId(); - String fromUserid = request.getFrom(); - // 单个上线 - UserChannelManager.addUser(fromUserid,ctx.channel()); - // 群组上线 - // 1.通知给好友 - try { - String friendSql = "select b_frienduser_Id from IM_Friends where b_user_id = '" + fromUserid +"'"; + System.out.println("===orgId===:" + orgId); - List> maps = DbUtils.queryForList(orgId, friendSql); - - ImResponse response = ImResponse.success(); - response.setType("online"); - response.setFrom(fromUserid); - - for (Map map : maps) { - String bFrienduserId = map.get("b_frienduser_Id").toString(); - if (CommonUtils.isNotEmpty(bFrienduserId) && UserChannelManager.isOnline(bFrienduserId)) { - WriteManager.writeToUser(bFrienduserId,response); - } - } - } catch (SQLException e) { - throw new RuntimeException(e); + // 单个用户上下线处理 + if ("online".equals(statusType)) { + UserChannelManager.addUser(orgId,fromUserid,ctx.channel()); + } else if ("offline".equals(statusType)) { + UserChannelManager.removeUserById(orgId,fromUserid); } + // 构造通知消息 + ImResponse response = ImResponse.success(); + response.setType(statusType); + response.setFrom(fromUserid); + try { - // 1. 查询用户所属群组 + // ---------- 1. 通知好友 ---------- + String friendSql = "SELECT b_frienduser_Id FROM IM_Friends WHERE b_user_id = '" + fromUserid + "'"; + List> friends = DbUtils.queryForList(orgId, friendSql); + + for (Map friend : friends) { + String friendId = (String) friend.get("b_frienduser_Id"); + if (CommonUtils.isNotEmpty(friendId) && UserChannelManager.isOnline(orgId,friendId)) { + WriteManager.writeToUser(orgId,friendId, response); + } + } + + // ---------- 2. 通知群组 ---------- String sqlGroups = "SELECT b_group_id FROM IM_GroupMembers WHERE b_member_id = '" + fromUserid + "'"; List> groupList = DbUtils.queryForList(orgId, sqlGroups); for (Map g : groupList) { - String groupId = (String) g.get("b_group_id"); - + Long groupId = (Long) 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> memberList = DbUtils.queryForList(orgId, sqlMembers); - // 3. 构造消息 - ImResponse response = ImResponse.success(); - response.setFrom(fromUserid); + // 添加群信息到消息 response.setData(CommonUtils.mapOf("groupId", groupId)); - // 4. 遍历群成员发送消息 for (Map member : memberList) { String memberId = (String) member.get("b_member_id"); if (!fromUserid.equals(memberId)) { - WriteManager.writeToUser(memberId, response); + WriteManager.writeToUser(orgId, memberId, response); } } } - } catch (SQLException e) { - throw new RuntimeException(e); - } + } catch (SQLException e) { + throw new RuntimeException("通知好友/群组失败", e); + } + } + + private void handleChatPrivate(ChannelHandlerContext ctx, ImRequest request) { + String orgId = request.getOrgId(); + String from = request.getFrom(); + String to = request.getTo().toString(); + long timestamp = request.getTimestamp(); + String _tempId = ""; + + try { + imService.ensureFriend(orgId, from, to); + int ensureFriend = imService.ensureFriend(orgId, to, from); + if(ensureFriend > 0){ + // 推送给 to + pushFriendUpdate(orgId,to,from); + } + + // 安全提取前端数据 + Map reqData = CommonUtils.objectToMap(request.getData()); + String content = reqData.getOrDefault("content", "").toString(); + String messageType = reqData.getOrDefault("messageType", "text").toString(); + _tempId = reqData.getOrDefault("_tempId", "").toString(); + + // 构建消息 + long messageId = SnowflakeId.nextId(); + Map saveMap = new HashMap<>(); + saveMap.put("b_id", messageId); + saveMap.put("b_sender_id", from); + saveMap.put("b_receiver_id", to); + saveMap.put("b_content", content); + saveMap.put("b_message_type", messageType); + saveMap.put("b_send_time", timestamp); + + // 保存消息 + int inserted = DbUtils.insertOne(orgId, "IM_PrivateMessages", saveMap, "b_id"); + if (inserted == 0) { + throw new RuntimeException("消息插入失败"); + } + + // 更新好友表(发送者与接收者各自更新) + updateFriendState(orgId, from, to, messageId, false); // 自己的记录 + updateFriendState(orgId, to, from, messageId, true); // 对方记录 + + // 构造回传数据 + saveMap.put("_tempId", _tempId); + ImResponse success = ImResponse.success(); + success.setType("private"); + success.setFrom(from); + success.setData(saveMap); + + // 回写给自己 + WriteManager.writeToUser(orgId, from, success); + + // 如果对方在线,推送消息 + if (UserChannelManager.isOnline(orgId, to)) { + WriteManager.writeToUser(orgId, to, success); + } + + } catch (Exception e) { + System.err.println("[IM][PRIVATE] 消息处理异常: " + e.getMessage()); + e.printStackTrace(); + + // 返回错误响应给发送者 + ImResponse error = ImResponse.error("消息发送失败: " + e.getMessage()); + error.setType("private"); + error.setFrom(from); + + Map data = new HashMap<>(); + data.put("_tempId", _tempId); + error.setData(data); + + WriteManager.writeToUser(orgId, from, error); + } + } + + + /** + * 更新好友表状态(使用 DbUtils.updateOne) + */ + private void updateFriendState(String orgId, String userId, String friendUserId, long messageId, boolean incrementUnread) { + try { + // 查找好友记录 + String sql = "SELECT * FROM IM_Friends WHERE b_user_id = '" + userId + "' AND b_frienduser_id = '" + friendUserId + "'"; + Map record = DbUtils.querySingle(orgId, sql); + if (record == null || record.isEmpty()) { + return; + } + + // 获取主键 ID + Object idObj = record.get("b_id"); + if (idObj == null) { + return; + } + long bId; + try { + bId = Long.parseLong(idObj.toString()); + } catch (NumberFormatException e) { + return; + } + + // 计算新的未读数 + int unread = 0; + Object unreadObj = record.get("b_unread_count"); + if (unreadObj != null) { + try { + unread = Integer.parseInt(unreadObj.toString()); + } catch (NumberFormatException ignored) {} + } + + // 构建更新数据 + Map updateMap = new HashMap<>(); + updateMap.put("b_id", bId); + updateMap.put("b_last_message_id", messageId); + if (incrementUnread) { + updateMap.put("b_unread_count", unread + 1); + } + + // 更新 + DbUtils.updateOne(orgId, "IM_Friends", updateMap, "b_id"); + } catch (Exception e) { + System.err.println("[IM][PRIVATE] 更新好友状态失败: " + e.getMessage()); + } + } + + /** + * 推送好友更新到客户端 + */ + private void pushFriendUpdate(String orgCode, String to, String from) { + try { + if(!UserChannelManager.isOnline(orgCode,to)){ + return; + } + + String friendSql = "SELECT f.*, u.b_name AS v_frienduser_name, " + + "m.b_content AS v_last_message, m.b_send_time AS v_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 = '" + to + "' AND f.b_frienduser_id = '" + from + "'"; + + Map friendRecord = DbUtils.querySingle(orgCode, friendSql); + if (CommonUtils.isEmpty(friendRecord)) return; + + // 在线状态 + boolean isOnline = UserChannelManager.isOnline(orgCode, from); + friendRecord.put("is_online", isOnline ? 1 : 0); + + ImResponse imResponse = ImResponse.success(); + imResponse.setType("friend_update"); + imResponse.setFrom(from); + imResponse.setData(friendRecord); + + WriteManager.writeToUser(orgCode, to, imResponse); + } catch (Exception e) { + System.err.println("[IM][FRIEND_UPDATE] 推送异常: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void handleChatGroup(ChannelHandlerContext ctx, ImRequest request) { + String orgId = request.getOrgId(); + String from = request.getFrom(); + Long to = Long.valueOf(request.getTo().toString()); + long timestamp = request.getTimestamp(); + String _tempId = ""; + + try { + // 将请求数据转为 Map + Map reqData = CommonUtils.objectToMap(request.getData()); + String content = reqData.get("content").toString(); + String messageType = reqData.get("messageType").toString(); + _tempId = reqData.get("_tempId").toString(); + + // 生成消息ID并保存消息 + long messageId = SnowflakeId.nextId(); + Map messageSaveMap = new HashMap<>(); + messageSaveMap.put("b_id", messageId); + messageSaveMap.put("b_sender_id", from); + messageSaveMap.put("b_group_id", to); + messageSaveMap.put("b_content", content); + messageSaveMap.put("b_message_type", messageType); + messageSaveMap.put("b_send_time", timestamp); + + DbUtils.insertOne(orgId, "IM_GroupMessages", messageSaveMap, "b_id"); + + // 更新群表的最后消息ID + Map groupUpdateMap = new HashMap<>(); + groupUpdateMap.put("b_id", to); + groupUpdateMap.put("b_last_message_id", messageId); + DbUtils.updateOne(orgId, "IM_Groups", groupUpdateMap, "b_id"); + + // 查询群成员 + String groupMembersSql = "SELECT b_id,b_member_id, b_unread_count FROM IM_GroupMembers WHERE b_group_id = '" + to + "'"; + List> groupMembers = DbUtils.queryForList(orgId, groupMembersSql); + + List> updateGroupMembersMap = new ArrayList<>(); + List onlineMemberIds = new ArrayList<>(); + + // 遍历群成员,构建更新数据并收集在线成员 + for (Map groupMember : groupMembers) { + String bMemberId = groupMember.get("b_member_id").toString(); + String bId = groupMember.get("b_id").toString(); + + Map map = new HashMap<>(); + map.put("b_id", bId); + + if (bMemberId.equals(from)) { + // 发送者自己,更新活跃时间 + map.put("b_last_active_time", CommonUtils.formatTimestamp(timestamp)); + } else { + // 非发送者,未读+1 + int unreadCount = CommonUtils.toInt(groupMember.get("b_unread_count")); + map.put("b_unread_count", unreadCount + 1); + } + + updateGroupMembersMap.add(map); + + // 收集在线成员ID + if (UserChannelManager.isOnline(orgId, bMemberId)) { + onlineMemberIds.add(bMemberId); + } + } + + // 批量更新群成员表 + DbUtils.updateBatch(orgId, "IM_GroupMembers", updateGroupMembersMap, "b_id"); + + // 设置消息数据,保留临时ID + messageSaveMap.put("_tempId", _tempId); + String userSql = "select b_name from b_user where b_id = '" + from + "'"; + Map querySingle = DbUtils.querySingle(orgId, userSql); + + messageSaveMap.put("v_sender_name",querySingle.getOrDefault("b_name","")); + ImResponse success = ImResponse.success(); + success.setType("group"); + success.setFrom(from); + success.setData(messageSaveMap); + + // 广播给在线成员 + if (!onlineMemberIds.isEmpty()) { + WriteManager.broadcastToUsers(orgId, onlineMemberIds, success); + } + + } catch (Exception e) { + System.err.println("[IM][GROUP] 消息处理异常: " + e.getMessage()); + e.printStackTrace(); + + // 错误响应返回给发送者 + ImResponse error = ImResponse.error("消息发送失败: " + e.getMessage()); + error.setType("group"); + error.setFrom(from); + Map data = new HashMap<>(); + data.put("_tempId", _tempId); + error.setData(data); + + WriteManager.writeToUser(orgId, from, error); + } } } diff --git a/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java b/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java index c1b6f1b2..5bdce05a 100644 --- a/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java +++ b/code/link/link-backend/src/main/java/com/link/im/handler/ImWebSocketHandler.java @@ -2,6 +2,7 @@ package com.link.im.handler; import com.fasterxml.jackson.databind.ObjectMapper; import com.link.im.manager.UserChannelManager; +import com.link.im.manager.UserSession; import com.link.im.manager.WriteManager; import com.link.im.model.ImRequest; import com.link.im.model.ImResponse; @@ -12,12 +13,17 @@ import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.timeout.IdleStateEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.annotation.Resource; @ChannelHandler.Sharable public class ImWebSocketHandler extends SimpleChannelInboundHandler { + private final ImMessageProcessor imMessageProcessor = new ImMessageProcessor(); + private static final Logger logger = LoggerFactory.getLogger(ImWebSocketHandler.class); private static final ObjectMapper MAPPER = new ObjectMapper(); - private final ImMessageProcessor imMessageProcessor = new ImMessageProcessor(); @Override public void handlerAdded(ChannelHandlerContext ctx) { @@ -26,10 +32,17 @@ public class ImWebSocketHandler extends SimpleChannelInboundHandler userChannelMap = new ConcurrentHashMap<>(); + // orgId -> (userId -> UserSession) + private static final ConcurrentHashMap> sessions = + new ConcurrentHashMap<>(); - /** - * 添加用户与Channel绑定,如果已有绑定,先移除旧的 - */ - public static void addUser(String userId, Channel channel) { - if (userId == null || channel == null) return; + // ----------------- 添加用户 ----------------- + public static void addUser(String orgId, String userId, Channel channel) { + if (orgId == null || userId == null || channel == null) return; - // 如果已有旧的 channel,先移除 - Channel oldChannel = userChannelMap.put(userId, channel); - - if (oldChannel != null && oldChannel != channel) { - try { - if (oldChannel.isActive()) { - oldChannel.close(); - } - logger.info("用户 {} 已有旧 channel,被替换: oldChannelId={}, newChannelId={}", - userId, oldChannel.id().asShortText(), channel.id().asShortText()); - } catch (Exception e) { - logger.error("关闭旧 channel 异常: userId={}", userId, e); + ConcurrentHashMap orgMap = sessions.get(orgId); + if (orgMap == null) { + orgMap = new ConcurrentHashMap<>(); + ConcurrentHashMap prev = sessions.putIfAbsent(orgId, orgMap); + if (prev != null) { + orgMap = prev; } } - logger.info("用户上线: userId={}, channelId={}", userId, channel.id().asShortText()); + UserSession session = orgMap.get(userId); + if (session == null) { + session = new UserSession(userId, orgId); + UserSession prev = orgMap.putIfAbsent(userId, session); + if (prev != null) { + session = prev; + } + } + + session.getChannels().add(channel); + logger.info("用户上线: orgId={}, userId={}, channelId={}", orgId, userId, channel.id().asShortText()); } - /** - * 移除用户绑定 - */ - public static void removeUserById(String userId) { - if (userId == null) return; + // ----------------- 移除用户 ----------------- + public static void removeUserById(String orgId, String userId) { + if (orgId == null || userId == null) return; - Channel removed = userChannelMap.remove(userId); - if (removed != null) { - try { - if (removed.isActive()) { - removed.close(); - } - } catch (Exception e) { - logger.error("关闭 channel 异常: userId={}", userId, e); + ConcurrentHashMap orgMap = sessions.get(orgId); + if (orgMap == null) return; + + UserSession session = orgMap.remove(userId); + if (session != null) { + for (Channel ch : session.getChannels()) { + try { if (ch.isActive()) ch.close(); } catch (Exception e) { logger.error("关闭 channel 异常", e); } + logger.info("用户下线: orgId={}, userId={}, channelId={}", orgId, userId, ch.id().asShortText()); } - logger.info("用户下线: userId={}, channelId={}", userId, removed.id().asShortText()); + } + + if (orgMap.isEmpty()) { + sessions.remove(orgId); } } - /** - * 根据 Channel 移除对应用户绑定 - */ + // ----------------- 根据 Channel 移除 ----------------- public static void removeUserByChannel(Channel channel) { if (channel == null) return; - userChannelMap.entrySet().removeIf(entry -> { - boolean match = entry.getValue().equals(channel); - if (match) { - logger.info("用户下线: userId={}, channelId={}", entry.getKey(), channel.id().asShortText()); + for (String orgId : sessions.keySet()) { + ConcurrentHashMap orgMap = sessions.get(orgId); + if (orgMap == null) continue; + + for (String userId : orgMap.keySet()) { + UserSession session = orgMap.get(userId); + if (session != null && session.getChannels().remove(channel)) { + logger.info("用户下线: orgId={}, userId={}, channelId={}", orgId, userId, channel.id().asShortText()); + } + if (session != null && session.getChannels().isEmpty()) { + orgMap.remove(userId); + } } - return match; - }); + + if (orgMap.isEmpty()) { + sessions.remove(orgId); + } + } + } + + // ----------------- 获取用户 Channels ----------------- + public static Set getChannels(String orgId, String userId) { + ConcurrentHashMap orgMap = sessions.get(orgId); + if (orgMap == null) return Collections.emptySet(); + + UserSession session = orgMap.get(userId); + return session != null ? Collections.unmodifiableSet(session.getChannels()) : Collections.emptySet(); + } + + // ----------------- 判断是否在线 ----------------- + public static boolean isOnline(String orgId, String userId) { + ConcurrentHashMap orgMap = sessions.get(orgId); + if (orgMap == null) return false; + + UserSession session = orgMap.get(userId); + return session != null && session.getChannels().stream().anyMatch(Channel::isActive); } /** - * 根据 userId 获取 Channel + * 根据 Channel 获取对应的 UserSession */ - public static Channel getChannel(String userId) { - return userChannelMap.get(userId); + public static UserSession getSessionByChannel(Channel channel) { + if (channel == null) return null; + + for (ConcurrentHashMap orgMap : sessions.values()) { + for (UserSession session : orgMap.values()) { + if (session.getChannels().contains(channel)) { + return session; + } + } + } + return null; } - /** - * 判断用户是否在线 - */ - public static boolean isOnline(String userId) { - Channel ch = userChannelMap.get(userId); - return ch != null && ch.isActive(); - } + } diff --git a/code/link/link-backend/src/main/java/com/link/im/manager/UserSession.java b/code/link/link-backend/src/main/java/com/link/im/manager/UserSession.java new file mode 100644 index 00000000..e3ac5fb0 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/manager/UserSession.java @@ -0,0 +1,22 @@ +package com.link.im.manager; + +import io.netty.channel.Channel; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class UserSession { + private final String userId; + private final String orgId; + private final Set channels = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public UserSession(String userId, String orgId) { + this.userId = userId; + this.orgId = orgId; + } + + public String getUserId() { return userId; } + public String getOrgId() { return orgId; } + public Set getChannels() { return channels; } +} diff --git a/code/link/link-backend/src/main/java/com/link/im/manager/WriteManager.java b/code/link/link-backend/src/main/java/com/link/im/manager/WriteManager.java index fd5a4e84..0d50c994 100644 --- a/code/link/link-backend/src/main/java/com/link/im/manager/WriteManager.java +++ b/code/link/link-backend/src/main/java/com/link/im/manager/WriteManager.java @@ -1,12 +1,15 @@ package com.link.im.manager; import com.fasterxml.jackson.databind.ObjectMapper; +import com.link.im.utils.CommonUtils; import io.netty.channel.Channel; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Map; +import java.util.Set; public class WriteManager { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -15,42 +18,61 @@ public class WriteManager { /** * 发送消息 - 根据 userId * 内部会自动判断用户是否在线 + * ✅ 现在会发送到用户的所有活跃 channel */ - public static void writeToUser(String userId, Object message) { + public static void writeToUser(String orgId,String userId, Object message) { if (userId == null || message == null) return; - if (!UserChannelManager.isOnline(userId)) { - // 用户不在线,直接跳过 - return; - } + Set channels = UserChannelManager.getChannels(orgId,userId); + if (channels.isEmpty()) return; - Channel ch = UserChannelManager.getChannel(userId); - writeToChannel(ch, message); + for (Channel ch : channels) { + writeToChannel(ch, message); + } } /** * 发送消息 - 根据 Channel */ public static void writeToChannel(Channel channel, Object message) { - if (channel != null && channel.isActive() && message != null) { - try { - String json = message instanceof String ? (String) message : MAPPER.writeValueAsString(message); - channel.writeAndFlush(new TextWebSocketFrame(json)); - } catch (Exception e) { - logger.error("发送消息失败", e); + if (channel == null || !channel.isActive() || message == null) { + return; + } + + try { + Object payload; + + // 如果 message 是字符串,直接用字符串;否则先转 Map 并加 timestamp + if (message instanceof String) { + payload = message; + } else { + // 将对象转换为 Map + Map map = CommonUtils.objectToMap(message); + map.put("timestamp", System.currentTimeMillis()); + payload = map; } + + // 转 JSON 字符串 + String json = payload instanceof String ? (String) payload : MAPPER.writeValueAsString(payload); + + // 发送 + channel.writeAndFlush(new TextWebSocketFrame(json)); + } catch (Exception e) { + logger.error("发送消息失败", e); } } + /** * 广播消息给指定用户列表 * 内部会自动过滤离线用户 + * ✅ 现在每个用户消息会发送到所有活跃 channel */ - public static void broadcastToUsers(List userIds, Object message) { + public static void broadcastToUsers(String orgId,List userIds, Object message) { if (message == null || userIds == null || userIds.isEmpty()) return; for (String userId : userIds) { - writeToUser(userId, message); // 内部自动判断在线 + writeToUser(orgId,userId, message); // 内部自动处理多 channel } } diff --git a/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java b/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java index 6aad21af..1ee7f0f9 100644 --- a/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java +++ b/code/link/link-backend/src/main/java/com/link/im/model/ImRequest.java @@ -1,24 +1,25 @@ package com.link.im.model; public class ImRequest { + private String orgId; + private String type; private String from; - private String to; - private String content; - private String orgId; + private Object to; private long timestamp; + private Object data; // 无参构造 public ImRequest() {} // 全参构造 - public ImRequest(String type, String from, String to, String content, long timestamp,String orgId) { + public ImRequest(String type, String from, Object to, long timestamp,String orgId,Object data) { this.type = type; this.from = from; this.to = to; - this.content = content; this.timestamp = timestamp; this.orgId = orgId; + this.data = data; } public String getOrgId() { @@ -45,7 +46,7 @@ public class ImRequest { this.from = from; } - public String getTo() { + public Object getTo() { return to; } @@ -53,14 +54,6 @@ public class ImRequest { this.to = to; } - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - public long getTimestamp() { return timestamp; } @@ -69,14 +62,23 @@ public class ImRequest { this.timestamp = timestamp; } + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + @Override public String toString() { return "ImRequest{" + - "type='" + type + '\'' + + "orgId='" + orgId + '\'' + + ", type='" + type + '\'' + ", from='" + from + '\'' + ", to='" + to + '\'' + - ", content='" + content + '\'' + ", timestamp=" + timestamp + + ", data=" + data + '}'; } } \ No newline at end of file diff --git a/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java b/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java index a9efc4c2..6d6ab789 100644 --- a/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java +++ b/code/link/link-backend/src/main/java/com/link/im/model/ImResponse.java @@ -36,6 +36,10 @@ public class ImResponse { return new ImResponse(type, 200, message, System.currentTimeMillis(), from,data); } + public static ImResponse error(String message) { + return new ImResponse("error", 500, message, System.currentTimeMillis(), null,null); + } + public static ImResponse error(String type, String message) { return new ImResponse(type, 500, message, System.currentTimeMillis(), null,null); } diff --git a/code/link/link-backend/src/main/java/com/link/im/model/PageResult.java b/code/link/link-backend/src/main/java/com/link/im/model/PageResult.java new file mode 100644 index 00000000..6741a8ca --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/model/PageResult.java @@ -0,0 +1,33 @@ +package com.link.im.model; + +import java.util.List; +import java.util.Map; + +public class PageResult { + private int total; + private List> records; + + public PageResult() { + } + + public PageResult(int total, List> records) { + this.total = total; + this.records = records; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List> getRecords() { + return records; + } + + public void setRecords(List> records) { + this.records = records; + } +} diff --git a/code/link/link-backend/src/main/java/com/link/im/service/ImService.java b/code/link/link-backend/src/main/java/com/link/im/service/ImService.java new file mode 100644 index 00000000..7bea559a --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/service/ImService.java @@ -0,0 +1,52 @@ +package com.link.im.service; + +import com.link.im.handler.ImWebSocketHandler; +import com.link.im.manager.UserChannelManager; +import com.link.im.manager.WriteManager; +import com.link.im.model.ImResponse; +import com.link.im.utils.CommonUtils; +import com.link.im.utils.DbUtils; +import com.link.im.utils.SnowflakeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class ImService { + private static final Logger logger = LoggerFactory.getLogger(ImService.class); + + public int ensureFriend(String orgCode, String userId, String friendId) throws SQLException { + if (CommonUtils.isEmpty(userId) || CommonUtils.isEmpty(friendId)) { + System.out.println("确保好友关系存在失败,userId或friendId不能为空。"); + return 0; + } + + // 查询好友关系是否已存在 + String sql = String.format( + "SELECT * FROM IM_Friends WHERE b_user_id = '%s' AND b_frienduser_id = '%s'", + userId, friendId + ); + Map friendRecord = DbUtils.querySingle(orgCode, sql); + + // 已存在直接返回 + if (CommonUtils.isNotEmpty(friendRecord)) { + return 0; + } + + // 不存在则插入 + Map insertMap = new HashMap<>(); + long newId = SnowflakeId.nextId(); + insertMap.put("b_id", newId); + insertMap.put("b_user_id", userId); + insertMap.put("b_frienduser_id", friendId); + insertMap.put("b_create_time", CommonUtils.currentDateTime()); + insertMap.put("b_unread_count", 0); + + return DbUtils.insertOne(orgCode, "IM_Friends", insertMap, "b_id"); + } + +} diff --git a/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java b/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java index 215819a4..3649e019 100644 --- a/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java +++ b/code/link/link-backend/src/main/java/com/link/im/utils/CommonUtils.java @@ -1,28 +1,80 @@ package com.link.im.utils; -import java.util.HashMap; -import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.lang.reflect.Array; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; public class CommonUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + /** - * 判断字符串是否为空(null 或 "") - * @param str 待判断的字符串 - * @return true 如果字符串为 null 或 空串 + * 判断 map 中指定字段是否有空值,若有则直接抛异常 + * + * @param map 待检查的 Map + * @param keys 需要检查的字段名,可变参数 + * @throws IllegalArgumentException 如果有字段为空 */ - public static boolean isEmpty(String str) { - return str == null || str.trim().isEmpty(); + public static void requireFields(Map map, String... keys) { + if (map == null) { + throw new IllegalArgumentException("参数不能为空"); + } + if (keys == null || keys.length == 0) { + throw new IllegalArgumentException("需要检查的字段不能为空"); + } + + List emptyFields = new ArrayList<>(); + for (String key : keys) { + Object value = map.get(key); + if (value == null || (value instanceof String && ((String) value).trim().isEmpty())) { + emptyFields.add(key); + } + } + + if (!emptyFields.isEmpty()) { + throw new IllegalArgumentException(String.join(", ", emptyFields) + " 不能为空"); + } + } + /** + * 判断对象是否为空: + * @param obj 待判断对象 + * @return true 如果为空 + */ + public static boolean isEmpty(Object obj) { + if (obj == null) { + return true; + } + if (obj instanceof String) { + return ((String) obj).trim().isEmpty(); + } + if (obj instanceof Collection) { + return ((Collection) obj).isEmpty(); + } + if (obj instanceof Map) { + return ((Map) obj).isEmpty(); + } + if (obj.getClass().isArray()) { + return Array.getLength(obj) == 0; + } + // 其他类型认为非空 + return false; } /** - * 判断字符串是否非空(不是 null 且不为空串) - * @param str 待判断的字符串 - * @return true 如果字符串非空 + * 判断对象是否非空 + * @param obj 待判断对象 + * @return true 如果非空 */ - public static boolean isNotEmpty(String str) { - return !isEmpty(str); + public static boolean isNotEmpty(Object obj) { + return !isEmpty(obj); } - - /** * 快速创建 Map,类似 JDK9 的 Map.of() * 用法示例: @@ -46,4 +98,69 @@ public class CommonUtils { } return map; } + + // 获取当前时间字符串(默认格式 yyyy-MM-dd HH:mm:ss) + public static String currentDateTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } + + public static String formatTimestamp(long timestampMillis) { + return LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestampMillis), + ZoneId.systemDefault() + ).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + /** + * 将 Object 转为 Map + * 支持原生 Map 或 JSON 对象 + * + * @param obj 待转换对象 + * @return Map + * @throws IllegalArgumentException 如果无法转换 + */ + public static Map objectToMap(Object obj) { + if (obj == null) { + return null; + } + + // 如果已经是 Map 类型,直接返回 + if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) obj; + return map; + } + + // 如果不是 Map,可以尝试用 Jackson 转换 + try { + return objectMapper.convertValue(obj, new TypeReference>() {}); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("对象无法转换为 Map: " + obj.getClass(), e); + } + } + + public static int toInt(Object obj) { + if (obj == null) { + return 0; + } + if (obj instanceof Integer) { + return (Integer) obj; + } + if (obj instanceof Long) { + return ((Long) obj).intValue(); + } + if (obj instanceof String) { + try { + return Integer.parseInt(((String) obj).trim()); + } catch (NumberFormatException e) { + return 0; + } + } + try { + return Integer.parseInt(obj.toString().trim()); + } catch (Exception e) { + return 0; + } + } + } diff --git a/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java b/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java index dd6564c2..077b7306 100644 --- a/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java +++ b/code/link/link-backend/src/main/java/com/link/im/utils/DbUtils.java @@ -1,7 +1,10 @@ package com.link.im.utils; +import com.fasterxml.jackson.core.JsonProcessingException; import com.link.api.util.DruidDataSourceManager; +import com.link.api.util.Result; +import com.link.im.model.PageResult; import java.sql.*; import java.util.*; @@ -38,4 +41,429 @@ public class DbUtils { return resultList; } + + public static PageResult queryTablePage( + String orgCode, + String tableName, + int page, + int pageSize, + String whereClause, + String orderBy + ) throws SQLException { + page = Math.max(page, 1); + pageSize = Math.max(pageSize, 10); + + // 设置默认排序(使用主键列) + if (orderBy == null || orderBy.trim().isEmpty()) { + orderBy = "b_id desc"; + } + + PageResult pageResult = new PageResult(); + try (Connection conn = DruidDataSourceManager.getConnection(orgCode)) { + + String where = (whereClause != null && !whereClause.trim().isEmpty()) + ? " WHERE " + whereClause + : ""; + + // 1. 查询总数量 + String countSql = "SELECT COUNT(1) AS totalCount FROM " + tableName + where; + int totalCount = 0; + try (PreparedStatement stmt = conn.prepareStatement(countSql); + ResultSet rs = stmt.executeQuery()) { + if (rs.next()) totalCount = rs.getInt("totalCount"); + } + + int startRow = (page - 1) * pageSize + 1; + int endRow = page * pageSize; + + // 2. 分页查询(支持自定义排序) + String pageSql = "SELECT * FROM (" + + "SELECT ROW_NUMBER() OVER (ORDER BY " + orderBy + ") AS row_num, * FROM " + + tableName + where + + ") AS P WHERE row_num BETWEEN ? AND ?"; + + List> dataList = new ArrayList<>(); + try (PreparedStatement stmt = conn.prepareStatement(pageSql)) { + stmt.setInt(1, startRow); + stmt.setInt(2, endRow); + + try (ResultSet rs = stmt.executeQuery()) { + ResultSetMetaData meta = rs.getMetaData(); + int colCount = meta.getColumnCount(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= colCount; i++) { + String columnName = meta.getColumnLabel(i); + // 跳过 row_num 列 + if ("row_num".equalsIgnoreCase(columnName)) { + continue; + } + row.put(columnName, rs.getObject(i)); + } + dataList.add(row); + } + } + } + + pageResult.setRecords(dataList); + pageResult.setTotal(totalCount); + } + + return pageResult; + } + + /** + * 通用分页查询 + * + * @param orgCode 数据源组织编码 + * @param sql 原始 SQL(不带分页) + * @param orderBy 排序字段,例如 "b_SendTime DESC",可为空,空则用第一个字段 + * @param page 页码,从1开始 + * @param pageSize 每页大小 + * @return Map 包含分页数据和总数量 + */ + public static PageResult queryForPage(String orgCode, String sql, String orderBy, int page, int pageSize) + throws SQLException { + page = Math.max(page, 1); + pageSize = Math.max(pageSize, 10); + + PageResult pageResult = new PageResult(); + try (Connection conn = DruidDataSourceManager.getConnection(orgCode)) { + + // 1. 查询总数量 + String countSql = "SELECT COUNT(1) AS totalCount FROM (" + sql + ") AS T"; + int totalCount = 0; + try (PreparedStatement countStmt = conn.prepareStatement(countSql); + ResultSet rsCount = countStmt.executeQuery()) { + if (rsCount.next()) totalCount = rsCount.getInt("totalCount"); + } + + // 2. 分页查询 + int startRow = (page - 1) * pageSize + 1; + int endRow = page * pageSize; + + String pageSql = "SELECT * FROM (" + + "SELECT ROW_NUMBER() OVER (ORDER BY " + orderBy + ") AS row_num, * FROM (" + + sql + + ") AS T) AS P WHERE row_num BETWEEN ? AND ?"; + + List> dataList = new ArrayList<>(); + try (PreparedStatement stmt = conn.prepareStatement(pageSql)) { + stmt.setInt(1, startRow); + stmt.setInt(2, endRow); + + try (ResultSet rs = stmt.executeQuery()) { + ResultSetMetaData meta = rs.getMetaData(); + int colCount = meta.getColumnCount(); + + while (rs.next()) { + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= colCount; i++) { + String columnName = meta.getColumnLabel(i); + + if ("row_num".equalsIgnoreCase(columnName)) { + continue; + } + + row.put(columnName, rs.getObject(i)); + } + dataList.add(row); + } + } + } + + pageResult.setRecords(dataList); + pageResult.setTotal(totalCount); + + return pageResult; + } + } + + /** + * 根据 SQL 查询单条记录 + * + * @param orgCode 数据源组织编码 + * @param sql 查询 SQL + * @return 单条记录(Map),没有结果返回 null + */ + public static Map querySingle(String orgCode, String sql) throws SQLException { + try (Connection conn = DruidDataSourceManager.getConnection(orgCode); + PreparedStatement stmt = conn.prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + if (rs.next()) { + Map rowMap = new LinkedHashMap<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnLabel(i); + Object value = rs.getObject(i); + rowMap.put(columnName, value); + } + return rowMap; + } else { + return null; + } + } + } + + /** + * 辅助方法:在已有 Connection 上插入单条数据 + */ + private static int insertOneWithConnection(Connection conn, String tableName, Map data, String primaryKey) throws SQLException { + // 主键检查 + if (!CommonUtils.isEmpty(primaryKey) && data.containsKey(primaryKey)) { + Object pkValue = data.get(primaryKey); + String checkSql = String.format("SELECT 1 FROM %s WHERE %s = ?", tableName, primaryKey); + try (PreparedStatement stmt = conn.prepareStatement(checkSql)) { + stmt.setObject(1, pkValue); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return 0; // 主键已存在 + } + } + } + } + + StringBuilder columns = new StringBuilder(); + StringBuilder placeholders = new StringBuilder(); + List values = new ArrayList<>(); + for (Map.Entry entry : data.entrySet()) { + if (columns.length() > 0) { + columns.append(", "); + placeholders.append(", "); + } + columns.append(entry.getKey()); + placeholders.append("?"); + values.add(entry.getValue()); + } + + String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columns, placeholders); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + stmt.setObject(i + 1, values.get(i)); + } + return stmt.executeUpdate(); + } + } + + /** + * 更新单条记录(根据主键) + * + * @param orgCode 数据源组织编码 + * @param tableName 表名 + * @param data 更新字段和值(必须包含主键字段) + * @param primaryKey 主键列名 + * @return 影响的行数 + */ + public static int updateOne(String orgCode, String tableName, Map data, String primaryKey) throws SQLException { + Connection conn = null; + try { + conn = DruidDataSourceManager.getConnection(orgCode); + conn.setAutoCommit(false); + + Object pkValue = data.get(primaryKey); + StringBuilder sql = new StringBuilder("UPDATE ").append(tableName).append(" SET "); + List values = new ArrayList<>(); + + for (Map.Entry entry : data.entrySet()) { + if (!entry.getKey().equals(primaryKey)) { + sql.append(entry.getKey()).append(" = ?, "); + values.add(entry.getValue()); + } + } + + // 去掉末尾的逗号 + sql.setLength(sql.length() - 2); + sql.append(" WHERE ").append(primaryKey).append(" = ?"); + values.add(pkValue); + + try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) { + for (int i = 0; i < values.size(); i++) { + stmt.setObject(i + 1, values.get(i)); + } + int rows = stmt.executeUpdate(); + conn.commit(); + return rows; + } + + } catch (SQLException e) { + rollbackTransaction(conn); + throw e; + } finally { + closeConnection(conn); + } + } + + + /** + * 批量更新多条记录(根据主键),每条记录字段可以不同 + * + * @param orgCode 数据源组织编码 + * @param tableName 表名 + * @param dataList 多条更新数据(每条必须包含主键字段) + * @param primaryKey 主键列名 + * @return 成功更新的行数 + */ + public static int updateBatch(String orgCode, String tableName, List> dataList, String primaryKey) throws SQLException { + if (dataList == null || dataList.isEmpty()) { + return 0; + } + + int total = 0; + for (Map data : dataList) { + total += updateOne(orgCode, tableName, data, primaryKey); + } + return total; + } + + public static int insertOne(String orgCode, String tableName, Map data, String primaryKey) throws SQLException { + Connection conn = null; + try { + conn = DruidDataSourceManager.getConnection(orgCode); + conn.setAutoCommit(false); + + // 主键检查 + if (!CommonUtils.isEmpty(primaryKey) && data.containsKey(primaryKey)) { + Object pkValue = data.get(primaryKey); + String checkSql = String.format("SELECT 1 FROM %s WHERE %s = ?", tableName, primaryKey); + try (PreparedStatement stmt = conn.prepareStatement(checkSql)) { + stmt.setObject(1, pkValue); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + conn.commit(); // 主键已存在,直接提交事务 + return 0; + } + } + } + } + + // 构建插入 SQL + StringBuilder columns = new StringBuilder(); + StringBuilder placeholders = new StringBuilder(); + List values = new ArrayList<>(); + for (Map.Entry entry : data.entrySet()) { + if (columns.length() > 0) { + columns.append(", "); + placeholders.append(", "); + } + columns.append(entry.getKey()); + placeholders.append("?"); + values.add(entry.getValue()); + } + + String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columns, placeholders); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + stmt.setObject(i + 1, values.get(i)); + } + int rows = stmt.executeUpdate(); + conn.commit(); + return rows; + } + + } catch (SQLException e) { + rollbackTransaction(conn); + throw e; + } finally { + closeConnection(conn); + } + } + public static int insertBatch(String orgCode, String tableName, List> dataList, String primaryKey) throws SQLException { + if (dataList == null || dataList.isEmpty()) { + return 0; + } + + Connection conn = null; + int total = 0; + try { + conn = DruidDataSourceManager.getConnection(orgCode); + conn.setAutoCommit(false); + + List> filteredList = new ArrayList<>(); + + // 主键过滤 + if (!CommonUtils.isEmpty(primaryKey)) { + Set existingKeys = new HashSet<>(); + String inClause = String.join(",", Collections.nCopies(dataList.size(), "?")); + String checkSql = String.format("SELECT %s FROM %s WHERE %s IN (%s)", primaryKey, tableName, primaryKey, inClause); + try (PreparedStatement stmt = conn.prepareStatement(checkSql)) { + int idx = 1; + for (Map data : dataList) { + stmt.setObject(idx++, data.get(primaryKey)); + } + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + existingKeys.add(rs.getObject(1)); + } + } + } + + for (Map data : dataList) { + if (!existingKeys.contains(data.get(primaryKey))) { + filteredList.add(data); + } + } + } else { + filteredList.addAll(dataList); + } + + if (filteredList.isEmpty()) { + conn.commit(); + return 0; + } + + // 构建 SQL + Set columnsSet = filteredList.get(0).keySet(); + String columns = String.join(", ", columnsSet); + String placeholders = String.join(", ", Collections.nCopies(columnsSet.size(), "?")); + String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columns, placeholders); + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + for (Map data : filteredList) { + int idx = 1; + for (String col : columnsSet) { + stmt.setObject(idx++, data.get(col)); + } + stmt.addBatch(); + } + + int[] results = stmt.executeBatch(); + for (int r : results) total += r; + } + + conn.commit(); + return total; + + } catch (SQLException e) { + rollbackTransaction(conn); + throw e; + } finally { + closeConnection(conn); + } + } + + // 事务回滚 + private static void rollbackTransaction(Connection conn) { + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + System.err.println("回滚事务时出错: " + ex.getMessage()); + } + } + } + + // 关闭连接 + private static void closeConnection(Connection conn) { + if (conn != null) { + try { + conn.setAutoCommit(true); + conn.close(); + } catch (SQLException e) { + System.err.println("关闭数据库连接时出错: " + e.getMessage()); + } + } + } } diff --git a/code/link/link-backend/src/main/java/com/link/im/utils/SnowflakeId.java b/code/link/link-backend/src/main/java/com/link/im/utils/SnowflakeId.java new file mode 100644 index 00000000..b63686e5 --- /dev/null +++ b/code/link/link-backend/src/main/java/com/link/im/utils/SnowflakeId.java @@ -0,0 +1,75 @@ +package com.link.im.utils; + +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.concurrent.ThreadLocalRandom; + +public class SnowflakeId { + + private static final long MACHINE_BITS = 3L; // 机器ID占3位 + private static final long SEQUENCE_BITS = 7L; // 序列号占7位 + private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_BITS); + private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); + + private static final long MACHINE_SHIFT = SEQUENCE_BITS; + private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS; + + // 自定义纪元,可换成系统上线时间 + private static final long EPOCH = 1698796800000L; + + private static final long machineId; // 静态机器ID + private static long lastTimestamp = -1L; + private static long sequence = 0L; + + static { + machineId = getMachineId() & MAX_MACHINE_ID; + } + + private SnowflakeId() { + } // 私有化构造,防止实例化 + + /** + * 生成唯一ID,返回 long 类型,64位 + */ + public static synchronized long nextId() { + long timestamp = System.currentTimeMillis(); + + if (timestamp < lastTimestamp) { + throw new RuntimeException("系统时钟回退,拒绝生成ID"); + } + + if (timestamp == lastTimestamp) { + sequence = (sequence + 1) & MAX_SEQUENCE; + if (sequence == 0) { + timestamp = waitNextMillis(timestamp); + } + } else { + sequence = 0L; + } + + lastTimestamp = timestamp; + + return ((timestamp - EPOCH) << TIMESTAMP_SHIFT) | (machineId << MACHINE_SHIFT) | sequence; + } + + private static long waitNextMillis(long timestamp) { + while (timestamp <= lastTimestamp) { + timestamp = System.currentTimeMillis(); + } + return timestamp; + } + + private static long getMachineId() { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + int hash = 0; + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + hash += ni.toString().hashCode(); + } + return hash & 0x7; // 取低3位 + } catch (Exception e) { + return ThreadLocalRandom.current().nextLong(0, MAX_MACHINE_ID + 1); + } + } +} diff --git a/code/link/link-backend/src/main/resources/application.properties b/code/link/link-backend/src/main/resources/application.properties index 8fa00146..f24f4eb9 100644 --- a/code/link/link-backend/src/main/resources/application.properties +++ b/code/link/link-backend/src/main/resources/application.properties @@ -15,3 +15,6 @@ logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG logging.level.org.springframework.jdbc.core.StatementCreatorUtils=TRACE logging.level.p6spy=DEBUG logging.level.com.p6spy=DEBUG + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=Asia/Shanghai \ No newline at end of file diff --git a/code/link/link-backend/target/classes/application.properties b/code/link/link-backend/target/classes/application.properties index 8fa00146..f24f4eb9 100644 --- a/code/link/link-backend/target/classes/application.properties +++ b/code/link/link-backend/target/classes/application.properties @@ -15,3 +15,6 @@ logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG logging.level.org.springframework.jdbc.core.StatementCreatorUtils=TRACE logging.level.p6spy=DEBUG logging.level.com.p6spy=DEBUG + +spring.jackson.date-format=yyyy-MM-dd HH:mm:ss +spring.jackson.time-zone=Asia/Shanghai \ No newline at end of file diff --git a/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class b/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class index d5435fac..8b8d1188 100644 Binary files a/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class and b/code/link/link-backend/target/classes/com/link/api/util/PaginationUtils.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class b/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class deleted file mode 100644 index 69d3e163..00000000 Binary files a/code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class and /dev/null differ diff --git a/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class b/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class index b1ccb5c8..4b33f48f 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class and b/code/link/link-backend/target/classes/com/link/im/handler/ImMessageProcessor.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class b/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class index 501ac092..da839d53 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class and b/code/link/link-backend/target/classes/com/link/im/handler/ImWebSocketHandler.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class b/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class index 2281d39a..a131a5d2 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class and b/code/link/link-backend/target/classes/com/link/im/manager/UserChannelManager.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/manager/WriteManager.class b/code/link/link-backend/target/classes/com/link/im/manager/WriteManager.class index 2e4abb93..9fd3d087 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/manager/WriteManager.class and b/code/link/link-backend/target/classes/com/link/im/manager/WriteManager.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class b/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class index c364c427..8cb9362f 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class and b/code/link/link-backend/target/classes/com/link/im/model/ImRequest.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class b/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class index 0a434fe6..1a3eb84e 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class and b/code/link/link-backend/target/classes/com/link/im/model/ImResponse.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class b/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class index 3adb0441..ba8b6df6 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class and b/code/link/link-backend/target/classes/com/link/im/utils/CommonUtils.class differ diff --git a/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class b/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class index ff384498..3bdf50c9 100644 Binary files a/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class and b/code/link/link-backend/target/classes/com/link/im/utils/DbUtils.class differ diff --git a/code/link/link-frontend/src/App.vue b/code/link/link-frontend/src/App.vue index 063b1316..3717eb07 100644 --- a/code/link/link-frontend/src/App.vue +++ b/code/link/link-frontend/src/App.vue @@ -1,10 +1,15 @@