u
This commit is contained in:
1 parent
2f8a815c62
commit
a09fdf0a66
127 files changed
+4518
-1159
No files matched your search
@@ -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
|
||||
Generated
-8
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
Generated
-12
@@ -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
@@ -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>
|
||||
Generated
-7
@@ -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>
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user