From a09fdf0a6644915ebcef1ad21565235fe86d574f Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Sat, 27 Dec 2025 17:21:38 +0800 Subject: [PATCH] u --- code/link/link-backend/.gitignore | 38 + code/link/link-backend/.idea/.gitignore | 8 - code/link/link-backend/.idea/compiler.xml | 18 - code/link/link-backend/.idea/encodings.xml | 6 - .../link-backend/.idea/jarRepositories.xml | 35 - code/link/link-backend/.idea/misc.xml | 12 - .../link-backend/.idea/runConfigurations.xml | 10 - code/link/link-backend/.idea/vcs.xml | 7 - code/link/link-backend/pom.xml | 2 +- .../java/com/link/api/util/BatchColumn.java | 2 + .../com/link/api/util/PaginationUtils.java | 2 - .../link/im/controller/CompanyController.java | 47 - .../com/link/im/controller/ImController.java | 409 +++++++ .../link/im/handler/ImMessageProcessor.java | 343 +++++- .../link/im/handler/ImWebSocketHandler.java | 21 +- .../link/im/manager/UserChannelManager.java | 141 ++- .../java/com/link/im/manager/UserSession.java | 22 + .../com/link/im/manager/WriteManager.java | 52 +- .../java/com/link/im/model/ImRequest.java | 34 +- .../java/com/link/im/model/ImResponse.java | 4 + .../java/com/link/im/model/PageResult.java | 33 + .../java/com/link/im/service/ImService.java | 52 + .../java/com/link/im/utils/CommonUtils.java | 145 ++- .../main/java/com/link/im/utils/DbUtils.java | 428 +++++++ .../java/com/link/im/utils/SnowflakeId.java | 75 ++ .../src/main/resources/application.properties | 3 + .../target/classes/application.properties | 3 + .../com/link/api/util/PaginationUtils.class | Bin 20185 -> 20185 bytes .../im/controller/CompanyController.class | Bin 2151 -> 0 bytes .../link/im/handler/ImMessageProcessor.class | Bin 4691 -> 12755 bytes .../link/im/handler/ImWebSocketHandler.class | Bin 4343 -> 4624 bytes .../link/im/manager/UserChannelManager.class | Bin 3991 -> 5605 bytes .../com/link/im/manager/WriteManager.class | Bin 2915 -> 3740 bytes .../classes/com/link/im/model/ImRequest.class | Bin 2320 -> 2460 bytes .../com/link/im/model/ImResponse.class | Bin 3015 -> 3157 bytes .../com/link/im/utils/CommonUtils.class | Bin 1563 -> 5272 bytes .../classes/com/link/im/utils/DbUtils.class | Bin 3108 -> 20966 bytes code/link/link-frontend/src/App.vue | 7 +- .../src/hooks/useScrollWatcher.js | 94 ++ .../src/stores/websocketStore.js | 7 +- .../link-frontend/src/utils/commonUtils.js | 77 ++ code/link/link-frontend/src/utils/uid.js | 116 ++ .../link-frontend/src/views/home/index.vue | 41 +- .../link/link-frontend/src/views/im/index.vue | 1058 ++++++++++++++--- code/link/link-frontend/test.html | 125 -- code/link/link-frontend/test.js | 57 + code/link56/link56-backend/pom.xml | 9 + .../auth/identity/model/vo/AuthLoginVO.java | 31 +- .../identity/service/AuthIdentityService.java | 27 +- .../cn/link56/base/mapper/AreaMapper.java | 4 +- .../cn/link56/base/mapper/FileMapper.java | 4 +- .../cn/link56/base/mapper/IndustryMapper.java | 4 +- .../base/model/po/{AreaPO.java => Area.java} | 2 +- .../base/model/po/{FilePO.java => File.java} | 4 +- .../po/{IndustryPO.java => Industry.java} | 2 +- .../link56/base/service/BaseDataService.java | 14 +- .../cn/link56/base/service/FileService.java | 10 +- .../common/config/AutoMetaObjectHandler.java | 3 + .../cn/link56/common/utils/TimeUtils.java | 13 + .../link56/company/mapper/CompanyMapper.java | 4 +- .../company/mapper/CompanyUserMapper.java | 4 +- .../model/dto/UpdateCompanyUserDTO.java | 2 + .../model/po/{CompanyPO.java => Company.java} | 2 +- .../{CompanyUserPO.java => CompanyUser.java} | 7 +- .../company/model/vo/QueryCompanyUserVO.java | 1 + .../company/service/CompanyService.java | 73 +- .../company/service/CompanyUserService.java | 135 ++- .../main/java/cn/link56/im/NettyServer.java | 78 ++ .../java/cn/link56/im/config/NettyConfig.java | 21 + .../MessageResponseStatusConstants.java | 18 + .../cn/link56/im/enums/MessageTypeEnums.java | 7 + .../cn/link56/im/handler/MessageHandler.java | 62 + .../cn/link56/im/handler/ServerHandler.java | 50 + .../cn/link56/im/message/MessageRequest.java | 16 + .../cn/link56/im/message/MessageResponse.java | 50 + .../java/cn/link56/im/storage/Session.java | 41 + .../cn/link56/im/storage/SessionManager.java | 51 + .../cn/link56/im/utils/MessageWriter.java | 58 + .../post/controller/PostViewController.java | 9 + .../post/mapper/PostFavoriteMapper.java | 9 - .../cn/link56/post/mapper/PostLikeMapper.java | 9 - .../cn/link56/post/mapper/PostStatMapper.java | 15 + .../post/model/dto/CreatePostCommentDTO.java | 1 + .../link56/post/model/dto/CreatePostDTO.java | 2 - .../cn/link56/post/model/po/PostComment.java | 15 +- .../cn/link56/post/model/po/PostLike.java | 30 - .../po/{PostFavorite.java => PostStat.java} | 20 +- .../post/model/vo/EditorDashboardVO.java | 2 - .../link56/post/model/vo/PostCommentVO.java | 1 + .../link56/post/model/vo/PostCompanyVO.java | 2 + .../post/service/PostCommentService.java | 147 ++- .../post/service/PostFavoriteService.java | 14 - .../link56/post/service/PostLikeService.java | 14 - .../cn/link56/post/service/PostService.java | 32 +- .../link56/post/service/PostStatService.java | 42 + .../link56/post/service/PostViewService.java | 9 + .../src/main/resources/application-dev.yml | 3 + .../src/main/resources/application.yml | 7 +- .../cn/link56/base/mapper/AreaMapper.xml | 2 +- .../link56/post/mapper/PostCommentMapper.xml | 7 +- .../cn/link56/post/mapper/PostMapper.xml | 24 +- .../cn/link56/post/mapper/PostStatMapper.xml | 33 + code/link56/link56-frontend-new/src/App.vue | 13 +- .../src/components/EmojiPopover.vue | 2 +- .../src/components/QueryCompany.vue | 2 +- .../src/components/SafeImage.vue | 74 ++ .../src/layout/components/header.vue | 9 +- .../src/request/api/post/index.ts | 2 +- .../link56-frontend-new/src/request/index.ts | 8 +- .../link56-frontend-new/src/router/index.ts | 2 +- .../link56-frontend-new/src/store/common.ts | 45 +- .../link56-frontend-new/src/store/user.ts | 114 +- .../src/types/auth/index.ts | 21 +- .../src/types/company/user.ts | 5 + .../src/types/post/index.ts | 7 +- .../link56-frontend-new/src/utils/time.ts | 69 ++ .../src/views/auth/login.vue | 15 +- .../company/{detail/index.vue => detail.vue} | 6 +- .../src/views/company/index.vue | 32 +- .../post/detail/components/comment-list.vue | 198 ++- .../src/views/post/detail/index.vue | 182 +-- .../src/views/post/home/index.vue | 6 +- .../src/views/settings/creator/post.vue | 4 +- .../src/views/settings/index.vue | 2 +- .../src/views/settings/info/company.vue | 11 +- .../info/user/components/EditDrawer.vue | 47 +- .../src/views/settings/info/user/index.vue | 11 +- 127 files changed, 4518 insertions(+), 1159 deletions(-) create mode 100644 code/link/link-backend/.gitignore delete mode 100644 code/link/link-backend/.idea/.gitignore delete mode 100644 code/link/link-backend/.idea/compiler.xml delete mode 100644 code/link/link-backend/.idea/encodings.xml delete mode 100644 code/link/link-backend/.idea/jarRepositories.xml delete mode 100644 code/link/link-backend/.idea/misc.xml delete mode 100644 code/link/link-backend/.idea/runConfigurations.xml delete mode 100644 code/link/link-backend/.idea/vcs.xml delete mode 100644 code/link/link-backend/src/main/java/com/link/im/controller/CompanyController.java create mode 100644 code/link/link-backend/src/main/java/com/link/im/controller/ImController.java create mode 100644 code/link/link-backend/src/main/java/com/link/im/manager/UserSession.java create mode 100644 code/link/link-backend/src/main/java/com/link/im/model/PageResult.java create mode 100644 code/link/link-backend/src/main/java/com/link/im/service/ImService.java create mode 100644 code/link/link-backend/src/main/java/com/link/im/utils/SnowflakeId.java delete mode 100644 code/link/link-backend/target/classes/com/link/im/controller/CompanyController.class create mode 100644 code/link/link-frontend/src/hooks/useScrollWatcher.js create mode 100644 code/link/link-frontend/src/utils/commonUtils.js create mode 100644 code/link/link-frontend/src/utils/uid.js delete mode 100644 code/link/link-frontend/test.html create mode 100644 code/link/link-frontend/test.js rename code/link56/link56-backend/src/main/java/cn/link56/base/model/po/{AreaPO.java => Area.java} (94%) rename code/link56/link56-backend/src/main/java/cn/link56/base/model/po/{FilePO.java => File.java} (92%) rename code/link56/link56-backend/src/main/java/cn/link56/base/model/po/{IndustryPO.java => Industry.java} (96%) create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/common/utils/TimeUtils.java rename code/link56/link56-backend/src/main/java/cn/link56/company/model/po/{CompanyPO.java => Company.java} (98%) rename code/link56/link56-backend/src/main/java/cn/link56/company/model/po/{CompanyUserPO.java => CompanyUser.java} (90%) create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/NettyServer.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/config/NettyConfig.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/constants/MessageResponseStatusConstants.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/enums/MessageTypeEnums.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/handler/MessageHandler.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/handler/ServerHandler.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageRequest.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/message/MessageResponse.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/storage/Session.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/storage/SessionManager.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/im/utils/MessageWriter.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/controller/PostViewController.java delete mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/mapper/PostFavoriteMapper.java delete mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/mapper/PostLikeMapper.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/mapper/PostStatMapper.java delete mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/model/po/PostLike.java rename code/link56/link56-backend/src/main/java/cn/link56/post/model/po/{PostFavorite.java => PostStat.java} (61%) delete mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/service/PostFavoriteService.java delete mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/service/PostLikeService.java create mode 100644 code/link56/link56-backend/src/main/java/cn/link56/post/service/PostStatService.java create mode 100644 code/link56/link56-backend/src/main/resources/cn/link56/post/mapper/PostStatMapper.xml create mode 100644 code/link56/link56-frontend-new/src/components/SafeImage.vue rename code/link56/link56-frontend-new/src/views/company/{detail/index.vue => detail.vue} (98%) 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 d5435facbf9a49a7e37c5773c9cbf40211f7b2ab..8b8d11886cf035aba2e55a3579ebc43c92f08de7 100644 GIT binary patch delta 1014 zcmW;KeN5G57{>AM^*bURYDr1VqBkvGLh&^ni$Zf1HgZWtyet|PG6;n@%ol3K62mP- zEnuMq)t8ns zxi~1LC@3Q*sH8Y}&Awp=?NNJ~l4xE^QG8(q8C0>5YMx*@Pf^1%R`9HCpq?fov^e!S z_uElca+XzGV70=mQIRdBMo&9$vR(EI8l!(sc{r|VEn%9ONGmC9UguR)+}4b8CCL&BK)eC7}KylMocFe3*w0di8hzt{%>Yobl>!{bqvNFlk zROVdi>UQdJHl|%%2DO>c!oljvxHifQo~cMV5P0|inE3$ z>Pa|v+5y&amG%5ZgHqU_GFw57mN_@uJ+_OD;qMdYCB+*_VH2a+MmpP>NHdx2;BH#T zWfyt0lFx4D+G1Yyv9I~yy)@9~UF~dTKgXRvw3m64Ya9?9)NtNb)K=1}YUftlX3x;U zT3`diyf>TY!c$pmli~?R@(y=!jPWivnfEB-1LnJIEnV)W+sB`Ce;52~zk9sMY1_^B z{NQ!j9sj|P+~kt77|;wGZf8cL;ZrPi)eDqA$dJ&vSZV>qna zcvop$a2`;aM(B30bY*C)W@?;XvP~MQX6F<3wEZp6_^|!n;>d%U+~PKdYcgY$%>+#$ zTRGgLsXU-*%+Yk_yTwZ7QmvWP`~61UPowhKtXW}ZUS6bhHYqA(l;(KrV@y|xx6h-% z&elRow1`D2<$34LHuMcSsl|NiTk^FY*CbUaOC_4&9JMj~ik5`O@_Pm?Sn9IN7^kPX z%LC3(71M04o^|;e9#JhToY&j8w34G*MW^aqdX1-0r_nAp*7;Gpz&5Bp?0xv}WM9J} z+C-{eCSO~5NZVbi*^Ta?TrD2>c( z=uWk2ie0Qjs?uRCS3+A9s?+O~PU?)u-lO;Rt-Yv^bm+PTek*e}%=9+{hy F{2z;u{Wkyr 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 69d3e1634e82090515a7ef0c8110f370892c5d33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2151 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 AjQ{`u 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 501ac092901dc7ae644a74a79d75f18b20fc9c22..da839d5334b48603859a72926addd0aef864db38 100644 GIT binary patch delta 1870 zcma)*`&U#|6vsbz2Ik(mTm^>&L_l#Aff*Q0#E>#lu}~hBs8MQ-I3V(DKvR2+y)(14 z+e3T5t*BJS1`snXGqYdvpJ=Uq>$lcwty=AKX98ROP_x#Zz0Wy&fA{yh_c^~6AD$n( z^5>azKovilEFz-QVd5gFhSi zRDlmDyT@cN`%E6>A+>y1d5!Bvz@$Jy zKg&Ur=QyPEyuk~q{9#4)h{200($;y&l+2fPUNL!JAwg1KAgb$g?0pZ&XQt6NCNyWM9s?M=Hhy}`4L8#GGnO06WzVlmh2yk+q=?`Y)O z`;A=tVtRh*yS%6KzQqT8sPmDV8r4i zCv;9(j1U(J7N=Fhmz=OjC~s7GV~kszQOl)NTTF0PBYS6fcR17&?&}QI?`e+=42g7$ zB~;mi9)HnS76&+|ksgUg`=dHvtMz%E3l`t-t$o_lWL)Gsi#u6wd-VnG16;Bf>nl9# zXiQ4Yu@CA#@Anp$`9ULaRiz%wXYg^_OF^AUtk>441F*%)Vg;d zVxRN|vpXY0sY$KeyJ|)0W4k;)^I{juW5b!5p^Q3ik-U|Xovz73fvie`h(9d! z(!^Fzkl8j$R#VV7BY`h4j;YBF$ZoE{hMcdaUfAbR#2RiD+}i{(1MLrj zIt@B&uN9HTx)_bB6%uqgrJ{a>^k9Nn38s&buRLF7g4yTEY<4&c|GeWAH;+*0E2-X-&{E=9%J4Z{!>oJ{i31ZAKI44Y^r32OGV{!IjxR~#APzr8X;cmh^r!90=$k8 zx11KzM6-Y(!jc12UUDGHYvFcLpk(T}I&|(RbR>FR&YW6gUsI1o5R}TJVii_h@B;IX zvD6Z^6L@57x^IC!Jgp!mVoEeN9miW92#&IFYCJi@44Jwv$RW89HO?KVcUaF&qE0fb zn`Br=D;rV_({RNHZ@*D%|A)aKS|9`8+S|QO36?4t1TuEWwm7g-LNV_R5VQK z#(ByU1g5HaoGH(AswCf-nX2+{iCNGhpTRxJ$10E^DXo6lzTg-YaSA7>Y-<=L6enL5 p(Hu$1$2?0i^Q9x3W#7dXwmSA+?h~tMY!}-upDBKrH`rF<{R=c|L^1#X delta 1530 zcmZXTTU1nK6vuzxFf-@OnFHdWh}_4);WD5FXhM;QXla6_k`Za5jcIaGK$0$w-K{h) zr%Wx&w4x$4z_=uC6*k{p3 zyGe&d0i8O2zrg{EBD!?OZi61ZdC;WS&+{BI>9a^*se@nOMT?hs+2F8AOcyw!VO}wL zRlB}s@VX_TZy3C3@fJr7-qv{q_8li%HJ|!*)#YBLoIhNLoL*OnvmPYjckCHXm``;A5K$d}8pa%|$*_%>V7evHUw%FHIgD{e9rt=@TD{1%}9z+>ipsj<@Ve3@s+|8?dt06GWc4*e`7FU z^DS4L45Puk$~BvZS?9DHi@kk(r>JuNaaWl(53*7bc5bM!X)tIr#IR$S+q?$X6-7?a zmp43M9#>ACRg%#aZSU-jHg!k4>iSmiY3=BUwkdLoHi%b-Q{&&{{AD!_pYTtrNLb{D zBr*|5S!L?8hz%f$i8WD(hVra%E+z*fm9nS140CZak7cs5m9UUCph<)yNY3lcaEqT_Z(GN0>W8$`Ap) z3Z#uN?h`ljMI3v(|EP4=aDHoH?ArV8zhTch|*_6u9rNs46bOxBi5fR47-8{DHLnos;h*^ttf?w@A2Ai`~Ig PS#C?#TX~l!!oL3is;%aM 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 2281d39aaea5f57da427732f0478b5e24ddbc7fd..a131a5d22fc09206093d534988e50f0be11f9aba 100644 GIT binary patch literal 5605 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 Ccc3i* literal 3991 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 kk4%Jy2J z`~rRw-#DW)q|E4yAHWag@ISj@Q#Tu5_#)@td-j}jf9K)m?|=UI3&3?e%3~JmQ#gZ< zMcfi`TSP-dQ^Y40KF!0%x{W)s-W9Q7<1-ugY;4;2Tom_3Y*}bo_(DMse>m_IirX*T z@7;RG?d{hehkd`de~aI~=?7f}EesT_cF+sGUZ`LM{r#;yg{jc*dWWIgJy1|B1@pGw z^TVb>uCn@+&KtoV1J7*xJ@4UBch~Dbad$h>RYBYBJazlNoQFz1eC{7AEN-`hZoT98 zzODP+dY2J8^{wug-gig5i9}yl#yS6=rZw&+)P)CL_&nHq@_BMU##B@^HtI~Z3i&%0nhj|BQMZ7JdBw_*Q9h}3w z!rAnk+&%I;dtRTVOQQWKGu2peu#E>I9ulEemSJkFa87FFFp$luT6s8Ba&oACL*5Xt z9S1kT4-5Y>61;W9v$OlcYjZ$N_u#k<#HcfD;?w z65ioSoZsbT;`|G8eB8jYI9IO>pjB%F}}=+%-wVAOeI0JB=M2CzoaFGXH6&{4n? zPGcHHrgMhPpJm)RTqdyGd>@uXAKolGNmX!{qhz&cC+X&*og}PBJ4w}uc9Oish(O7t zJLm|G8jt)f@OO?s#egf6|E|bOSikY3hB9Sbky2(wDp(y>q{&sNOsU3+jPsFpkxWZb zg!$Mi$;^~-CxCDoYRFX0WYW@17c-fzWHV`_F2qci$+SFyDX#XANouE1%Vf%>nN~BI z*0Py$X{I$YU7f%rL!Q)0dZ}od)jK`SbS;yqp3S7EnQoBj<^(30z=TO=OX0mtCL_(X zp2>7Ao5@HsHOSPQz$9-WVUk%_cpo27#xt&QC$Ih`%wM@VIpcRSnXcmot;D&>*Cy}H ztX|;TPBYyj)8+&wd6fy1ygeoVxkIL(8B~~_{R#W4SC}g1k8z588kfS#F`VLCu_Bi! prelSkC}v`XkthnW!aT<5!cUBGjAFV4trO`!OaA0OB#DNPUIS(pS55!` 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 0a434fe626c550f3f0226b07edd9d960efec9046..1a3eb84e8706762cc6f35202e1a9ee09269e80f4 100644 GIT binary patch literal 3157 zcmbVNZBrXn6h1fE>?UCgNuXF>3fLD63pA}(Z7s!WDH0ke6l7Y*4=!Pat$ATKi!=R4 ze$W}6G2rNopF94Zes+A$-OVQ1Y|Q9~+_ydVJkNRVIVXSp{rjIpbeFzNQ6+bso*llj$}aq< zD|XFxR|UoM3kNWLEZvTj?3%TGRz0+weee-xd}Rmpw)mnEqA>{L(GeMkK!0Xc=6NfaL687hW18r@20G!`mzQA{(S(u+^%W zG`oe|rIN`AA2vM$Su{rf9iUz4@gO+KhLHuVg;$hUHex$V`@oU*lU zbJ2~otLPR>q0=;7)hS1#I*rj~oyK|f4qee{f>)C?C20EJicv?K`cS8j>5fKwI_=Ya zoj#!l?D#tBs;#$2sq1L(b;GsLPO{}Q$FXWslP$Yau^aR*+Ex}lP^B2N!z#uEbVv}OVbd%!7a+4HeTxr7dgxVz4n9!P}wGpn#Tf;yYp%i6k7~zZ}6ec?h zk~a~|9QBoUU9fgd0qz0Jm>jYz=KyDv?5pRNtG)OT%#LUPpG)g8W64!jB$kD zBz`GC&ciVW^#aZqV&)G@zPzB+QzJ`n&M9r=hR#Vhexpn0G;I6~ix|kVuLQ;`2x$W0 zOoI9pB$^JSOTywkDIKHaed%_A$)raag%NT?@VvNmK_hL~sw6TeUCnvs{)6M9REvp@ zawftp$9O3&W9*D;!AJZGh%8R7g}4a7rR4~~8xqj)tOLND6q*6Ohhp(_5;`uSvx@ zm!#8i&$=fl)-J`>^VnNV=Y>m5PFH zQ32|3`ti)>3FB9ag;U|K#0VGUZMx%gUB%3DsInN_1?QFYJ!I;A>A{mh4GN1ZgA0pg zfXXZb{@`ST2y=@6h>u?8d+E6n$q9|Ghtm{#LzvSeaQdtdC+=whC)P^PN0B-yVV%|@ zIiU~ta6;2k!kjk1sn~}TH=BSHXQQBd5ViGoF|53|OZ*hlsT9cxe+L1lxXd%vm__3Z zbJ_u?-9DVS?FF2;tqEF*)JYBNv>(aoVYE(an9~bzdfA5)H{gI1_X9zz^eM)0K51c2 nUq^C!ge>v?bh>i9<4?uQ@o&J%?8AxMWWb5rzM#+LBR&2Hy(Q*n literal 3015 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 literal 1563 zcma)6&vO%H7=FI&ZnpVFODRRPPzx2(Ei|=M#I{sQEm|9jPHG(V;x?@5(%nra*)Vcb zM+dlw2R*<|FI*h0mlj&4+&u7SFb1b5@6ISbU$VB_X?2|B`}KbB`@GNl@MPwr9=O+Mz(;f$nUfZ+Ac6G`v`_~2br?XehYi7nVy<#TkS8T61+V=t7SN)3gvg1-9mGjNQ z;zhF@cx}6A`3t2h@0bDUj8SA%G`|P zFqh|iV7u8EuP7g9tyrH}&MtbUU#pN$er#rBXK49dWaIxM2b+GF$6TpaDOeY5S@O0} z4F_aEN$f$Zj@OeohlwQi-*`<7W;|I z6@|AXi%B`X!Mr3fg$n|Q{<8rY$`+YsaoJT{oEv;YD$Ta!V_4WatS--HCKnXp3$SFQ?K!uR$=iJ6qhO6MJ87mM!a}&W{ z&e4W2igxakO$F>j2Tw6sDQZdMw*%u2;}@iiWki;t+<~s%gR1<%dnJUG3So8N0G{Lh zL5?VtJ<2(O=egI|x?Ey-VbjI=z(vZq5B2W9V0VVFQz2{(k;HcBTj;_e@*;cD!c=H< zmm~WYWBo1?YtSyQAi89%YvnsgtwF!s-m-$&BY8ogvx0b5IooBde~-gP*U+rdrG7Lt zqV}IWs6IeK!TVoCBKMv>>0ctO+6aCC3GQEHOg%gwW!Swqg5wxK8mD3K3wi<*IEfja z-(@vq!iPy~gxZJEO{x=!)4rQ{gT&~ebwrXTRxj59yu{xzX6bmtj}lm>H!b^U+0-(q hB~#uCEvC2qiIk3h@dv?!+#luL362Q)2-?V9=5OkDW0(K{ 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 ff384498302ed22495e5a57d14cbb5874d48eea5..3bdf50c91ab32e5d791cb2c9822b19ce53d45340 100644 GIT binary patch literal 20966 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
-1JWUU7Q+79??cfwx-DZAm$QhqBUs za&rj|H7*A?bUOO3m;k8(oL2$N@btC|q{=g(JddB#xPOZyQM{|e`q7l+@zx=$k+Kja zl}=c0O9L0oLRt!sq!gA>Mq^30Mz+Spxr$4f&rcR71s)tGk&+Y3JTSBu4*Vo1OL>{1 zGZYlHDXo1$Bf&vCLw1$Ii}Fc~{$KaC#KP4)7bUCG=> zsKLFXnSgs0p5Aj2z^%HQ);x?;XfK7}i-E6E`4)$WH-(?UvBQsG8ur70K8Ho~JdFW) zmmBxEdxd2(jNZpK1RAEXYRQrEv6=Y10UoJgIAOvq6~-Gv-T2=vXaTR$0v-W*3xA);1a`Br4L{*MRNSVmhk^&+~W`aADM;j1ONa4 delta 1117 zcmaKr$x~BV6vlry3>Weq7K#W_oCR@!N6(6g+O3E!s2CB&F+rpsCLt!#2B+w*t9sRy za_6db<<6okt6b>HjceCkuKXJ;s}#TYQcmrV$~)(tbI&)to0}K6&7J1r(>D(v0Y7ph z$|CwLd!j_>kMcWv9R?V5*vEc{0}MGFBp$2|S%#xT=?^-GgU*P<5so^Hax5s1TTVoo z&&dFMJfKnMl;yOcy2sD>gg=lIcu3PM0ZV(jb{t~ zR3>lvqm--sr_}GZ^fl3JXqZe~On4Uxe#+aC%Oz$8{CvUUyG(LUTrMfnVsptbB}$hp z=f&%iBkxk+Lg}u%WEmIv(`A|&>A76uvYfbF;i}6unoG~!rap>XH^j!K(q78Xob&v2 zsG9e7CWob-H`Iq#Qi;rjw?8>KK34do6))uc%tYyN{d8O12hW3vtf4Wi^XF4ud}!eP zyUvczw~;t2Y1B^=d?Fb1&WbTrp0ZJ{TK@oZFWg?NSRFQNS)#RD9-xJ#s%lxraw>_< zK5OcT6ocWjAyWf2(x{h8Ou0oGt;+4HzGE@n>g*K3px`4SNV0;oT+OI1vYNkG!{0*O z7v>SIW+9Dcsd83%M)_aXhGs6wPEy<}gsmf8RJlsb0X z6w8~xsJTgyc3S47R 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 @@