首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 0 到 1 打造企业级 IM 聊天室:基于环信 SDK 的实战指南

从 0 到 1 打造企业级 IM 聊天室:基于环信 SDK 的实战指南

作者头像
果酱带你啃java
发布2026-04-14 10:41:06
发布2026-04-14 10:41:06
700
举报

在当今数字化时代,即时通讯(IM)功能已成为各类应用的标配。无论是社交软件、电商平台还是企业协同工具,都需要可靠、高效的聊天室功能来增强用户互动。然而,从零开发一套 IM 系统不仅技术复杂度高,还需要应对高并发、消息可靠性等一系列挑战。

作为一名资深开发者,我深知自研 IM 的痛点:需要处理消息存储、推送、离线同步、已读未读等核心功能,还要考虑扩展性和安全性。这就是为什么越来越多的团队选择成熟的第三方 IM 解决方案 —— 它们能让我们专注于业务逻辑,而非重复造轮子。

在众多 IM 服务商中,经过性能测试、API 友好度、文档完整性等多维度对比,环信 IM凭借其企业级稳定性、丰富的功能和灵活的集成方式脱颖而出。本文将带您深入了解环信 IM 的技术优势,并通过一个完整的实战案例,教您如何在 Java 项目中快速集成环信聊天室功能。

一、为什么选择环信 IM?深度技术解析

环信作为国内领先的即时通讯云服务商,已为超过 10 万家企业提供 IM 解决方案。其底层架构经过多年打磨,具备以下核心优势:

1.1 技术架构解析

环信采用分布式微服务架构,确保高可用和可扩展性:

  • 多区域部署:全球多个数据中心,确保低延迟访问
  • 弹性伸缩:基于 K8s 的容器化部署,可根据并发量自动扩缩容
  • 消息可靠性:采用确认机制和持久化存储,确保消息不丢失
  • 安全性:全程 TLS 加密,支持端到端加密选项

1.2 核心功能对比

与其他主流 IM 服务商相比,环信的优势显而易见:

功能

环信 IM

其他服务商 A

其他服务商 B

单聊 / 群聊

✅ 支持

✅ 支持

✅ 支持

聊天室

✅ 支持万人级

❌ 限制 500 人

✅ 支持千人级

消息类型

文本、语音、视频、文件等 10 + 种

基础 5 种类型

基础 7 种类型

离线消息

✅ 无限存储

❌ 7 天限制

✅ 30 天存储

已读回执

✅ 支持

❌ 不支持

✅ 部分支持

自定义消息

✅ 高度灵活

❌ 不支持

✅ 有限支持

多端同步

✅ 全平台一致

✅ 部分支持

✅ 基本支持

开源 UI 组件

✅ 提供

❌ 无

✅ 有限提供

1.3 性能指标(官方数据)

  • 消息送达率:99.99%
  • 消息延迟:平均 < 300ms
  • 并发连接数:支持千万级
  • 单聊天室人数上限:10 万 +
  • 消息吞吐量:每秒处理百万级消息

这些数据意味着环信能够满足从中小型应用到大型企业级系统的各种需求。

二、环信 IM 聊天室核心概念与工作流程

在开始实战前,我们需要先理解环信 IM 的核心概念和工作流程,这将帮助我们更好地设计和实现功能。

2.1 核心概念

  1. AppKey:每个应用的唯一标识,在环信控制台创建应用时生成
  2. 用户 ID(Username):用户在 IM 系统中的唯一标识,需与业务系统用户关联
  3. 聊天室(Chat Room):一个公共的聊天空间,支持大量用户同时在线聊天
  4. 管理员(Admin):拥有聊天室管理权限的用户,可踢人、禁言等
  5. 消息(Message):用户间传递的内容,有多种类型
  6. AccessToken:访问环信 API 的凭证,需要定期刷新

2.2 聊天室工作流程

  1. 服务端创建聊天室并设置基本属性
  2. 用户登录系统后,获取环信 IM 的访问凭证
  3. 用户加入指定聊天室
  4. 用户在聊天室内发送和接收消息
  5. 消息通过环信服务器转发给所有在线成员,离线成员将在上线后收到消息
  6. 用户可随时退出聊天室

三、实战:Spring Boot 集成环信 IM 聊天室

接下来,我们将通过一个完整的实战案例,展示如何在 Spring Boot 项目中集成环信 IM 聊天室功能。我们将实现用户注册、登录、创建聊天室、加入聊天室、发送消息、接收消息等核心功能。

3.1 项目初始化

3.1.1 创建 Maven 项目

首先,我们创建一个 Spring Boot 项目,pom.xml文件如下:

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

3.1.2 配置文件

创建application.yml配置文件:

代码语言:javascript
复制
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
代码语言:javascript
复制

注意:需要将环信相关配置替换为你在环信控制台创建应用后获得的实际信息。

3.1.3 创建数据库表

我们需要创建用户表和聊天室表,用于存储业务系统与环信 IM 的关联信息:

代码语言:javascript
复制
-- 创建数据库
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='用户聊天室关联表';
代码语言:javascript
复制

3.2 项目核心结构

我们的项目采用分层架构设计,结构如下:

代码语言:javascript
复制
com.example.easemobchatroomdemo
├── config           # 配置类
├── controller       # 控制器
├── dto              # 数据传输对象
├── entity           # 实体类
├── exception        # 异常处理
├── mapper           # 数据访问层
├── service          # 业务逻辑层
│   ├── impl         # 业务逻辑实现
│   └── easemob      # 环信相关服务
├── util             # 工具类
└── EasemobChatroomDemoApplication.java  # 启动类
代码语言:javascript
复制

3.3 核心代码实现

3.3.1 启动类
代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

3.3.2 环信配置类
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

3.3.3 实体类

用户实体类

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

聊天室实体类

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

用户聊天室关联实体类

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

3.3.4 DTO 类

用户注册 DTO

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

用户登录 DTO

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

创建聊天室 DTO

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

通用响应 DTO

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

3.3.5 异常处理

自定义异常类

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

全局异常处理器

代码语言:javascript
复制
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("系统异常,请联系管理员");
    }
}
代码语言:javascript
复制

3.3.6 环信服务封装

环信 API 响应实体

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

环信错误信息实体

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

环信 AccessToken 实体

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

环信聊天室实体

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

环信消息实体

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

环信服务接口

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

环信服务实现类

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

3.3.7 业务服务实现

用户服务接口

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

用户服务实现类

代码语言:javascript
复制
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));
    }
}
代码语言:javascript
复制

聊天室服务接口

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

聊天室服务实现类

代码语言:javascript
复制
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());
        }
    }
}
代码语言:javascript
复制

3.3.8 控制器

用户控制器

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

聊天室控制器

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

3.3.9 Mapper 接口及 XML

SysUserMapper.java

代码语言:javascript
复制
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> {
}
代码语言:javascript
复制

ChatRoomMapper.java

代码语言:javascript
复制
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> {
}
代码语言:javascript
复制

UserChatRoomMapper.java

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

UserChatRoomMapper.xml

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

3.3.10 工具类

ObjectUtils.java

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

四、功能测试与验证

完成代码开发后,我们需要进行测试验证。我们可以通过 Swagger UI 来测试各个接口,Swagger UI 的访问地址为:http://localhost:8080/swagger-ui.html

4.1 测试流程

  1. 用户注册:调用/api/user/register接口注册两个用户,例如user1user2
  2. 创建聊天室:使用user1调用/api/chatroom/create接口创建一个聊天室
  3. 加入聊天室:使用user2调用/api/chatroom/join接口加入刚创建的聊天室
  4. 发送消息:user1user2分别调用/api/chatroom/sendMessage接口发送消息
  5. 获取用户聊天室列表:调用/api/chatroom/userChatRooms接口验证用户已加入的聊天室
  6. 退出聊天室:user2调用/api/chatroom/leave接口退出聊天室
  7. 删除聊天室:user1调用/api/chatroom/delete接口删除聊天室

4.2 测试注意事项

  • 确保环信配置信息正确,否则会导致 API 调用失败
  • 测试前需要创建好数据库并执行 SQL 脚本
  • 发送消息后,可以通过环信控制台验证消息是否成功发送

五、环信 IM 高级功能与最佳实践

除了基础的聊天室功能,环信还提供了许多高级功能,可以根据业务需求进行扩展:

5.1 高级功能介绍

  1. 消息撤回:支持撤回已发送的消息
  2. 消息已读回执:获取消息的已读状态
  3. 消息漫游:在不同设备上同步历史消息
  4. 聊天室禁言:可以对特定用户进行禁言操作
  5. 自定义消息类型:支持发送自定义格式的消息,如地理位置、商品信息等
  6. 离线推送:当用户离线时,通过推送通知提醒用户有新消息

5.2 最佳实践

  1. AccessToken 管理
    • 建议使用缓存存储 AccessToken,避免频繁请求
    • 实现自动刷新机制,确保 AccessToken 始终有效
    • 考虑分布式环境下的并发问题
  2. 异常处理
    • 对环信 API 调用进行重试机制,应对网络波动
    • 记录详细的错误日志,便于问题排查
    • 实现降级策略,当环信服务不可用时,保证业务系统基本可用
  3. 性能优化
    • 对频繁访问的聊天室信息进行本地缓存
    • 批量操作 API,减少 HTTP 请求次数
    • 合理设置消息历史的存储策略
  4. 安全性
    • 对用户输入的消息内容进行过滤,防止 XSS 攻击
    • 实现聊天室权限控制,确保只有授权用户才能加入
    • 敏感操作需要二次验证

六、总结与展望

通过本文的实战案例,我们展示了如何使用环信 IM SDK 快速集成聊天室功能到 Java 项目中。环信 IM 提供了稳定可靠的底层服务,让我们可以专注于业务逻辑的实现,大大提高了开发效率。

未来,我们可以进一步扩展这个项目,例如:

  • 集成环信的实时音视频功能
  • 实现更丰富的消息类型,如图片、文件等
  • 开发 Web 端或移动端的聊天界面
  • 加入 AI 聊天机器人功能

环信 IM 作为一款成熟的第三方解决方案,不仅降低了 IM 功能的开发门槛,还提供了企业级的稳定性和安全性。无论是小型应用还是大型平台,都可以通过环信 IM 快速构建高质量的即时通讯功能。

希望本文能为您在集成 IM 聊天室功能时提供有价值的参考,让您的项目开发更加高效、顺利!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么选择环信 IM?深度技术解析
    • 1.1 技术架构解析
    • 1.2 核心功能对比
    • 1.3 性能指标(官方数据)
  • 二、环信 IM 聊天室核心概念与工作流程
    • 2.1 核心概念
    • 2.2 聊天室工作流程
  • 三、实战:Spring Boot 集成环信 IM 聊天室
    • 3.1 项目初始化
      • 3.1.1 创建 Maven 项目
      • 3.1.2 配置文件
      • 3.1.3 创建数据库表
    • 3.2 项目核心结构
    • 3.3 核心代码实现
      • 3.3.1 启动类
      • 3.3.2 环信配置类
      • 3.3.3 实体类
      • 3.3.4 DTO 类
      • 3.3.5 异常处理
      • 3.3.6 环信服务封装
      • 3.3.7 业务服务实现
      • 3.3.8 控制器
      • 3.3.9 Mapper 接口及 XML
      • 3.3.10 工具类
  • 四、功能测试与验证
    • 4.1 测试流程
    • 4.2 测试注意事项
  • 五、环信 IM 高级功能与最佳实践
    • 5.1 高级功能介绍
    • 5.2 最佳实践
  • 六、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档