This commit is contained in:
oneao committed 2025-12-27 17:21:38 +08:00
1 parent 2f8a815c62
commit a09fdf0a66
127 files changed
+4518 -1159

No files matched your search

+38
View File
@@ -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
-8
View File
@@ -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/
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="link-backend" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="link-backend" options="-parameters" />
</option>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>
-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="aliyun" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="aliyun" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+1 -1
View File
@@ -31,7 +31,7 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version> <!-- 建议使用最新版本 -->
<version>1.2.18</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
@@ -123,4 +123,6 @@ public class BatchColumn {
}
}
}
}
@@ -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,
@@ -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<Map<String, Object>> 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<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql);
return Result.ok("成功",maps);
} catch (Exception e) {
return Result.fail("公司列表查询异常:" + e.getMessage());
}
}
@GetMapping("/api/loadGroupList")
public Result<?> loadGroupList(){
return Result.ok("");
}
}
@@ -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<Map<String, Object>> records = pageResult.getRecords();
// 判断是否在线
for (Map<String, Object> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String,Object> 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<String, Object> 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<String,Object> 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<String, Object> body){
try {
String orgId = UserContext.getOrgId();
CommonUtils.requireFields(body, "b_groupname");
Map<String,Object> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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());
}
}
}
@@ -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<Map<String, Object>> maps = DbUtils.queryForList(orgId, friendSql);
ImResponse response = ImResponse.success();
response.setType("online");
response.setFrom(fromUserid);
for (Map<String, Object> map : maps) {
String bFrienduserId = map.get("b_frienduser_Id").toString();
if (CommonUtils.isNotEmpty(bFrienduserId) && 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<Map<String, Object>> friends = DbUtils.queryForList(orgId, friendSql);
for (Map<String, Object> 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<Map<String, Object>> groupList = DbUtils.queryForList(orgId, sqlGroups);
for (Map<String, Object> 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<Map<String, Object>> memberList = DbUtils.queryForList(orgId, sqlMembers);
// 3. 构造消息
ImResponse response = ImResponse.success();
response.setFrom(fromUserid);
// 添加群信息到消息
response.setData(CommonUtils.mapOf("groupId", groupId));
// 4. 遍历群成员发送消息
for (Map<String, Object> member : memberList) {
String memberId = (String) member.get("b_member_id");
if (!fromUserid.equals(memberId)) {
WriteManager.writeToUser(memberId, response);
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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<Map<String, Object>> groupMembers = DbUtils.queryForList(orgId, groupMembersSql);
List<Map<String, Object>> updateGroupMembersMap = new ArrayList<>();
List<String> onlineMemberIds = new ArrayList<>();
// 遍历群成员,构建更新数据并收集在线成员
for (Map<String, Object> groupMember : groupMembers) {
String bMemberId = groupMember.get("b_member_id").toString();
String bId = groupMember.get("b_id").toString();
Map<String, Object> 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<String, Object> 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<String, Object> data = new HashMap<>();
data.put("_tempId", _tempId);
error.setData(data);
WriteManager.writeToUser(orgId, from, error);
}
}
}
@@ -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<TextWebSocketFrame> {
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<TextWebSocke
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
// 下线时通过 UserChannelManager 移除
UserChannelManager.removeUserByChannel(ctx.channel());
ImRequest request = new ImRequest();
request.setType("offline");
System.out.println("❌ 客户端断开: " + ctx.channel().id().asShortText());
UserSession userSession = UserChannelManager.getSessionByChannel(ctx.channel());
if(userSession != null){
request.setOrgId(userSession.getOrgId());
request.setFrom(userSession.getUserId());
}
// 离线通知
imMessageProcessor.process(ctx,request);
}
@Override
@@ -4,83 +4,120 @@ import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class UserChannelManager {
private static final Logger logger = LoggerFactory.getLogger(UserChannelManager.class);
private static final ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap<>();
// orgId -> (userId -> UserSession)
private static final ConcurrentHashMap<String, ConcurrentHashMap<String, UserSession>> 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<String, UserSession> orgMap = sessions.get(orgId);
if (orgMap == null) {
orgMap = new ConcurrentHashMap<>();
ConcurrentHashMap<String, UserSession> 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<String, UserSession> 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<String, UserSession> 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<Channel> getChannels(String orgId, String userId) {
ConcurrentHashMap<String, UserSession> 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<String, UserSession> 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<String, UserSession> 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();
}
}
@@ -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<Channel> 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<Channel> getChannels() { return channels; }
}
@@ -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<Channel> 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<String, Object> 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<String> userIds, Object message) {
public static void broadcastToUsers(String orgId,List<String> userIds, Object message) {
if (message == null || userIds == null || userIds.isEmpty()) return;
for (String userId : userIds) {
writeToUser(userId, message); // 内部自动判断在线
writeToUser(orgId,userId, message); // 内部自动处理多 channel
}
}
@@ -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 +
'}';
}
}
@@ -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);
}
@@ -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<Map<String,Object>> records;
public PageResult() {
}
public PageResult(int total, List<Map<String, Object>> records) {
this.total = total;
this.records = records;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public List<Map<String, Object>> getRecords() {
return records;
}
public void setRecords(List<Map<String, Object>> records) {
this.records = records;
}
}
@@ -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<String, Object> friendRecord = DbUtils.querySingle(orgCode, sql);
// 已存在直接返回
if (CommonUtils.isNotEmpty(friendRecord)) {
return 0;
}
// 不存在则插入
Map<String, Object> 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");
}
}
@@ -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<String, Object> map, String... keys) {
if (map == null) {
throw new IllegalArgumentException("参数不能为空");
}
if (keys == null || keys.length == 0) {
throw new IllegalArgumentException("需要检查的字段不能为空");
}
List<String> 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<String, Object>
* 支持原生 Map 或 JSON 对象
*
* @param obj 待转换对象
* @return Map<String, Object>
* @throws IllegalArgumentException 如果无法转换
*/
public static Map<String, Object> objectToMap(Object obj) {
if (obj == null) {
return null;
}
// 如果已经是 Map 类型,直接返回
if (obj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) obj;
return map;
}
// 如果不是 Map,可以尝试用 Jackson 转换
try {
return objectMapper.convertValue(obj, new TypeReference<Map<String, Object>>() {});
} 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;
}
}
}
@@ -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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<Object> values = new ArrayList<>();
for (Map.Entry<String, Object> 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<String, Object> 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<Object> values = new ArrayList<>();
for (Map.Entry<String, Object> 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<Map<String, Object>> dataList, String primaryKey) throws SQLException {
if (dataList == null || dataList.isEmpty()) {
return 0;
}
int total = 0;
for (Map<String, Object> data : dataList) {
total += updateOne(orgCode, tableName, data, primaryKey);
}
return total;
}
public static int insertOne(String orgCode, String tableName, Map<String, Object> 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<Object> values = new ArrayList<>();
for (Map.Entry<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> filteredList = new ArrayList<>();
// 主键过滤
if (!CommonUtils.isEmpty(primaryKey)) {
Set<Object> 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<String, Object> data : dataList) {
stmt.setObject(idx++, data.get(primaryKey));
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
existingKeys.add(rs.getObject(1));
}
}
}
for (Map<String, Object> data : dataList) {
if (!existingKeys.contains(data.get(primaryKey))) {
filteredList.add(data);
}
}
} else {
filteredList.addAll(dataList);
}
if (filteredList.isEmpty()) {
conn.commit();
return 0;
}
// 构建 SQL
Set<String> 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<String, Object> 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());
}
}
}
}
@@ -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<NetworkInterface> 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);
}
}
}
@@ -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
@@ -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