
在当今数字化时代,即时通讯(IM)功能已成为各类应用的标配。无论是社交软件、电商平台还是企业协同工具,都需要可靠、高效的聊天室功能来增强用户互动。然而,从零开发一套 IM 系统不仅技术复杂度高,还需要应对高并发、消息可靠性等一系列挑战。
作为一名资深开发者,我深知自研 IM 的痛点:需要处理消息存储、推送、离线同步、已读未读等核心功能,还要考虑扩展性和安全性。这就是为什么越来越多的团队选择成熟的第三方 IM 解决方案 —— 它们能让我们专注于业务逻辑,而非重复造轮子。
在众多 IM 服务商中,经过性能测试、API 友好度、文档完整性等多维度对比,环信 IM凭借其企业级稳定性、丰富的功能和灵活的集成方式脱颖而出。本文将带您深入了解环信 IM 的技术优势,并通过一个完整的实战案例,教您如何在 Java 项目中快速集成环信聊天室功能。
环信作为国内领先的即时通讯云服务商,已为超过 10 万家企业提供 IM 解决方案。其底层架构经过多年打磨,具备以下核心优势:
环信采用分布式微服务架构,确保高可用和可扩展性:

与其他主流 IM 服务商相比,环信的优势显而易见:
功能 | 环信 IM | 其他服务商 A | 其他服务商 B |
|---|---|---|---|
单聊 / 群聊 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
聊天室 | ✅ 支持万人级 | ❌ 限制 500 人 | ✅ 支持千人级 |
消息类型 | 文本、语音、视频、文件等 10 + 种 | 基础 5 种类型 | 基础 7 种类型 |
离线消息 | ✅ 无限存储 | ❌ 7 天限制 | ✅ 30 天存储 |
已读回执 | ✅ 支持 | ❌ 不支持 | ✅ 部分支持 |
自定义消息 | ✅ 高度灵活 | ❌ 不支持 | ✅ 有限支持 |
多端同步 | ✅ 全平台一致 | ✅ 部分支持 | ✅ 基本支持 |
开源 UI 组件 | ✅ 提供 | ❌ 无 | ✅ 有限提供 |
这些数据意味着环信能够满足从中小型应用到大型企业级系统的各种需求。
在开始实战前,我们需要先理解环信 IM 的核心概念和工作流程,这将帮助我们更好地设计和实现功能。

接下来,我们将通过一个完整的实战案例,展示如何在 Spring Boot 项目中集成环信 IM 聊天室功能。我们将实现用户注册、登录、创建聊天室、加入聊天室、发送消息、接收消息等核心功能。
首先,我们创建一个 Spring Boot 项目,pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>easemob-chatroom-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>easemob-chatroom-demo</name>
<description>Demo project for Spring Boot with Easemob IM</description>
<properties>
<java.version>17</java.version>
<easemob-sdk.version>1.0.2</easemob-sdk.version>
<lombok.version>1.18.30</lombok.version>
<fastjson2.version>2.0.47</fastjson2.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql-connector.version>8.0.33</mysql-connector.version>
<springdoc.version>2.4.0</springdoc.version>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 环信SDK -->
<dependency>
<groupId>com.easemob.im</groupId>
<artifactId>im-sdk-core</artifactId>
<version>${easemob-sdk.version}</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
创建application.yml配置文件:
spring:
application:
name: easemob-chatroom-demo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/easemob_chatroom?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# 环信配置
easemob:
app-key: your_appkey # 替换为你的环信AppKey
client-id: your_client_id # 替换为你的环信Client ID
client-secret: your_client_secret # 替换为你的环信Client Secret
api-url: https://a1.easemob.com # 环信API地址,根据你的应用区域选择
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.easemobchatroomdemo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
com.example.easemobchatroomdemo: debug
# 服务器配置
server:
port: 8080
# SpringDoc配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
注意:需要将环信相关配置替换为你在环信控制台创建应用后获得的实际信息。
我们需要创建用户表和聊天室表,用于存储业务系统与环信 IM 的关联信息:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS easemob_chatroom CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE easemob_chatroom;
-- 用户表:存储系统用户与环信IM用户的关联
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名(与环信IM的username一致)',
`password` varchar(100) NOT NULL COMMENT '密码',
`nickname` varchar(50) NOT NULL COMMENT '昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
-- 聊天室表:存储聊天室信息
CREATE TABLE `chat_room` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '聊天室ID',
`room_id` varchar(50) NOT NULL COMMENT '环信聊天室ID',
`name` varchar(100) NOT NULL COMMENT '聊天室名称',
`description` varchar(500) DEFAULT NULL COMMENT '聊天室描述',
`owner_username` varchar(50) NOT NULL COMMENT '创建者用户名',
`max_users` int NOT NULL DEFAULT 2000 COMMENT '最大用户数',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-关闭,1-正常',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_room_id` (`room_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天室表';
-- 用户聊天室关联表:记录用户加入的聊天室
CREATE TABLE `user_chat_room` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`room_id` varchar(50) NOT NULL COMMENT '环信聊天室ID',
`role` tinyint NOT NULL DEFAULT 0 COMMENT '角色:0-普通成员,1-管理员',
`join_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
`quit_time` datetime DEFAULT NULL COMMENT '退出时间',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-已退出,1-在室中',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_room` (`username`,`room_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户聊天室关联表';
我们的项目采用分层架构设计,结构如下:
com.example.easemobchatroomdemo
├── config # 配置类
├── controller # 控制器
├── dto # 数据传输对象
├── entity # 实体类
├── exception # 异常处理
├── mapper # 数据访问层
├── service # 业务逻辑层
│ ├── impl # 业务逻辑实现
│ └── easemob # 环信相关服务
├── util # 工具类
└── EasemobChatroomDemoApplication.java # 启动类
package com.example.easemobchatroomdemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* 环信聊天室Demo启动类
*
* @author ken
*/
@SpringBootApplication
@MapperScan("com.example.easemobchatroomdemo.mapper")
public class EasemobChatroomDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EasemobChatroomDemoApplication.class, args);
}
/**
* 注册RestTemplate用于HTTP请求
*
* @return RestTemplate实例
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package com.example.easemobchatroomdemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 环信配置属性类
*
* @author ken
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "easemob")
public class EasemobProperties {
/**
* 环信应用的唯一标识
*/
private String appKey;
/**
* 环信应用的Client ID
*/
private String clientId;
/**
* 环信应用的Client Secret
*/
private String clientSecret;
/**
* 环信API的基础URL
*/
private String apiUrl;
}
用户实体类:
package com.example.easemobchatroomdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体类
*
* @author ken
*/
@Data
@TableName("sys_user")
public class SysUser {
/**
* 用户ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名(与环信IM的username一致)
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 头像URL
*/
private String avatar;
/**
* 状态:0-禁用,1-正常
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdTime;
/**
* 更新时间
*/
private LocalDateTime updatedTime;
}
聊天室实体类:
package com.example.easemobchatroomdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 聊天室实体类
*
* @author ken
*/
@Data
@TableName("chat_room")
public class ChatRoom {
/**
* 自增ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 环信聊天室ID
*/
private String roomId;
/**
* 聊天室名称
*/
private String name;
/**
* 聊天室描述
*/
private String description;
/**
* 创建者用户名
*/
private String ownerUsername;
/**
* 最大用户数
*/
private Integer maxUsers;
/**
* 状态:0-关闭,1-正常
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdTime;
/**
* 更新时间
*/
private LocalDateTime updatedTime;
}
用户聊天室关联实体类:
package com.example.easemobchatroomdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户聊天室关联实体类
*
* @author ken
*/
@Data
@TableName("user_chat_room")
public class UserChatRoom {
/**
* 自增ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 环信聊天室ID
*/
private String roomId;
/**
* 角色:0-普通成员,1-管理员
*/
private Integer role;
/**
* 加入时间
*/
private LocalDateTime joinTime;
/**
* 退出时间
*/
private LocalDateTime quitTime;
/**
* 状态:0-已退出,1-在室中
*/
private Integer status;
}
用户注册 DTO:
package com.example.easemobchatroomdemo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 用户注册请求DTO
*
* @author ken
*/
@Data
@Schema(description = "用户注册请求参数")
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "testuser1")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码", example = "123456")
private String password;
@NotBlank(message = "昵称不能为空")
@Schema(description = "昵称", example = "测试用户1")
private String nickname;
@Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
private String avatar;
}
用户登录 DTO:
package com.example.easemobchatroomdemo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 用户登录请求DTO
*
* @author ken
*/
@Data
@Schema(description = "用户登录请求参数")
public class UserLoginDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "testuser1")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码", example = "123456")
private String password;
}
创建聊天室 DTO:
package com.example.easemobchatroomdemo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建聊天室请求DTO
*
* @author ken
*/
@Data
@Schema(description = "创建聊天室请求参数")
public class CreateChatRoomDTO {
@NotBlank(message = "聊天室名称不能为空")
@Schema(description = "聊天室名称", example = "技术交流群")
private String name;
@Schema(description = "聊天室描述", example = "这是一个技术交流的聊天室")
private String description;
@NotNull(message = "最大用户数不能为空")
@Schema(description = "最大用户数", example = 2000)
private Integer maxUsers;
}
通用响应 DTO:
package com.example.easemobchatroomdemo.dto;
import com.alibaba.fastjson2.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 通用响应DTO
*
* @author ken
*/
@Data
@Schema(description = "通用响应结果")
public class ApiResponse<T> {
@Schema(description = "状态码:200表示成功,其他表示失败", example = "200")
private int code;
@Schema(description = "响应消息", example = "操作成功")
private String message;
@Schema(description = "响应数据")
private T data;
/**
* 成功响应
*
* @param data 响应数据
* @return 成功响应对象
*/
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("操作成功");
response.setData(data);
return response;
}
/**
* 成功响应(无数据)
*
* @return 成功响应对象
*/
public static <T> ApiResponse<T> success() {
return success(null);
}
/**
* 失败响应
*
* @param code 错误码
* @param message 错误消息
* @return 失败响应对象
*/
public static <T> ApiResponse<T> fail(int code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
response.setData(null);
return response;
}
/**
* 失败响应(默认错误码)
*
* @param message 错误消息
* @return 失败响应对象
*/
public static <T> ApiResponse<T> fail(String message) {
return fail(500, message);
}
}
自定义异常类:
package com.example.easemobchatroomdemo.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 业务异常类
*
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private int code;
/**
* 构造方法
*
* @param code 错误码
* @param message 错误消息
*/
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
/**
* 构造方法(默认错误码)
*
* @param message 错误消息
*/
public BusinessException(String message) {
this(500, message);
}
}
全局异常处理器:
package com.example.easemobchatroomdemo.exception;
import com.example.easemobchatroomdemo.dto.ApiResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* @author ken
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*
* @param e 业务异常
* @return 错误响应
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return ApiResponse.fail(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*
* @param e 参数校验异常
* @return 错误响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String errorMsg = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: {}", errorMsg);
return ApiResponse.fail(400, errorMsg);
}
/**
* 处理参数校验异常(非实体类参数)
*
* @param e 参数校验异常
* @return 错误响应
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
log.error("参数校验异常: {}", errorMsg);
return ApiResponse.fail(400, errorMsg);
}
/**
* 处理其他异常
*
* @param e 异常
* @return 错误响应
*/
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return ApiResponse.fail("系统异常,请联系管理员");
}
}
环信 API 响应实体:
package com.example.easemobchatroomdemo.service.easemob.vo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* 环信API响应通用结构
*
* @author ken
*/
@Data
public class EasemobApiResponse<T> {
@JSONField(name = "action")
private String action;
@JSONField(name = "application")
private String application;
@JSONField(name = "path")
private String path;
@JSONField(name = "uri")
private String uri;
@JSONField(name = "entities")
private T entities;
@JSONField(name = "data")
private T data;
@JSONField(name = "timestamp")
private Long timestamp;
@JSONField(name = "duration")
private Integer duration;
@JSONField(name = "status")
private String status;
@JSONField(name = "error")
private EasemobError error;
/**
* 判断响应是否成功
*
* @return 是否成功
*/
public boolean isSuccess() {
return "success".equals(status);
}
}
环信错误信息实体:
package com.example.easemobchatroomdemo.service.easemob.vo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* 环信API错误信息
*
* @author ken
*/
@Data
public class EasemobError {
@JSONField(name = "message")
private String message;
@JSONField(name = "code")
private Integer code;
@JSONField(name = "error")
private String error;
}
环信 AccessToken 实体:
package com.example.easemobchatroomdemo.service.easemob.vo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* 环信AccessToken
*
* @author ken
*/
@Data
public class EasemobAccessToken {
@JSONField(name = "access_token")
private String accessToken;
@JSONField(name = "expires_in")
private Long expiresIn;
@JSONField(name = "application")
private String application;
}
环信聊天室实体:
package com.example.easemobchatroomdemo.service.easemob.vo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* 环信聊天室信息
*
* @author ken
*/
@Data
public class EasemobChatRoom {
@JSONField(name = "id")
private String id;
@JSONField(name = "name")
private String name;
@JSONField(name = "description")
private String description;
@JSONField(name = "owner")
private String owner;
@JSONField(name = "maxusers")
private Integer maxUsers;
@JSONField(name = "affiliations_count")
private Integer affiliationsCount;
}
环信消息实体:
package com.example.easemobchatroomdemo.service.easemob.vo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* 环信消息
*
* @author ken
*/
@Data
public class EasemobMessage {
@JSONField(name = "target_type")
private String targetType = "chatrooms";
@JSONField(name = "target")
private String[] target;
@JSONField(name = "msg")
private MsgContent msg;
@JSONField(name = "from")
private String from;
@JSONField(name = "ext")
private Object ext;
/**
* 消息内容
*/
@Data
public static class MsgContent {
@JSONField(name = "type")
private String type;
@JSONField(name = "msg")
private String msg;
}
}
环信服务接口:
package com.example.easemobchatroomdemo.service.easemob;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobChatRoom;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobMessage;
/**
* 环信IM服务接口
*
* @author ken
*/
public interface EasemobService {
/**
* 获取环信AccessToken
*
* @return AccessToken
*/
String getAccessToken();
/**
* 注册环信用户
*
* @param username 用户名
* @param password 密码
* @param nickname 昵称
*/
void registerUser(String username, String password, String nickname);
/**
* 创建聊天室
*
* @param owner 聊天室所有者(用户名)
* @param name 聊天室名称
* @param description 聊天室描述
* @param maxUsers 最大用户数
* @return 聊天室ID
*/
String createChatRoom(String owner, String name, String description, Integer maxUsers);
/**
* 加入聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
*/
void joinChatRoom(String roomId, String username);
/**
* 退出聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
*/
void leaveChatRoom(String roomId, String username);
/**
* 发送消息到聊天室
*
* @param from 发送者用户名
* @param roomId 聊天室ID
* @param message 消息内容
* @param msgType 消息类型(如:txt)
*/
void sendMessage(String from, String roomId, String message, String msgType);
/**
* 获取聊天室信息
*
* @param roomId 聊天室ID
* @return 聊天室信息
*/
EasemobChatRoom getChatRoomInfo(String roomId);
/**
* 删除聊天室
*
* @param roomId 聊天室ID
*/
void destroyChatRoom(String roomId);
}
环信服务实现类:
package com.example.easemobchatroomdemo.service.easemob.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.easemobchatroomdemo.config.EasemobProperties;
import com.example.easemobchatroomdemo.exception.BusinessException;
import com.example.easemobchatroomdemo.service.easemob.EasemobService;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobAccessToken;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobApiResponse;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobChatRoom;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 环信IM服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class EasemobServiceImpl implements EasemobService {
private final EasemobProperties easemobProperties;
private final RestTemplate restTemplate;
/**
* 缓存的AccessToken
*/
private String accessToken;
/**
* AccessToken过期时间(毫秒)
*/
private long tokenExpireTime;
/**
* 刷新token的锁
*/
private final ReentrantLock tokenLock = new ReentrantLock();
public EasemobServiceImpl(EasemobProperties easemobProperties, RestTemplate restTemplate) {
this.easemobProperties = easemobProperties;
this.restTemplate = restTemplate;
}
@Override
public String getAccessToken() {
// 判断token是否有效(提前60秒过期,避免网络延迟等问题)
if (StringUtils.hasText(accessToken) && System.currentTimeMillis() < tokenExpireTime - 60 * 1000) {
return accessToken;
}
try {
// 尝试获取锁,避免并发刷新token
if (tokenLock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 双重检查,防止已经刷新过
if (StringUtils.hasText(accessToken) && System.currentTimeMillis() < tokenExpireTime - 60 * 1000) {
return accessToken;
}
log.info("开始刷新环信AccessToken");
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 构建认证信息
String auth = easemobProperties.getClientId() + ":" + easemobProperties.getClientSecret();
byte[] encodedAuth = Base64Utils.encode(auth.getBytes(StandardCharsets.UTF_8));
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
// 构建请求体
Map<String, String> requestBody = new HashMap<>(1);
requestBody.put("grant_type", "client_credentials");
HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
// 发送请求
String url = easemobProperties.getApiUrl() + "/token";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("获取环信AccessToken失败,响应状态:{},响应内容:{}",
response.getStatusCode(), response.getBody());
throw new BusinessException("获取环信访问凭证失败");
}
// 解析响应
EasemobApiResponse<EasemobAccessToken> apiResponse = JSON.parseObject(
response.getBody(), EasemobApiResponse.class);
if (!apiResponse.isSuccess() || ObjectUtils.isEmpty(apiResponse.getData())) {
log.error("获取环信AccessToken失败,响应内容:{}", response.getBody());
throw new BusinessException("获取环信访问凭证失败");
}
EasemobAccessToken token = apiResponse.getData();
this.accessToken = token.getAccessToken();
// 计算过期时间(当前时间 + 有效期 - 60秒缓冲)
this.tokenExpireTime = System.currentTimeMillis() + (token.getExpiresIn() - 60) * 1000;
log.info("刷新环信AccessToken成功,有效期至:{}", tokenExpireTime);
return accessToken;
} finally {
tokenLock.unlock();
}
} else {
log.error("获取环信AccessToken锁超时");
throw new BusinessException("获取环信访问凭证失败,请稍后重试");
}
} catch (InterruptedException e) {
log.error("获取环信AccessToken异常", e);
Thread.currentThread().interrupt();
throw new BusinessException("获取环信访问凭证失败");
}
}
@Override
public void registerUser(String username, String password, String nickname) {
log.info("开始注册环信用户:{}", username);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + getAccessToken());
// 构建请求体
Map<String, Object> requestBody = new HashMap<>(3);
requestBody.put("username", username);
requestBody.put("password", password);
requestBody.put("nickname", nickname);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
// 解析AppKey,格式为 {org_name}/{app_name}
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] + "/users";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
// 处理响应
handleEasemobResponse(response, "注册环信用户失败:" + username);
log.info("环信用户注册成功:{}", username);
}
@Override
public String createChatRoom(String owner, String name, String description, Integer maxUsers) {
log.info("开始创建环信聊天室,所有者:{},名称:{}", owner, name);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + getAccessToken());
// 构建请求体
Map<String, Object> requestBody = new HashMap<>(4);
requestBody.put("owner", owner);
requestBody.put("name", name);
requestBody.put("description", StringUtils.hasText(description) ? description : "");
requestBody.put("maxusers", maxUsers);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] + "/chatrooms";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
// 处理响应
String responseBody = handleEasemobResponse(response, "创建环信聊天室失败");
// 解析聊天室ID
EasemobApiResponse<EasemobChatRoom> apiResponse = JSON.parseObject(responseBody, EasemobApiResponse.class);
if (ObjectUtils.isEmpty(apiResponse.getEntities())) {
log.error("解析聊天室ID失败,响应内容:{}", responseBody);
throw new BusinessException("创建聊天室失败");
}
EasemobChatRoom chatRoom = apiResponse.getEntities();
log.info("环信聊天室创建成功,ID:{},名称:{}", chatRoom.getId(), name);
return chatRoom.getId();
}
@Override
public void joinChatRoom(String roomId, String username) {
log.info("用户 {} 加入聊天室 {}", username, roomId);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + getAccessToken());
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] +
"/chatrooms/" + roomId + "/users/" + username;
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
// 处理响应
handleEasemobResponse(response, "用户 " + username + " 加入聊天室 " + roomId + " 失败");
log.info("用户 {} 成功加入聊天室 {}", username, roomId);
}
@Override
public void leaveChatRoom(String roomId, String username) {
log.info("用户 {} 退出聊天室 {}", username, roomId);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getAccessToken());
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] +
"/chatrooms/" + roomId + "/users/" + username;
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class);
// 处理响应
handleEasemobResponse(response, "用户 " + username + " 退出聊天室 " + roomId + " 失败");
log.info("用户 {} 成功退出聊天室 {}", username, roomId);
}
@Override
public void sendMessage(String from, String roomId, String message, String msgType) {
log.info("用户 {} 向聊天室 {} 发送消息:{}", from, roomId, message);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + getAccessToken());
// 构建消息内容
EasemobMessage easemobMessage = new EasemobMessage();
easemobMessage.setFrom(from);
easemobMessage.setTarget(new String[]{roomId});
EasemobMessage.MsgContent msgContent = new EasemobMessage.MsgContent();
msgContent.setType(msgType);
msgContent.setMsg(message);
easemobMessage.setMsg(msgContent);
HttpEntity<EasemobMessage> request = new HttpEntity<>(easemobMessage, headers);
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] + "/messages";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
// 处理响应
handleEasemobResponse(response, "发送消息失败");
log.info("用户 {} 向聊天室 {} 发送消息成功", from, roomId);
}
@Override
public EasemobChatRoom getChatRoomInfo(String roomId) {
log.info("获取聊天室 {} 信息", roomId);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getAccessToken());
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] +
"/chatrooms/" + roomId;
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, request, String.class);
// 处理响应
String responseBody = handleEasemobResponse(response, "获取聊天室 " + roomId + " 信息失败");
// 解析响应
EasemobApiResponse<EasemobChatRoom> apiResponse = JSON.parseObject(responseBody, EasemobApiResponse.class);
if (ObjectUtils.isEmpty(apiResponse.getData())) {
log.error("解析聊天室信息失败,响应内容:{}", responseBody);
throw new BusinessException("获取聊天室信息失败");
}
return apiResponse.getData();
}
@Override
public void destroyChatRoom(String roomId) {
log.info("删除聊天室 {}", roomId);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getAccessToken());
// 解析AppKey
String[] appKeyParts = easemobProperties.getAppKey().split("/");
if (appKeyParts.length != 2) {
throw new BusinessException("环信AppKey格式错误");
}
// 发送请求
String url = easemobProperties.getApiUrl() + "/" + appKeyParts[0] + "/" + appKeyParts[1] +
"/chatrooms/" + roomId;
HttpEntity<Void> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class);
// 处理响应
handleEasemobResponse(response, "删除聊天室 " + roomId + " 失败");
log.info("聊天室 {} 删除成功", roomId);
}
/**
* 处理环信API响应
*
* @param response 响应对象
* @param errorMsg 错误消息
* @return 响应体
*/
private String handleEasemobResponse(ResponseEntity<String> response, String errorMsg) {
if (response.getStatusCode() != HttpStatus.OK) {
log.error("{},响应状态:{},响应内容:{}", errorMsg, response.getStatusCode(), response.getBody());
throw new BusinessException(errorMsg);
}
String responseBody = response.getBody();
if (!StringUtils.hasText(responseBody)) {
log.error("{},响应内容为空", errorMsg);
throw new BusinessException(errorMsg);
}
EasemobApiResponse<?> apiResponse = JSON.parseObject(responseBody, EasemobApiResponse.class);
if (!apiResponse.isSuccess()) {
String errorDetail = ObjectUtils.isEmpty(apiResponse.getError()) ? "" : apiResponse.getError().getMessage();
log.error("{},错误详情:{},响应内容:{}", errorMsg, errorDetail, responseBody);
throw new BusinessException(errorMsg + (StringUtils.hasText(errorDetail) ? ":" + errorDetail : ""));
}
return responseBody;
}
}
用户服务接口:
package com.example.easemobchatroomdemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.easemobchatroomdemo.dto.UserLoginDTO;
import com.example.easemobchatroomdemo.dto.UserRegisterDTO;
import com.example.easemobchatroomdemo.entity.SysUser;
/**
* 用户服务接口
*
* @author ken
*/
public interface SysUserService extends IService<SysUser> {
/**
* 用户注册
*
* @param userRegisterDTO 注册信息
* @return 注册成功的用户
*/
SysUser register(UserRegisterDTO userRegisterDTO);
/**
* 用户登录
*
* @param userLoginDTO 登录信息
* @return 登录成功的用户信息
*/
SysUser login(UserLoginDTO userLoginDTO);
/**
* 根据用户名获取用户
*
* @param username 用户名
* @return 用户信息
*/
SysUser getByUsername(String username);
}
用户服务实现类:
package com.example.easemobchatroomdemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.easemobchatroomdemo.dto.UserLoginDTO;
import com.example.easemobchatroomdemo.dto.UserRegisterDTO;
import com.example.easemobchatroomdemo.entity.SysUser;
import com.example.easemobchatroomdemo.exception.BusinessException;
import com.example.easemobchatroomdemo.mapper.SysUserMapper;
import com.example.easemobchatroomdemo.service.SysUserService;
import com.example.easemobchatroomdemo.service.easemob.EasemobService;
import com.example.easemobchatroomdemo.util.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private final EasemobService easemobService;
private final BCryptPasswordEncoder passwordEncoder;
public SysUserServiceImpl(EasemobService easemobService) {
this.easemobService = easemobService;
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Override
@Transactional(rollbackFor = Exception.class)
public SysUser register(UserRegisterDTO userRegisterDTO) {
log.info("用户注册:{}", userRegisterDTO.getUsername());
// 检查用户是否已存在
SysUser existingUser = getByUsername(userRegisterDTO.getUsername());
if (!ObjectUtils.isEmpty(existingUser)) {
throw new BusinessException("用户名已存在");
}
// 创建系统用户
SysUser sysUser = new SysUser();
sysUser.setUsername(userRegisterDTO.getUsername());
// 加密密码
sysUser.setPassword(passwordEncoder.encode(userRegisterDTO.getPassword()));
sysUser.setNickname(userRegisterDTO.getNickname());
sysUser.setAvatar(userRegisterDTO.getAvatar());
sysUser.setStatus(1); // 正常状态
// 保存用户
boolean saveResult = save(sysUser);
if (!saveResult) {
log.error("保存用户失败:{}", userRegisterDTO.getUsername());
throw new BusinessException("注册失败,请稍后重试");
}
try {
// 在环信注册用户
easemobService.registerUser(
userRegisterDTO.getUsername(),
userRegisterDTO.getPassword(),
userRegisterDTO.getNickname()
);
} catch (Exception e) {
log.error("环信用户注册失败:{}", userRegisterDTO.getUsername(), e);
// 抛出异常,触发事务回滚
throw new BusinessException("注册失败:" + e.getMessage());
}
log.info("用户注册成功:{}", userRegisterDTO.getUsername());
return sysUser;
}
@Override
public SysUser login(UserLoginDTO userLoginDTO) {
log.info("用户登录:{}", userLoginDTO.getUsername());
// 获取用户
SysUser sysUser = getByUsername(userLoginDTO.getUsername());
if (ObjectUtils.isEmpty(sysUser)) {
throw new BusinessException("用户名或密码错误");
}
// 检查状态
if (sysUser.getStatus() != 1) {
throw new BusinessException("账号已被禁用");
}
// 验证密码
if (!passwordEncoder.matches(userLoginDTO.getPassword(), sysUser.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
log.info("用户登录成功:{}", userLoginDTO.getUsername());
return sysUser;
}
@Override
public SysUser getByUsername(String username) {
return getOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username));
}
}
聊天室服务接口:
package com.example.easemobchatroomdemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.easemobchatroomdemo.dto.CreateChatRoomDTO;
import com.example.easemobchatroomdemo.entity.ChatRoom;
import com.example.easemobchatroomdemo.entity.UserChatRoom;
import java.util.List;
/**
* 聊天室服务接口
*
* @author ken
*/
public interface ChatRoomService extends IService<ChatRoom> {
/**
* 创建聊天室
*
* @param createChatRoomDTO 聊天室信息
* @param username 创建者用户名
* @return 创建的聊天室
*/
ChatRoom createChatRoom(CreateChatRoomDTO createChatRoomDTO, String username);
/**
* 加入聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
* @return 关联信息
*/
UserChatRoom joinChatRoom(String roomId, String username);
/**
* 退出聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
*/
void leaveChatRoom(String roomId, String username);
/**
* 发送消息到聊天室
*
* @param roomId 聊天室ID
* @param username 发送者用户名
* @param message 消息内容
*/
void sendMessage(String roomId, String username, String message);
/**
* 获取用户加入的聊天室列表
*
* @param username 用户名
* @return 聊天室列表
*/
List<ChatRoom> getUserChatRooms(String username);
/**
* 删除聊天室
*
* @param roomId 聊天室ID
* @param username 操作人用户名(必须是创建者)
*/
void deleteChatRoom(String roomId, String username);
}
聊天室服务实现类:
package com.example.easemobchatroomdemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.easemobchatroomdemo.dto.CreateChatRoomDTO;
import com.example.easemobchatroomdemo.entity.ChatRoom;
import com.example.easemobchatroomdemo.entity.UserChatRoom;
import com.example.easemobchatroomdemo.exception.BusinessException;
import com.example.easemobchatroomdemo.mapper.ChatRoomMapper;
import com.example.easemobchatroomdemo.mapper.UserChatRoomMapper;
import com.example.easemobchatroomdemo.service.ChatRoomService;
import com.example.easemobchatroomdemo.service.SysUserService;
import com.example.easemobchatroomdemo.service.easemob.EasemobService;
import com.example.easemobchatroomdemo.service.easemob.vo.EasemobChatRoom;
import com.example.easemobchatroomdemo.util.CollectionUtils;
import com.example.easemobchatroomdemo.util.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 聊天室服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class ChatRoomServiceImpl extends ServiceImpl<ChatRoomMapper, ChatRoom> implements ChatRoomService {
private final EasemobService easemobService;
private final SysUserService sysUserService;
private final UserChatRoomMapper userChatRoomMapper;
public ChatRoomServiceImpl(EasemobService easemobService,
SysUserService sysUserService,
UserChatRoomMapper userChatRoomMapper) {
this.easemobService = easemobService;
this.sysUserService = sysUserService;
this.userChatRoomMapper = userChatRoomMapper;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ChatRoom createChatRoom(CreateChatRoomDTO createChatRoomDTO, String username) {
log.info("用户 {} 创建聊天室:{}", username, createChatRoomDTO.getName());
// 检查用户是否存在
if (ObjectUtils.isEmpty(sysUserService.getByUsername(username))) {
throw new BusinessException("用户不存在");
}
try {
// 调用环信API创建聊天室
String roomId = easemobService.createChatRoom(
username,
createChatRoomDTO.getName(),
createChatRoomDTO.getDescription(),
createChatRoomDTO.getMaxUsers()
);
// 保存聊天室信息到本地数据库
ChatRoom chatRoom = new ChatRoom();
chatRoom.setRoomId(roomId);
chatRoom.setName(createChatRoomDTO.getName());
chatRoom.setDescription(createChatRoomDTO.getDescription());
chatRoom.setOwnerUsername(username);
chatRoom.setMaxUsers(createChatRoomDTO.getMaxUsers());
chatRoom.setStatus(1); // 正常状态
boolean saveResult = save(chatRoom);
if (!saveResult) {
log.error("保存聊天室信息失败:{}", roomId);
// 删除环信聊天室
easemobService.destroyChatRoom(roomId);
throw new BusinessException("创建聊天室失败,请稍后重试");
}
// 创建者自动加入聊天室,并设为管理员
UserChatRoom userChatRoom = new UserChatRoom();
userChatRoom.setUsername(username);
userChatRoom.setRoomId(roomId);
userChatRoom.setRole(1); // 管理员
userChatRoom.setJoinTime(LocalDateTime.now());
userChatRoom.setStatus(1); // 在室中
int insertResult = userChatRoomMapper.insert(userChatRoom);
if (insertResult <= 0) {
log.error("保存用户聊天室关联信息失败:{} - {}", username, roomId);
// 删除环信聊天室和本地聊天室信息
easemobService.destroyChatRoom(roomId);
removeById(chatRoom.getId());
throw new BusinessException("创建聊天室失败,请稍后重试");
}
log.info("用户 {} 成功创建聊天室:{}(ID:{})", username, createChatRoomDTO.getName(), roomId);
return chatRoom;
} catch (Exception e) {
log.error("创建聊天室失败", e);
throw new BusinessException("创建聊天室失败:" + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserChatRoom joinChatRoom(String roomId, String username) {
log.info("用户 {} 加入聊天室:{}", username, roomId);
// 检查用户是否存在
if (ObjectUtils.isEmpty(sysUserService.getByUsername(username))) {
throw new BusinessException("用户不存在");
}
// 检查聊天室是否存在且正常
ChatRoom chatRoom = getOne(new LambdaQueryWrapper<ChatRoom>()
.eq(ChatRoom::getRoomId, roomId)
.eq(ChatRoom::getStatus, 1));
if (ObjectUtils.isEmpty(chatRoom)) {
throw new BusinessException("聊天室不存在或已关闭");
}
// 检查用户是否已加入
UserChatRoom existing = userChatRoomMapper.selectOne(new LambdaQueryWrapper<UserChatRoom>()
.eq(UserChatRoom::getUsername, username)
.eq(UserChatRoom::getRoomId, roomId));
if (!ObjectUtils.isEmpty(existing)) {
if (existing.getStatus() == 1) {
throw new BusinessException("已加入该聊天室");
} else {
// 重新加入
existing.setStatus(1);
existing.setJoinTime(LocalDateTime.now());
existing.setQuitTime(null);
userChatRoomMapper.updateById(existing);
return existing;
}
}
try {
// 调用环信API加入聊天室
easemobService.joinChatRoom(roomId, username);
// 保存关联信息
UserChatRoom userChatRoom = new UserChatRoom();
userChatRoom.setUsername(username);
userChatRoom.setRoomId(roomId);
userChatRoom.setRole(0); // 普通成员
userChatRoom.setJoinTime(LocalDateTime.now());
userChatRoom.setStatus(1); // 在室中
int insertResult = userChatRoomMapper.insert(userChatRoom);
if (insertResult <= 0) {
log.error("保存用户聊天室关联信息失败:{} - {}", username, roomId);
// 退出环信聊天室
easemobService.leaveChatRoom(roomId, username);
throw new BusinessException("加入聊天室失败,请稍后重试");
}
log.info("用户 {} 成功加入聊天室:{}", username, roomId);
return userChatRoom;
} catch (Exception e) {
log.error("加入聊天室失败", e);
throw new BusinessException("加入聊天室失败:" + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void leaveChatRoom(String roomId, String username) {
log.info("用户 {} 退出聊天室:{}", username, roomId);
// 检查用户是否已加入
UserChatRoom userChatRoom = userChatRoomMapper.selectOne(new LambdaQueryWrapper<UserChatRoom>()
.eq(UserChatRoom::getUsername, username)
.eq(UserChatRoom::getRoomId, roomId)
.eq(UserChatRoom::getStatus, 1));
if (ObjectUtils.isEmpty(userChatRoom)) {
throw new BusinessException("未加入该聊天室");
}
try {
// 调用环信API退出聊天室
easemobService.leaveChatRoom(roomId, username);
// 更新关联信息
userChatRoom.setStatus(0); // 已退出
userChatRoom.setQuitTime(LocalDateTime.now());
userChatRoomMapper.updateById(userChatRoom);
log.info("用户 {} 成功退出聊天室:{}", username, roomId);
} catch (Exception e) {
log.error("退出聊天室失败", e);
throw new BusinessException("退出聊天室失败:" + e.getMessage());
}
}
@Override
public void sendMessage(String roomId, String username, String message) {
log.info("用户 {} 向聊天室 {} 发送消息", username, roomId);
// 检查用户是否已加入该聊天室
UserChatRoom userChatRoom = userChatRoomMapper.selectOne(new LambdaQueryWrapper<UserChatRoom>()
.eq(UserChatRoom::getUsername, username)
.eq(UserChatRoom::getRoomId, roomId)
.eq(UserChatRoom::getStatus, 1));
if (ObjectUtils.isEmpty(userChatRoom)) {
throw new BusinessException("请先加入聊天室");
}
// 检查聊天室是否正常
ChatRoom chatRoom = getOne(new LambdaQueryWrapper<ChatRoom>()
.eq(ChatRoom::getRoomId, roomId)
.eq(ChatRoom::getStatus, 1));
if (ObjectUtils.isEmpty(chatRoom)) {
throw new BusinessException("聊天室不存在或已关闭");
}
try {
// 调用环信API发送消息(文本类型)
easemobService.sendMessage(username, roomId, message, "txt");
log.info("用户 {} 向聊天室 {} 发送消息成功", username, roomId);
} catch (Exception e) {
log.error("发送消息失败", e);
throw new BusinessException("发送消息失败:" + e.getMessage());
}
}
@Override
public List<ChatRoom> getUserChatRooms(String username) {
log.info("获取用户 {} 加入的聊天室列表", username);
// 查询用户加入的聊天室ID
List<UserChatRoom> userChatRooms = userChatRoomMapper.selectList(new LambdaQueryWrapper<UserChatRoom>()
.eq(UserChatRoom::getUsername, username)
.eq(UserChatRoom::getStatus, 1));
if (CollectionUtils.isEmpty(userChatRooms)) {
return List.of();
}
// 提取聊天室ID
List<String> roomIds = userChatRooms.stream()
.map(UserChatRoom::getRoomId)
.collect(Collectors.toList());
// 查询聊天室信息
return list(new LambdaQueryWrapper<ChatRoom>()
.in(ChatRoom::getRoomId, roomIds)
.eq(ChatRoom::getStatus, 1));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteChatRoom(String roomId, String username) {
log.info("用户 {} 删除聊天室:{}", username, roomId);
// 检查聊天室是否存在
ChatRoom chatRoom = getOne(new LambdaQueryWrapper<ChatRoom>()
.eq(ChatRoom::getRoomId, roomId));
if (ObjectUtils.isEmpty(chatRoom)) {
throw new BusinessException("聊天室不存在");
}
// 检查是否是创建者
if (!chatRoom.getOwnerUsername().equals(username)) {
throw new BusinessException("没有权限删除该聊天室,只有创建者可以删除");
}
try {
// 调用环信API删除聊天室
easemobService.destroyChatRoom(roomId);
// 更新本地聊天室状态
chatRoom.setStatus(0); // 已关闭
updateById(chatRoom);
// 更新用户聊天室关联信息
userChatRoomMapper.updateStatusByRoomId(roomId, 0, LocalDateTime.now());
log.info("用户 {} 成功删除聊天室:{}", username, roomId);
} catch (Exception e) {
log.error("删除聊天室失败", e);
throw new BusinessException("删除聊天室失败:" + e.getMessage());
}
}
}
用户控制器:
package com.example.easemobchatroomdemo.controller;
import com.example.easemobchatroomdemo.dto.ApiResponse;
import com.example.easemobchatroomdemo.dto.UserLoginDTO;
import com.example.easemobchatroomdemo.dto.UserRegisterDTO;
import com.example.easemobchatroomdemo.entity.SysUser;
import com.example.easemobchatroomdemo.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制器
*
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/user")
@Tag(name = "用户管理", description = "用户注册、登录接口")
public class UserController {
private final SysUserService sysUserService;
public UserController(SysUserService sysUserService) {
this.sysUserService = sysUserService;
}
/**
* 用户注册
*
* @param userRegisterDTO 注册信息
* @return 注册结果
*/
@PostMapping("/register")
@Operation(summary = "用户注册", description = "注册新用户,同时在环信IM系统中创建对应的用户")
public ApiResponse<SysUser> register(@Valid @RequestBody UserRegisterDTO userRegisterDTO) {
SysUser user = sysUserService.register(userRegisterDTO);
// 隐藏密码
user.setPassword(null);
return ApiResponse.success(user);
}
/**
* 用户登录
*
* @param userLoginDTO 登录信息
* @return 登录结果
*/
@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户登录系统")
public ApiResponse<SysUser> login(@Valid @RequestBody UserLoginDTO userLoginDTO) {
SysUser user = sysUserService.login(userLoginDTO);
// 隐藏密码
user.setPassword(null);
return ApiResponse.success(user);
}
}
聊天室控制器:
package com.example.easemobchatroomdemo.controller;
import com.example.easemobchatroomdemo.dto.ApiResponse;
import com.example.easemobchatroomdemo.dto.CreateChatRoomDTO;
import com.example.easemobchatroomdemo.entity.ChatRoom;
import com.example.easemobchatroomdemo.entity.UserChatRoom;
import com.example.easemobchatroomdemo.service.ChatRoomService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 聊天室控制器
*
* @author ken
*/
@RestController
@RequestMapping("/api/chatroom")
@Tag(name = "聊天室管理", description = "聊天室创建、加入、发送消息等接口")
public class ChatRoomController {
private final ChatRoomService chatRoomService;
public ChatRoomController(ChatRoomService chatRoomService) {
this.chatRoomService = chatRoomService;
}
/**
* 创建聊天室
*
* @param createChatRoomDTO 聊天室信息
* @param username 创建者用户名
* @return 创建的聊天室
*/
@PostMapping("/create")
@Operation(summary = "创建聊天室", description = "创建新的聊天室,创建者自动成为管理员并加入聊天室")
public ApiResponse<ChatRoom> createChatRoom(
@Valid @RequestBody CreateChatRoomDTO createChatRoomDTO,
@Parameter(description = "创建者用户名", required = true) @RequestParam String username) {
ChatRoom chatRoom = chatRoomService.createChatRoom(createChatRoomDTO, username);
return ApiResponse.success(chatRoom);
}
/**
* 加入聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
* @return 加入结果
*/
@PostMapping("/join")
@Operation(summary = "加入聊天室", description = "用户加入指定的聊天室")
public ApiResponse<UserChatRoom> joinChatRoom(
@Parameter(description = "聊天室ID", required = true) @RequestParam String roomId,
@Parameter(description = "用户名", required = true) @RequestParam String username) {
UserChatRoom userChatRoom = chatRoomService.joinChatRoom(roomId, username);
return ApiResponse.success(userChatRoom);
}
/**
* 退出聊天室
*
* @param roomId 聊天室ID
* @param username 用户名
* @return 退出结果
*/
@PostMapping("/leave")
@Operation(summary = "退出聊天室", description = "用户退出指定的聊天室")
public ApiResponse<Void> leaveChatRoom(
@Parameter(description = "聊天室ID", required = true) @RequestParam String roomId,
@Parameter(description = "用户名", required = true) @RequestParam String username) {
chatRoomService.leaveChatRoom(roomId, username);
return ApiResponse.success();
}
/**
* 发送消息
*
* @param roomId 聊天室ID
* @param username 发送者用户名
* @param message 消息内容
* @return 发送结果
*/
@PostMapping("/sendMessage")
@Operation(summary = "发送消息", description = "用户向指定聊天室发送消息")
public ApiResponse<Void> sendMessage(
@Parameter(description = "聊天室ID", required = true) @RequestParam String roomId,
@Parameter(description = "发送者用户名", required = true) @RequestParam String username,
@Parameter(description = "消息内容", required = true) @RequestParam @NotBlank String message) {
chatRoomService.sendMessage(roomId, username, message);
return ApiResponse.success();
}
/**
* 获取用户加入的聊天室列表
*
* @param username 用户名
* @return 聊天室列表
*/
@GetMapping("/userChatRooms")
@Operation(summary = "获取用户加入的聊天室", description = "查询指定用户已加入的所有聊天室")
public ApiResponse<List<ChatRoom>> getUserChatRooms(
@Parameter(description = "用户名", required = true) @RequestParam String username) {
List<ChatRoom> chatRooms = chatRoomService.getUserChatRooms(username);
return ApiResponse.success(chatRooms);
}
/**
* 删除聊天室
*
* @param roomId 聊天室ID
* @param username 操作人用户名(必须是创建者)
* @return 删除结果
*/
@PostMapping("/delete")
@Operation(summary = "删除聊天室", description = "删除指定的聊天室,只有创建者可以执行此操作")
public ApiResponse<Void> deleteChatRoom(
@Parameter(description = "聊天室ID", required = true) @RequestParam String roomId,
@Parameter(description = "操作人用户名", required = true) @RequestParam String username) {
chatRoomService.deleteChatRoom(roomId, username);
return ApiResponse.success();
}
}
SysUserMapper.java:
package com.example.easemobchatroomdemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.easemobchatroomdemo.entity.SysUser;
/**
* 用户Mapper接口
*
* @author ken
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
}
ChatRoomMapper.java:
package com.example.easemobchatroomdemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.easemobchatroomdemo.entity.ChatRoom;
/**
* 聊天室Mapper接口
*
* @author ken
*/
public interface ChatRoomMapper extends BaseMapper<ChatRoom> {
}
UserChatRoomMapper.java:
package com.example.easemobchatroomdemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.easemobchatroomdemo.entity.UserChatRoom;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
/**
* 用户聊天室关联Mapper接口
*
* @author ken
*/
public interface UserChatRoomMapper extends BaseMapper<UserChatRoom> {
/**
* 根据聊天室ID更新用户状态
*
* @param roomId 聊天室ID
* @param status 状态
* @param quitTime 退出时间
*/
void updateStatusByRoomId(@Param("roomId") String roomId,
@Param("status") Integer status,
@Param("quitTime") LocalDateTime quitTime);
}
UserChatRoomMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.easemobchatroomdemo.mapper.UserChatRoomMapper">
<!-- 根据聊天室ID更新用户状态 -->
<update id="updateStatusByRoomId">
UPDATE user_chat_room
SET status = #{status},
quit_time = #{quitTime},
updated_time = NOW()
WHERE room_id = #{roomId}
</update>
</mapper>
ObjectUtils.java:
package com.example.easemobchatroomdemo.util;
import org.springframework.util.ObjectUtils as SpringObjectUtils;
/**
* 对象工具类
*
* @author ken
*/
public class ObjectUtils {
/**
* 判断对象是否为空
*
* @param obj 要判断的对象
* @return 如果对象为空则返回true,否则返回false
*/
public static boolean isEmpty(Object obj) {
return SpringObjectUtils.isEmpty(obj);
}
/**
* 判断对象是否不为空
*
* @param obj 要判断的对象
* @return 如果对象不为空则返回true,否则返回false
*/
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
}
完成代码开发后,我们需要进行测试验证。我们可以通过 Swagger UI 来测试各个接口,Swagger UI 的访问地址为:http://localhost:8080/swagger-ui.html。
/api/user/register接口注册两个用户,例如user1和user2user1调用/api/chatroom/create接口创建一个聊天室user2调用/api/chatroom/join接口加入刚创建的聊天室user1和user2分别调用/api/chatroom/sendMessage接口发送消息/api/chatroom/userChatRooms接口验证用户已加入的聊天室user2调用/api/chatroom/leave接口退出聊天室user1调用/api/chatroom/delete接口删除聊天室除了基础的聊天室功能,环信还提供了许多高级功能,可以根据业务需求进行扩展:
通过本文的实战案例,我们展示了如何使用环信 IM SDK 快速集成聊天室功能到 Java 项目中。环信 IM 提供了稳定可靠的底层服务,让我们可以专注于业务逻辑的实现,大大提高了开发效率。
未来,我们可以进一步扩展这个项目,例如:
环信 IM 作为一款成熟的第三方解决方案,不仅降低了 IM 功能的开发门槛,还提供了企业级的稳定性和安全性。无论是小型应用还是大型平台,都可以通过环信 IM 快速构建高质量的即时通讯功能。
希望本文能为您在集成 IM 聊天室功能时提供有价值的参考,让您的项目开发更加高效、顺利!