SpringBoot3 + Netty + UniApp 实时通讯,打造自己的聊天室

SpringBoot3 + Netty + UniApp 即时通讯系统开发详细教程

项目概述

本教程将从零开始,详细介绍如何构建一个完整的即时通讯系统。包含后端SpringBoot服务、Netty WebSocket服务器和UniApp前端应用。

技术栈说明

后端技术栈

  • SpringBoot 3.2.0 - Java Web开发框架,简化配置
  • JDK 17 - Java开发环境
  • Netty 4.1.104 - 高性能网络通信框架,用于WebSocket
  • MySQL 8.0 - 关系型数据库,存储用户和消息数据
  • Redis - 内存数据库,存储用户在线状态
  • Spring Data JPA - 数据访问层,简化数据库操作
  • JWT - JSON Web Token,用于用户身份认证
  • Maven - 项目构建和依赖管理工具

前端技术栈

  • UniApp - 跨平台开发框架,一套代码多端运行
  • Vue 3 - 前端MVVM框架
  • WebSocket - 实时双向通信协议

开发环境准备

1. 安装开发工具

1.1 安装JDK 17
  1. 下载JDK 17:https://www.oracle.com/java/technologies/downloads/
  2. 安装到指定目录(如:D:\Program Files\Java\jdk-17)
  3. 配置环境变量:
    • JAVA_HOME: D:\Program Files\Java\jdk-17
    • Path中添加: %JAVA_HOME%\bin
  4. 验证安装:打开命令行输入 java -version
1.2 安装IDE
  • IntelliJ IDEA(推荐)或 Eclipse
  • HBuilderX(用于UniApp开发)
1.3 安装数据库
  • MySQL 8.0:https://dev.mysql.com/downloads/mysql/
  • Redis:可以使用Docker或直接安装
1.4 安装Maven
  • 下载Maven:https://maven.apache.org/download.cgi
  • 配置环境变量

第一步:创建SpringBoot项目

1.1 使用Spring Initializr创建项目

  1. 访问 https://start.spring.io/
  2. 配置项目信息:
    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0
    • Group: com.zhentao
    • Artifact: Study-Im
    • Java: 17
  3. 添加依赖:
    • Spring Web
    • Spring Data JPA
    • MySQL Driver
    • Spring Data Redis
    • Validation
  4. 生成并下载项目

1.2 项目结构说明

Study-Im/
├── src/main/java/com/zhentao/im/
│   ├── ImApplication.java          # 主启动类
│   ├── config/                     # 配置类
│   ├── controller/                 # 控制器层
│   ├── dto/                        # 数据传输对象
│   ├── entity/                     # 实体类
│   ├── repository/                 # 数据访问层
│   ├── service/                    # 业务逻辑层
│   ├── util/                       # 工具类
│   └── netty/                      # Netty相关类
├── src/main/resources/
│   ├── application.yml             # 配置文件
│   └── db/                         # 数据库脚本
└── pom.xml                         # Maven配置文件
```## 第二步
:配置Maven依赖

### 2.1 编辑pom.xml文件
在项目根目录的pom.xml中添加所需依赖:

```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 
         http://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.0</version>
        <relativePath/>
    </parent>

    <groupId>com.zhentao</groupId>
    <artifactId>Study-Im</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <netty.version>4.1.104.Final</netty.version>
        <jwt.version>4.4.0</jwt.version>
    </properties>

    <dependencies>
        <!-- SpringBoot Web启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- SpringBoot WebSocket启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        
        <!-- SpringBoot JPA启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- SpringBoot Redis启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!-- SpringBoot 验证启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Netty网络框架 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>

        <!-- MySQL数据库驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- JSON处理库 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.43</version>
        </dependency>

        <!-- JWT身份认证 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>

        <!-- Lombok代码简化 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2.2 依赖说明

  • spring-boot-starter-web: 提供Web开发基础功能
  • spring-boot-starter-websocket: WebSocket支持
  • spring-boot-starter-data-jpa: 数据库操作简化
  • spring-boot-starter-data-redis: Redis操作支持
  • netty-all: 高性能网络通信框架
  • mysql-connector-j: MySQL数据库连接驱动
  • fastjson2: JSON数据处理
  • java-jwt: JWT令牌生成和验证
  • lombok: 减少样板代码(如getter/setter)

第三步:配置应用程序

3.1 创建application.yml配置文件

在src/main/resources/目录下创建application.yml:

# 服务器配置
server:
  port: 8080                        # HTTP服务端口

spring:
  application:
    name: study-im                  # 应用名称
  
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/study_im?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root                  # 数据库用户名
    password: root                  # 数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update              # 自动更新数据库表结构
    show-sql: true                  # 显示SQL语句
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
  
  # Redis配置
  redis:
    host: # Redis服务器地址
    port: 6379                      # Redis端口
    password: # Redis密码
    database: 0                     # 使用的数据库编号
    timeout: 3000ms                 # 连接超时时间
    lettuce:
      pool:
        max-active: 8               # 最大连接数
        max-wait: -1ms              # 最大等待时间
        max-idle: 8                 # 最大空闲连接
        min-idle: 0                 # 最小空闲连接

# Netty配置
netty:
  server:
    port: 9999                      # WebSocket服务端口
    boss-threads: 1                 # Boss线程数
    worker-threads: 4               # Worker线程数

# JWT配置
jwt:
  secret: study-im-secret-key-2024  # JWT密钥
  expiration: 86400000              # 过期时间(24小时)

# 日志配置
logging:
  level:
    com.zhentao.im: debug           # 项目日志级别
    org.springframework.web: debug  # Spring Web日志级别

3.2 配置说明

  • server.port: HTTP服务监听端口
  • datasource: 数据库连接配置
  • jpa.hibernate.ddl-auto:
    • update: 自动更新表结构
    • create: 每次启动重新创建表
    • none: 不自动操作表结构
  • redis: Redis缓存服务器配置
  • netty: WebSocket服务器配置
  • jwt: 用户身份认证配置## 第四步:创
    建数据库实体类

4.1 创建用户实体类

在src/main/java/com/zhentao/im/entity/目录下创建User.java:

package com.zhentao.im.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体类
 * @Data 注解自动生成getter/setter方法
 * @Entity 标记这是一个JPA实体
 * @Table 指定数据库表名
 */
@Data
@Entity
@Table(name = "users")
public class User {
    
    @Id                                     // 主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // 自增主键
    private Long id;
    
    @Column(unique = true, nullable = false)  // 唯一且非空
    private String username;                  // 用户名
    
    @Column(nullable = false)
    private String password;                  // 密码
    
    private String nickname;                  // 昵称
    
    private String avatar;                    // 头像URL
    
    @Enumerated(EnumType.STRING)             // 枚举类型存储为字符串
    private UserStatus status = UserStatus.OFFLINE;  // 用户状态
    
    @Column(name = "last_login_time")
    private LocalDateTime lastLoginTime;      // 最后登录时间
    
    @Column(name = "create_time")
    private LocalDateTime createTime = LocalDateTime.now();  // 创建时间
    
    /**
     * 用户状态枚举
     */
    public enum UserStatus {
        ONLINE,     // 在线
        OFFLINE,    // 离线
        BUSY        // 忙碌
    }
}

4.2 创建消息实体类

在同目录下创建Message.java:

package com.zhentao.im.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 消息实体类
 */
@Data
@Entity
@Table(name = "messages")
public class Message {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "from_user_id", nullable = false)
    private Long fromUserId;                  // 发送者ID
    
    @Column(name = "to_user_id", nullable = false)
    private Long toUserId;                    // 接收者ID
    
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;                   // 消息内容
    
    @Enumerated(EnumType.STRING)
    private MessageType type = MessageType.TEXT;  // 消息类型
    
    @Enumerated(EnumType.STRING)
    private MessageStatus status = MessageStatus.SENT;  // 消息状态
    
    @Column(name = "send_time")
    private LocalDateTime sendTime = LocalDateTime.now();  // 发送时间
    
    @Column(name = "read_time")
    private LocalDateTime readTime;           // 阅读时间
    
    /**
     * 消息类型枚举
     */
    public enum MessageType {
        TEXT,       // 文本消息
        IMAGE,      // 图片消息
        FILE,       // 文件消息
        SYSTEM      // 系统消息
    }
    
    /**
     * 消息状态枚举
     */
    public enum MessageStatus {
        SENT,       // 已发送
        DELIVERED,  // 已送达
        READ        // 已读
    }
}

4.3 实体类注解说明

  • @Data: Lombok注解,自动生成getter/setter/toString等方法
  • @Entity: JPA注解,标记这是一个数据库实体
  • @Table: 指定对应的数据库表名
  • @Id: 标记主键字段
  • @GeneratedValue: 主键生成策略,IDENTITY表示自增
  • @Column: 指定列的属性(唯一性、非空、列名等)
  • @Enumerated: 枚举类型的存储方式

第五步:创建数据访问层

5.1 创建用户Repository

在src/main/java/com/zhentao/im/repository/目录下创建UserRepository.java:

package com.zhentao.im.repository;

import com.zhentao.im.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

/**
 * 用户数据访问接口
 * JpaRepository提供基本的CRUD操作
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     * Spring Data JPA会自动实现这个方法
     */
    Optional<User> findByUsername(String username);
    
    /**
     * 检查用户名是否存在
     */
    boolean existsByUsername(String username);
}

5.2 创建消息Repository

在同目录下创建MessageRepository.java:

package com.zhentao.im.repository;

import com.zhentao.im.entity.Message;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

/**
 * 消息数据访问接口
 */
@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {
    
    /**
     * 查询两个用户之间的聊天记录
     * 使用JPQL查询语言
     */
    @Query("SELECT m FROM Message m WHERE " +
           "(m.fromUserId = :userId1 AND m.toUserId = :userId2) OR " +
           "(m.fromUserId = :userId2 AND m.toUserId = :userId1) " +
           "ORDER BY m.sendTime ASC")
    List<Message> findChatHistory(@Param("userId1") Long userId1, 
                                  @Param("userId2") Long userId2);
    
    /**
     * 查询发送给指定用户的消息
     */
    List<Message> findByToUserIdOrderBySendTimeDesc(Long toUserId);
}

5.3 Repository说明

  • JpaRepository<Entity, ID>: Spring Data JPA提供的基础接口
    • Entity: 实体类型
    • ID: 主键类型
  • 自动方法实现: Spring根据方法名自动生成实现
    • findBy + 字段名: 根据字段查询
    • existsBy + 字段名: 检查是否存在
  • @Query: 自定义JPQL查询语句
  • @Param: 绑定查询参数##
    第六步:创建数据传输对象(DTO)

6.1 创建统一响应格式

在src/main/java/com/zhentao/im/dto/目录下创建ApiResponse.java:

package com.zhentao.im.dto;

import lombok.Data;

/**
 * 统一API响应格式
 * 所有接口都使用这个格式返回数据
 */
@Data
public class ApiResponse<T> {
    private int code;           // 响应码:200成功,其他失败
    private String message;     // 响应消息
    private T data;            // 响应数据
    
    /**
     * 成功响应
     */
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("success");
        response.setData(data);
        return response;
    }
    
    /**
     * 失败响应
     */
    public static <T> ApiResponse<T> error(String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(500);
        response.setMessage(message);
        return response;
    }
    
    /**
     * 自定义响应码的失败响应
     */
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        return response;
    }
}

6.2 创建登录请求DTO

创建LoginRequest.java:

package com.zhentao.im.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * 登录请求数据传输对象
 */
@Data
public class LoginRequest {
    
    @NotBlank(message = "用户名不能为空")
    private String username;    // 用户名
    
    @NotBlank(message = "密码不能为空")
    private String password;    // 密码
}

6.3 创建注册请求DTO

创建RegisterRequest.java:

package com.zhentao.im.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * 注册请求数据传输对象
 */
@Data
public class RegisterRequest {
    
    @NotBlank(message = "用户名不能为空")
    private String username;    // 用户名
    
    @NotBlank(message = "密码不能为空")
    private String password;    // 密码
    
    private String nickname;    // 昵称(可选)
}

6.4 创建消息DTO

创建MessageDto.java:

package com.zhentao.im.dto;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 消息数据传输对象
 * 用于前后端消息传输
 */
@Data
public class MessageDto {
    private Long id;                    // 消息ID
    private Long fromUserId;            // 发送者ID
    private Long toUserId;              // 接收者ID
    private String content;             // 消息内容
    private String type;                // 消息类型
    private LocalDateTime sendTime;     // 发送时间
    private String fromUsername;        // 发送者用户名
    private String toUsername;          // 接收者用户名
}

6.5 DTO说明

  • DTO (Data Transfer Object): 数据传输对象,用于不同层之间传递数据
  • @NotBlank: 验证注解,确保字段不为空
  • 泛型: 使ApiResponse可以包装任何类型的数据
  • 静态方法: 提供便捷的创建方法

第七步:创建工具类

7.1 创建JWT工具类

在src/main/java/com/zhentao/im/util/目录下创建JwtUtil.java:

package com.zhentao.im.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * JWT工具类
 * 用于生成和验证JWT令牌
 */
@Component
public class JwtUtil {
    
    @Value("${jwt.secret}")         // 从配置文件读取密钥
    private String secret;
    
    @Value("${jwt.expiration}")     // 从配置文件读取过期时间
    private Long expiration;
    
    /**
     * 生成JWT令牌
     * @param userId 用户ID
     * @param username 用户名
     * @return JWT令牌字符串
     */
    public String generateToken(Long userId, String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return JWT.create()
                .withSubject(username)                  // 主题(用户名)
                .withClaim("userId", userId)            // 自定义声明(用户ID)
                .withIssuedAt(now)                      // 签发时间
                .withExpiresAt(expiryDate)              // 过期时间
                .sign(Algorithm.HMAC256(secret));       // 签名算法
    }
    
    /**
     * 验证JWT令牌
     * @param token JWT令牌
     * @return 解码后的JWT对象
     */
    public DecodedJWT verifyToken(String token) {
        try {
            return JWT.require(Algorithm.HMAC256(secret))
                    .build()
                    .verify(token);
        } catch (JWTVerificationException e) {
            throw new RuntimeException("无效的JWT令牌", e);
        }
    }
    
    /**
     * 从令牌中获取用户名
     */
    public String getUsernameFromToken(String token) {
        return verifyToken(token).getSubject();
    }
    
    /**
     * 从令牌中获取用户ID
     */
    public Long getUserIdFromToken(String token) {
        return verifyToken(token).getClaim("userId").asLong();
    }
    
    /**
     * 检查令牌是否过期
     */
    public boolean isTokenExpired(String token) {
        try {
            Date expiration = verifyToken(token).getExpiresAt();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }
}

7.2 JWT工具类说明

  • @Component: Spring组件注解,自动注入到容器
  • @Value: 从配置文件注入值
  • JWT.create(): 创建JWT构建器
  • withSubject(): 设置主题(通常是用户标识)
  • withClaim(): 添加自定义声明
  • sign(): 使用指定算法签名
  • verify(): 验证JWT令牌的有效性

第八步:创建Redis配置

8.1 创建Redis配置类

在src/main/java/com/zhentao/im/config/目录下创建RedisConfig.java:

package com.zhentao.im.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 * 配置Redis序列化方式
 */
@Configuration
public class RedisConfig {
    
    /**
     * 配置RedisTemplate
     * RedisTemplate是Spring操作Redis的核心类
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 设置key的序列化方式为String
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        
        // 设置value的序列化方式为JSON
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        template.afterPropertiesSet();
        return template;
    }
}

8.2 Redis配置说明

  • @Configuration: 标记这是一个配置类
  • @Bean: 将方法返回的对象注册为Spring Bean
  • RedisTemplate: Spring操作Redis的模板类
  • 序列化器:
    • StringRedisSerializer: 字符串序列化
    • GenericJackson2JsonRedisSerializer: JSON序列化## 第九步
      :创建业务逻辑层

9.1 创建用户服务类

在src/main/java/com/zhentao/im/service/目录下创建UserService.java:

package com.zhentao.im.service;

import com.zhentao.im.dto.LoginRequest;
import com.zhentao.im.dto.RegisterRequest;
import com.zhentao.im.entity.User;
import com.zhentao.im.repository.UserRepository;
import com.zhentao.im.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 用户业务逻辑服务类
 */
@Service
@RequiredArgsConstructor  // Lombok注解,自动生成构造函数
public class UserService {
    
    // 依赖注入,final字段会通过构造函数注入
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;
    private final RedisTemplate<String, Object> redisTemplate;
    private final InMemoryUserStatusService inMemoryUserStatusService;
    
    /**
     * 用户注册
     * @param request 注册请求数据
     * @return 包含token和用户信息的Map
     */
    public Map<String, Object> register(RegisterRequest request) {
        // 1. 检查用户名是否已存在
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new RuntimeException("用户名已存在");
        }
        
        // 2. 创建新用户
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(request.getPassword()); // 实际项目中应该加密密码
        user.setNickname(request.getNickname() != null ? 
                        request.getNickname() : request.getUsername());
        user.setCreateTime(LocalDateTime.now());
        
        // 3. 保存到数据库
        user = userRepository.save(user);
        
        // 4. 生成JWT令牌
        String token = jwtUtil.generateToken(user.getId(), user.getUsername());
        
        // 5. 返回结果
        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("user", user);
        
        return result;
    }
    
    /**
     * 用户登录
     * @param request 登录请求数据
     * @return 包含token和用户信息的Map
     */
    public Map<String, Object> login(LoginRequest request) {
        // 1. 查找用户
        Optional<User> userOpt = userRepository.findByUsername(request.getUsername());
        if (userOpt.isEmpty() || !userOpt.get().getPassword().equals(request.getPassword())) {
            throw new RuntimeException("用户名或密码错误");
        }
        
        // 2. 更新用户状态
        User user = userOpt.get();
        user.setLastLoginTime(LocalDateTime.now());
        user.setStatus(User.UserStatus.ONLINE);
        userRepository.save(user);
        
        // 3. 生成JWT令牌
        String token = jwtUtil.generateToken(user.getId(), user.getUsername());
        
        // 4. 将用户在线状态存储到Redis,如果失败则使用内存存储
        try {
            redisTemplate.opsForValue().set("user:online:" + user.getId(), true, 24, TimeUnit.HOURS);
        } catch (Exception e) {
            System.out.println("Redis连接失败,使用内存存储: " + e.getMessage());
            inMemoryUserStatusService.setUserOnline(user.getId());
        }
        
        // 5. 返回结果
        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("user", user);
        
        return result;
    }
    
    /**
     * 用户退出登录
     * @param userId 用户ID
     */
    public void logout(Long userId) {
        Optional<User> userOpt = userRepository.findById(userId);
        if (userOpt.isPresent()) {
            // 1. 更新数据库中的用户状态
            User user = userOpt.get();
            user.setStatus(User.UserStatus.OFFLINE);
            userRepository.save(user);
            
            // 2. 从Redis中移除在线状态,如果失败则从内存中移除
            try {
                redisTemplate.delete("user:online:" + userId);
            } catch (Exception e) {
                System.out.println("Redis连接失败,使用内存存储: " + e.getMessage());
                inMemoryUserStatusService.setUserOffline(userId);
            }
        }
    }
    
    /**
     * 根据ID获取用户
     * @param userId 用户ID
     * @return 用户对象
     */
    public User getUserById(Long userId) {
        return userRepository.findById(userId).orElse(null);
    }
    
    /**
     * 检查用户是否在线
     * @param userId 用户ID
     * @return true-在线,false-离线
     */
    public boolean isUserOnline(Long userId) {
        try {
            return Boolean.TRUE.equals(redisTemplate.hasKey("user:online:" + userId));
        } catch (Exception e) {
            System.out.println("Redis连接失败,使用内存存储: " + e.getMessage());
            return inMemoryUserStatusService.isUserOnline(userId);
        }
    }
}

9.2 创建消息服务类

创建MessageService.java:

package com.zhentao.im.service;

import com.zhentao.im.dto.MessageDto;
import com.zhentao.im.entity.Message;
import com.zhentao.im.entity.User;
import com.zhentao.im.repository.MessageRepository;
import com.zhentao.im.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 消息业务逻辑服务类
 */
@Service
@RequiredArgsConstructor
public class MessageService {
    
    private final MessageRepository messageRepository;
    private final UserRepository userRepository;
    
    /**
     * 保存消息到数据库
     * @param message 消息对象
     * @return 保存后的消息
     */
    public Message saveMessage(Message message) {
        return messageRepository.save(message);
    }
    
    /**
     * 获取两个用户之间的聊天历史
     * @param userId1 用户1的ID
     * @param userId2 用户2的ID
     * @return 消息DTO列表
     */
    public List<MessageDto> getChatHistory(Long userId1, Long userId2) {
        // 1. 从数据库查询消息
        List<Message> messages = messageRepository.findChatHistory(userId1, userId2);
        
        // 2. 转换为DTO对象
        return messages.stream().map(this::convertToDto).collect(Collectors.toList());
    }
    
    /**
     * 将Message实体转换为MessageDto
     * @param message 消息实体
     * @return 消息DTO
     */
    private MessageDto convertToDto(Message message) {
        MessageDto dto = new MessageDto();
        dto.setId(message.getId());
        dto.setFromUserId(message.getFromUserId());
        dto.setToUserId(message.getToUserId());
        dto.setContent(message.getContent());
        dto.setType(message.getType().name());
        dto.setSendTime(message.getSendTime());
        
        // 查询用户名
        User fromUser = userRepository.findById(message.getFromUserId()).orElse(null);
        User toUser = userRepository.findById(message.getToUserId()).orElse(null);
        
        if (fromUser != null) {
            dto.setFromUsername(fromUser.getNickname());
        }
        if (toUser != null) {
            dto.setToUsername(toUser.getNickname());
        }
        
        return dto;
    }
}

9.3 创建内存用户状态服务(Redis备用方案)

创建InMemoryUserStatusService.java:

package com.zhentao.im.service;

import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 内存用户状态服务
 * 当Redis不可用时的备用方案
 */
@Service
public class InMemoryUserStatusService {
    
    // 线程安全的HashMap,存储用户在线状态
    private final ConcurrentHashMap<Long, Long> onlineUsers = new ConcurrentHashMap<>();
    
    // 定时任务执行器
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public InMemoryUserStatusService() {
        // 每小时清理过期的在线状态
        scheduler.scheduleAtFixedRate(this::cleanExpiredUsers, 1, 1, TimeUnit.HOURS);
    }
    
    /**
     * 设置用户在线
     * @param userId 用户ID
     */
    public void setUserOnline(Long userId) {
        // 存储用户ID和过期时间(24小时后)
        onlineUsers.put(userId, System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24));
    }
    
    /**
     * 设置用户离线
     * @param userId 用户ID
     */
    public void setUserOffline(Long userId) {
        onlineUsers.remove(userId);
    }
    
    /**
     * 检查用户是否在线
     * @param userId 用户ID
     * @return true-在线,false-离线
     */
    public boolean isUserOnline(Long userId) {
        Long expireTime = onlineUsers.get(userId);
        if (expireTime == null) {
            return false;
        }
        
        // 检查是否过期
        if (System.currentTimeMillis() > expireTime) {
            onlineUsers.remove(userId);
            return false;
        }
        
        return true;
    }
    
    /**
     * 清理过期的用户状态
     */
    private void cleanExpiredUsers() {
        long currentTime = System.currentTimeMillis();
        onlineUsers.entrySet().removeIf(entry -> currentTime > entry.getValue());
    }
}

9.4 业务逻辑层说明

  • @Service: 标记这是一个业务逻辑层组件
  • @RequiredArgsConstructor: 自动生成包含final字段的构造函数
  • Optional: Java 8的容器类,避免空指针异常
  • Stream API: Java 8的流式处理,用于集合操作
  • ConcurrentHashMap: 线程安全的HashMap
  • ScheduledExecutorService: 定时任务执行器## 第十
    步:创建控制器层

10.1 创建认证控制器

在src/main/java/com/zhentao/im/controller/目录下创建AuthController.java:

package com.zhentao.im.controller;

import com.zhentao.im.dto.ApiResponse;
import com.zhentao.im.dto.LoginRequest;
import com.zhentao.im.dto.RegisterRequest;
import com.zhentao.im.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 认证控制器
 * 处理用户登录、注册、退出等请求
 */
@RestController                    // REST风格控制器
@RequestMapping("/api/auth")       // 请求路径前缀
@RequiredArgsConstructor          // 自动生成构造函数
@CrossOrigin(origins = "*")       // 允许跨域请求
public class AuthController {
    
    private final UserService userService;
    
    /**
     * 用户注册接口
     * POST /api/auth/register
     */
    @PostMapping("/register")
    public ApiResponse<Map<String, Object>> register(@Validated @RequestBody RegisterRequest request) {
        try {
            Map<String, Object> result = userService.register(request);
            return ApiResponse.success(result);
        } catch (Exception e) {
            return ApiResponse.error(e.getMessage());
        }
    }
    
    /**
     * 用户登录接口
     * POST /api/auth/login
     */
    @PostMapping("/login")
    public ApiResponse<Map<String, Object>> login(@Validated @RequestBody LoginRequest request) {
        try {
            Map<String, Object> result = userService.login(request);
            return ApiResponse.success(result);
        } catch (Exception e) {
            return ApiResponse.error(e.getMessage());
        }
    }
    
    /**
     * 用户退出登录接口
     * POST /api/auth/logout
     */
    @PostMapping("/logout")
    public ApiResponse<Void> logout(@RequestHeader("Authorization") String token) {
        // 这里应该从token中解析用户ID,简化处理
        return ApiResponse.success(null);
    }
}

10.2 创建消息控制器

创建MessageController.java:

package com.zhentao.im.controller;

import com.zhentao.im.dto.ApiResponse;
import com.zhentao.im.dto.MessageDto;
import com.zhentao.im.service.MessageService;
import com.zhentao.im.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 消息控制器
 * 处理消息相关的HTTP请求
 */
@RestController
@RequestMapping("/api/messages")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class MessageController {
    
    private final MessageService messageService;
    private final JwtUtil jwtUtil;
    
    /**
     * 获取聊天历史记录
     * GET /api/messages/history/{toUserId}
     */
    @GetMapping("/history/{toUserId}")
    public ApiResponse<List<MessageDto>> getChatHistory(
            @PathVariable Long toUserId,                    // 路径参数
            @RequestHeader("Authorization") String authHeader) {  // 请求头参数
        
        try {
            // 1. 从Authorization头中提取token
            String token = authHeader.replace("Bearer ", "");
            
            // 2. 从token中获取当前用户ID
            Long currentUserId = jwtUtil.getUserIdFromToken(token);
            
            // 3. 查询聊天历史
            List<MessageDto> messages = messageService.getChatHistory(currentUserId, toUserId);
            
            return ApiResponse.success(messages);
        } catch (Exception e) {
            return ApiResponse.error(e.getMessage());
        }
    }
    
    /**
     * 标记消息为已读
     * POST /api/messages/{messageId}/read
     */
    @PostMapping("/{messageId}/read")
    public ApiResponse<Void> markAsRead(@PathVariable Long messageId) {
        // 实现消息已读功能(这里简化处理)
        return ApiResponse.success(null);
    }
}

10.3 创建健康检查控制器

创建HealthController.java:

package com.zhentao.im.controller;

import com.zhentao.im.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;

/**
 * 健康检查控制器
 * 用于检查系统各组件的运行状态
 */
@RestController
@RequestMapping("/api/health")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class HealthController {
    
    private final DataSource dataSource;
    private final RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 系统健康检查
     * GET /api/health
     */
    @GetMapping
    public ApiResponse<Map<String, Object>> health() {
        Map<String, Object> status = new HashMap<>();
        
        // 检查数据库连接
        try (Connection connection = dataSource.getConnection()) {
            status.put("database", "UP");
        } catch (Exception e) {
            status.put("database", "DOWN - " + e.getMessage());
        }
        
        // 检查Redis连接
        try {
            redisTemplate.opsForValue().set("health:check", "ok");
            String result = (String) redisTemplate.opsForValue().get("health:check");
            status.put("redis", "ok".equals(result) ? "UP" : "DOWN");
        } catch (Exception e) {
            status.put("redis", "DOWN - " + e.getMessage());
        }
        
        status.put("application", "UP");
        status.put("timestamp", System.currentTimeMillis());
        
        return ApiResponse.success(status);
    }
}

10.4 控制器层注解说明

  • @RestController: 组合注解,等于@Controller + @ResponseBody
  • @RequestMapping: 指定请求路径前缀
  • @PostMapping/@GetMapping: 指定HTTP方法和路径
  • @RequestBody: 将请求体JSON转换为Java对象
  • @PathVariable: 获取URL路径中的参数
  • @RequestHeader: 获取请求头中的参数
  • @Validated: 启用参数验证
  • @CrossOrigin: 允许跨域请求

第十一步:创建Netty WebSocket服务器

11.1 创建WebSocket处理器

在src/main/java/com/zhentao/im/netty/目录下创建WebSocketHandler.java:

package com.zhentao.im.netty;

import com.alibaba.fastjson2.JSON;
import com.zhentao.im.entity.Message;
import com.zhentao.im.service.MessageService;
import com.zhentao.im.util.JwtUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket消息处理器
 * 处理WebSocket连接和消息
 */
@Slf4j
@Component
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    
    // 存储用户ID和Channel的映射关系
    private static final Map<Long, ChannelHandlerContext> USER_CHANNELS = new ConcurrentHashMap<>();
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private MessageService messageService;
    
    /**
     * 连接建立时调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("WebSocket连接建立: {}", ctx.channel().id());
        super.channelActive(ctx);
    }
    
    /**
     * 连接断开时调用
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("WebSocket连接断开: {}", ctx.channel().id());
        
        // 从映射中移除断开的连接
        USER_CHANNELS.entrySet().removeIf(entry -> entry.getValue() == ctx);
        
        super.channelInactive(ctx);
    }
    
    /**
     * 接收到消息时调用
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
        String text = frame.text();
        log.info("收到WebSocket消息: {}", text);
        
        try {
            // 解析JSON消息
            Map<String, Object> message = JSON.parseObject(text, Map.class);
            String type = (String) message.get("type");
            
            switch (type) {
                case "auth":
                    handleAuth(ctx, message);
                    break;
                case "chat":
                    handleChat(ctx, message);
                    break;
                case "heartbeat":
                    handleHeartbeat(ctx);
                    break;
                default:
                    log.warn("未知消息类型: {}", type);
            }
        } catch (Exception e) {
            log.error("处理WebSocket消息失败", e);
            sendErrorMessage(ctx, "消息处理失败: " + e.getMessage());
        }
    }
    
    /**
     * 处理认证消息
     */
    private void handleAuth(ChannelHandlerContext ctx, Map<String, Object> message) {
        try {
            String token = (String) message.get("token");
            
            // 验证JWT令牌
            Long userId = jwtUtil.getUserIdFromToken(token);
            
            // 将用户ID和Channel关联
            USER_CHANNELS.put(userId, ctx);
            
            // 发送认证成功消息
            sendMessage(ctx, Map.of(
                "type", "auth_success",
                "message", "认证成功",
                "userId", userId
            ));
            
            log.info("用户 {} 认证成功", userId);
        } catch (Exception e) {
            sendErrorMessage(ctx, "认证失败: " + e.getMessage());
        }
    }
    
    /**
     * 处理聊天消息
     */
    private void handleChat(ChannelHandlerContext ctx, Map<String, Object> message) {
        try {
            // 获取发送者ID
            Long fromUserId = getUserIdByChannel(ctx);
            if (fromUserId == null) {
                sendErrorMessage(ctx, "请先进行身份认证");
                return;
            }
            
            // 获取消息内容
            Long toUserId = Long.valueOf(message.get("toUserId").toString());
            String content = (String) message.get("content");
            
            // 保存消息到数据库
            Message chatMessage = new Message();
            chatMessage.setFromUserId(fromUserId);
            chatMessage.setToUserId(toUserId);
            chatMessage.setContent(content);
            chatMessage.setType(Message.MessageType.TEXT);
            chatMessage.setSendTime(LocalDateTime.now());
            
            Message savedMessage = messageService.saveMessage(chatMessage);
            
            // 构造转发消息
            Map<String, Object> forwardMessage = Map.of(
                "type", "chat",
                "messageId", savedMessage.getId(),
                "fromUserId", fromUserId,
                "toUserId", toUserId,
                "content", content,
                "sendTime", savedMessage.getSendTime().toString()
            );
            
            // 发送给接收者
            ChannelHandlerContext toChannel = USER_CHANNELS.get(toUserId);
            if (toChannel != null) {
                sendMessage(toChannel, forwardMessage);
            }
            
            // 发送确认消息给发送者
            sendMessage(ctx, Map.of(
                "type", "message_sent",
                "messageId", savedMessage.getId(),
                "status", "success"
            ));
            
            log.info("消息发送成功: {} -> {}", fromUserId, toUserId);
        } catch (Exception e) {
            sendErrorMessage(ctx, "发送消息失败: " + e.getMessage());
        }
    }
    
    /**
     * 处理心跳消息
     */
    private void handleHeartbeat(ChannelHandlerContext ctx) {
        sendMessage(ctx, Map.of("type", "heartbeat", "timestamp", System.currentTimeMillis()));
    }
    
    /**
     * 发送消息到客户端
     */
    private void sendMessage(ChannelHandlerContext ctx, Map<String, Object> message) {
        String json = JSON.toJSONString(message);
        ctx.writeAndFlush(new TextWebSocketFrame(json));
    }
    
    /**
     * 发送错误消息
     */
    private void sendErrorMessage(ChannelHandlerContext ctx, String error) {
        sendMessage(ctx, Map.of("type", "error", "message", error));
    }
    
    /**
     * 根据Channel获取用户ID
     */
    private Long getUserIdByChannel(ChannelHandlerContext ctx) {
        return USER_CHANNELS.entrySet().stream()
                .filter(entry -> entry.getValue() == ctx)
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);
    }
    
    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("WebSocket异常", cause);
        ctx.close();
    }
}
```#
## 11.2 创建Netty服务器
创建NettyServer.java:

```java
package com.zhentao.im.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
 * Netty WebSocket服务器
 * 提供WebSocket连接服务
 */
@Slf4j
@Component
public class NettyServer {
    
    @Value("${netty.server.port}")
    private int port;
    
    @Value("${netty.server.boss-threads}")
    private int bossThreads;
    
    @Value("${netty.server.worker-threads}")
    private int workerThreads;
    
    @Autowired
    private WebSocketHandler webSocketHandler;
    
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private Channel serverChannel;
    
    /**
     * 启动Netty服务器
     * @PostConstruct 注解表示在Bean初始化后自动调用
     */
    @PostConstruct
    public void start() {
        new Thread(() -> {
            try {
                // 1. 创建事件循环组
                bossGroup = new NioEventLoopGroup(bossThreads);      // 处理连接
                workerGroup = new NioEventLoopGroup(workerThreads);  // 处理I/O
                
                // 2. 创建服务器启动器
                ServerBootstrap bootstrap = new ServerBootstrap();
                bootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)           // 使用NIO
                        .option(ChannelOption.SO_BACKLOG, 128)          // 连接队列大小
                        .childOption(ChannelOption.SO_KEEPALIVE, true)  // 保持连接
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                
                                // 3. 添加处理器链
                                // HTTP编解码器
                                pipeline.addLast(new HttpServerCodec());
                                
                                // HTTP对象聚合器,将多个HTTP消息聚合成一个完整的HTTP消息
                                pipeline.addLast(new HttpObjectAggregator(65536));
                                
                                // 支持大文件传输
                                pipeline.addLast(new ChunkedWriteHandler());
                                
                                // WebSocket协议处理器
                                pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                                
                                // 自定义WebSocket消息处理器
                                pipeline.addLast(webSocketHandler);
                            }
                        });
                
                // 4. 绑定端口并启动服务器
                ChannelFuture future = bootstrap.bind(port).sync();
                serverChannel = future.channel();
                
                log.info("Netty WebSocket服务器启动成功,端口: {}", port);
                
                // 5. 等待服务器关闭
                future.channel().closeFuture().sync();
                
            } catch (Exception e) {
                log.error("Netty服务器启动失败", e);
            } finally {
                shutdown();
            }
        }, "netty-server").start();
    }
    
    /**
     * 关闭Netty服务器
     * @PreDestroy 注解表示在Bean销毁前自动调用
     */
    @PreDestroy
    public void shutdown() {
        log.info("正在关闭Netty服务器...");
        
        if (serverChannel != null) {
            serverChannel.close();
        }
        
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
        
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        
        log.info("Netty服务器已关闭");
    }
}

11.3 Netty相关概念说明

  • EventLoopGroup: 事件循环组,处理I/O操作
    • BossGroup: 处理客户端连接
    • WorkerGroup: 处理已建立连接的I/O操作
  • Channel: 网络连接的抽象
  • ChannelPipeline: 处理器链,按顺序处理消息
  • ChannelHandler: 消息处理器
  • Bootstrap: 服务器启动器,配置服务器参数

第十二步:创建主启动类

12.1 编辑主启动类

编辑src/main/java/com/zhentao/im/ImApplication.java:

package com.zhentao.im;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * SpringBoot主启动类
 * @SpringBootApplication 是一个组合注解,包含:
 * - @Configuration: 标记为配置类
 * - @EnableAutoConfiguration: 启用自动配置
 * - @ComponentScan: 自动扫描组件
 */
@SpringBootApplication
public class ImApplication {
    
    /**
     * 程序入口点
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        SpringApplication.run(ImApplication.class, args);
    }
}

12.2 启动类说明

  • @SpringBootApplication: SpringBoot核心注解
  • SpringApplication.run(): 启动SpringBoot应用
  • 自动扫描当前包及子包下的所有组件

第十三步:数据库初始化

13.1 创建数据库

在MySQL中执行以下SQL创建数据库:

-- 创建数据库
CREATE DATABASE IF NOT EXISTS study_im 
DEFAULT CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE study_im;

13.2 JPA自动创建表

由于我们在application.yml中配置了ddl-auto: update,JPA会自动根据实体类创建数据库表:

  • users表: 存储用户信息
  • messages表: 存储聊天消息

13.3 插入测试数据(可选)

-- 插入测试用户
INSERT INTO users (username, password, nickname, create_time) VALUES 
('user1', '123456', '用户1', NOW()),
('user2', '123456', '用户2', NOW());

-- 插入测试消息
INSERT INTO messages (from_user_id, to_user_id, content, send_time) VALUES 
(1, 2, '你好!这是第一条测试消息', NOW()),
(2, 1, '你好!收到了', NOW());

第十四步:启动和测试后端

14.1 启动应用

  1. 确保MySQL和Redis服务正在运行
  2. 在IDE中运行ImApplication.java
  3. 或使用Maven命令:mvn spring-boot:run

14.2 检查启动日志

启动成功后应该看到类似日志:

Started ImApplication in 3.456 seconds
Netty WebSocket服务器启动成功,端口: 9999

14.3 测试接口

使用Postman或浏览器测试:

  1. 健康检查: GET http://localhost:8080/api/health
  2. 用户注册: POST http://localhost:8080/api/auth/register
    {
      "username": "testuser",
      "password": "123456",
      "nickname": "测试用户"
    }
    
  3. 用户登录: POST http://localhost:8080/api/auth/login
    {
      "username": "testuser",
      "password": "123456"
    }
    

14.4 WebSocket测试

可以使用在线WebSocket测试工具:

  1. 连接地址: ws://localhost:9999/ws
  2. 发送认证消息:
    {
      "type": "auth",
      "token": "your_jwt_token_here"
    }
    
  3. 发送聊天消息:
    {
      "type": "chat",
      "toUserId": 2,
      "content": "Hello World!"
    }
    ```## 
    

第十五步:创建UniApp前端项目

15.1 创建UniApp项目

  1. 打开HBuilderX
  2. 文件 -> 新建 -> 项目
  3. 选择uni-app项目
  4. 输入项目名称:uniappIm
  5. 选择默认模板
  6. 点击创建

15.2 项目结构说明

uniappIm/
├── pages/                  # 页面目录
│   ├── index/             # 首页
│   ├── login/             # 登录页
│   ├── chat/              # 聊天列表页
│   └── chatDetail/        # 聊天详情页
├── static/                # 静态资源
├── utils/                 # 工具类
│   ├── api.js            # API接口封装
│   └── websocket.js      # WebSocket封装
├── App.vue               # 应用入口
├── main.js               # 入口文件
├── manifest.json         # 应用配置
└── pages.json            # 页面配置

15.3 配置pages.json

编辑pages.json文件:

{
  "pages": [
    {
      "path": "pages/login/login",
      "style": {
        "navigationBarTitleText": "登录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/chat/chat",
      "style": {
        "navigationBarTitleText": "聊天"
      }
    },
    {
      "path": "pages/chatDetail/chatDetail",
      "style": {
        "navigationBarTitleText": "聊天详情"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "即时通讯",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/chat/chat",
        "iconPath": "static/chat.png",
        "selectedIconPath": "static/chat-active.png",
        "text": "聊天"
      }
    ]
  }
}

15.4 配置App.vue

编辑App.vue文件:

<template>
  <view id="app">
    <!-- 应用入口 -->
  </view>
</template>

<script>
export default {
  onLaunch: function() {
    console.log('App Launch')
  },
  onShow: function() {
    console.log('App Show')
  },
  onHide: function() {
    console.log('App Hide')
  }
}
</script>

<style>
/*每个页面公共css */
page {
  background-color: #f5f5f5;
}

.container {
  padding: 20rpx;
}

.btn-primary {
  background-color: #007aff;
  color: white;
  border-radius: 10rpx;
  padding: 20rpx;
  text-align: center;
  margin: 20rpx 0;
}

.input-group {
  margin-bottom: 30rpx;
}

.input-group label {
  display: block;
  margin-bottom: 10rpx;
  font-weight: bold;
}

.input-group input {
  width: 100%;
  padding: 20rpx;
  border: 1px solid #ddd;
  border-radius: 10rpx;
  box-sizing: border-box;
}
</style>

第十六步:创建API工具类

16.1 创建API封装

在utils目录下创建api.js:

// API配置
const BASE_URL = 'http://localhost:8080/api'

/**
 * 统一请求封装
 * @param {string} url 请求地址
 * @param {object} options 请求选项
 * @returns {Promise} 请求Promise
 */
function request(url, options = {}) {
  return new Promise((resolve, reject) => {
    // 获取存储的token
    const token = uni.getStorageSync('token')
    
    // 发起请求
    uni.request({
      url: BASE_URL + url,
      method: options.method || 'GET',
      data: options.data,
      header: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : '',
        ...options.header
      },
      success: (res) => {
        resolve(res.data)
      },
      fail: (err) => {
        reject(err)
      }
    })
  })
}

/**
 * 用户登录
 * @param {object} data 登录数据 {username, password}
 * @returns {Promise} 登录结果
 */
export function login(data) {
  return request('/auth/login', {
    method: 'POST',
    data
  })
}

/**
 * 用户注册
 * @param {object} data 注册数据 {username, password, nickname}
 * @returns {Promise} 注册结果
 */
export function register(data) {
  return request('/auth/register', {
    method: 'POST',
    data
  })
}

/**
 * 用户退出登录
 * @returns {Promise} 退出结果
 */
export function logout() {
  return request('/auth/logout', {
    method: 'POST'
  })
}

/**
 * 获取聊天历史记录
 * @param {number} toUserId 对方用户ID
 * @returns {Promise} 聊天记录
 */
export function getChatHistory(toUserId) {
  return request(`/messages/history/${toUserId}`)
}

/**
 * 标记消息为已读
 * @param {number} messageId 消息ID
 * @returns {Promise} 操作结果
 */
export function markMessageAsRead(messageId) {
  return request(`/messages/${messageId}/read`, {
    method: 'POST'
  })
}

16.2 API工具类说明

  • BASE_URL: 后端API基础地址
  • request(): 统一请求封装函数
  • uni.request(): UniApp提供的网络请求API
  • uni.getStorageSync(): 同步获取本地存储数据
  • Promise: 异步操作封装

第十七步:创建WebSocket工具类

17.1 创建WebSocket封装

在utils目录下创建websocket.js:

let socketTask = null          // WebSocket连接对象
let messageHandler = null      // 消息处理函数
let reconnectTimer = null      // 重连定时器
let heartbeatTimer = null      // 心跳定时器

// WebSocket服务器地址
const WS_URL = 'ws://localhost:9999/ws'

/**
 * 连接WebSocket
 * @param {string} token JWT令牌
 * @param {function} onMessage 消息处理回调函数
 */
export function connectWebSocket(token, onMessage) {
  // 如果已有连接,先关闭
  if (socketTask) {
    socketTask.close()
  }
  
  messageHandler = onMessage
  
  // 创建WebSocket连接
  socketTask = uni.connectSocket({
    url: WS_URL,
    success: () => {
      console.log('WebSocket连接成功')
    },
    fail: (err) => {
      console.error('WebSocket连接失败:', err)
    }
  })
  
  // 连接打开事件
  socketTask.onOpen(() => {
    console.log('WebSocket已打开')
    
    // 发送认证消息
    sendWebSocketMessage({
      type: 'auth',
      token: token
    })
    
    // 开始心跳
    startHeartbeat()
  })
  
  // 接收消息事件
  socketTask.onMessage((res) => {
    try {
      const message = JSON.parse(res.data)
      console.log('收到WebSocket消息:', message)
      
      if (message.type === 'auth_success') {
        console.log('WebSocket认证成功')
      } else if (message.type === 'heartbeat') {
        console.log('心跳响应')
      } else if (messageHandler) {
        messageHandler(message)
      }
    } catch (error) {
      console.error('解析WebSocket消息失败:', error)
    }
  })
  
  // 连接关闭事件
  socketTask.onClose(() => {
    console.log('WebSocket连接关闭')
    stopHeartbeat()
    
    // 自动重连
    if (!reconnectTimer) {
      reconnectTimer = setTimeout(() => {
        console.log('尝试重连WebSocket')
        connectWebSocket(token, messageHandler)
        reconnectTimer = null
      }, 3000)
    }
  })
  
  // 连接错误事件
  socketTask.onError((err) => {
    console.error('WebSocket错误:', err)
  })
}

/**
 * 发送WebSocket消息
 * @param {object} message 要发送的消息对象
 */
export function sendWebSocketMessage(message) {
  if (socketTask && socketTask.readyState === 1) {
    socketTask.send({
      data: JSON.stringify(message),
      success: () => {
        console.log('WebSocket消息发送成功:', message)
      },
      fail: (err) => {
        console.error('WebSocket消息发送失败:', err)
      }
    })
  } else {
    console.error('WebSocket未连接')
  }
}

/**
 * 关闭WebSocket连接
 */
export function closeWebSocket() {
  stopHeartbeat()
  
  if (reconnectTimer) {
    clearTimeout(reconnectTimer)
    reconnectTimer = null
  }
  
  if (socketTask) {
    socketTask.close()
    socketTask = null
  }
}

/**
 * 开始心跳检测
 */
function startHeartbeat() {
  heartbeatTimer = setInterval(() => {
    sendWebSocketMessage({
      type: 'heartbeat'
    })
  }, 30000) // 30秒心跳
}

/**
 * 停止心跳检测
 */
function stopHeartbeat() {
  if (heartbeatTimer) {
    clearInterval(heartbeatTimer)
    heartbeatTimer = null
  }
}

17.2 WebSocket工具类说明

  • uni.connectSocket(): UniApp提供的WebSocket连接API
  • onOpen/onMessage/onClose/onError: WebSocket事件处理
  • 心跳机制: 定期发送心跳消息保持连接
  • 自动重连: 连接断开后自动尝试重连
  • JSON.parse/stringify: JSON数据解析和序列化##
    第十八步:创建登录页面

18.1 创建登录页面

在pages/login目录下创建login.vue:

<template>
  <view class="login-container">
    <!-- 登录头部 -->
    <view class="login-header">
      <text class="title">即时通讯</text>
      <text class="subtitle">请登录您的账号</text>
    </view>
    
    <!-- 登录表单 -->
    <view class="login-form">
      <view class="input-group">
        <input 
          v-model="loginForm.username" 
          placeholder="请输入用户名" 
          class="input-field"
        />
      </view>
      
      <view class="input-group">
        <input 
          v-model="loginForm.password" 
          placeholder="请输入密码" 
          type="password"
          class="input-field"
        />
      </view>
      
      <button @click="handleLogin" class="login-btn" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
      
      <view class="register-link">
        <text @click="handleRegister">还没有账号?点击注册</text>
      </view>
    </view>
    
    <!-- 注册弹窗 -->
    <view v-if="showRegisterModal" class="modal-overlay" @click="closeRegisterPopup">
      <view class="register-popup" @click.stop>
        <view class="popup-title">注册账号</view>
        <view class="input-group">
          <input 
            v-model="registerForm.username" 
            placeholder="请输入用户名" 
            class="input-field"
          />
        </view>
        <view class="input-group">
          <input 
            v-model="registerForm.password" 
            placeholder="请输入密码" 
            type="password"
            class="input-field"
          />
        </view>
        <view class="input-group">
          <input 
            v-model="registerForm.nickname" 
            placeholder="请输入昵称(可选)" 
            class="input-field"
          />
        </view>
        <view class="popup-buttons">
          <button @click="closeRegisterPopup" class="cancel-btn">取消</button>
          <button @click="submitRegister" class="confirm-btn">注册</button>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
import { login, register } from '@/utils/api.js'

export default {
  data() {
    return {
      loading: false,              // 登录加载状态
      showRegisterModal: false,    // 注册弹窗显示状态
      loginForm: {                 // 登录表单数据
        username: '',
        password: ''
      },
      registerForm: {              // 注册表单数据
        username: '',
        password: '',
        nickname: ''
      }
    }
  },
  methods: {
    /**
     * 处理登录
     */
    async handleLogin() {
      // 表单验证
      if (!this.loginForm.username || !this.loginForm.password) {
        uni.showToast({
          title: '请填写完整信息',
          icon: 'none'
        })
        return
      }
      
      this.loading = true
      try {
        // 调用登录API
        const result = await login(this.loginForm)
        if (result.code === 200) {
          // 保存token和用户信息到本地存储
          uni.setStorageSync('token', result.data.token)
          uni.setStorageSync('userInfo', result.data.user)
          
          uni.showToast({
            title: '登录成功',
            icon: 'success'
          })
          
          // 跳转到聊天页面
          uni.reLaunch({
            url: '/pages/chat/chat'
          })
        } else {
          uni.showToast({
            title: result.message,
            icon: 'none'
          })
        }
      } catch (error) {
        uni.showToast({
          title: '登录失败',
          icon: 'none'
        })
      } finally {
        this.loading = false
      }
    },
    
    /**
     * 显示注册弹窗
     */
    handleRegister() {
      this.showRegisterModal = true
    },
    
    /**
     * 关闭注册弹窗
     */
    closeRegisterPopup() {
      this.showRegisterModal = false
      this.registerForm = {
        username: '',
        password: '',
        nickname: ''
      }
    },
    
    /**
     * 提交注册
     */
    async submitRegister() {
      // 表单验证
      if (!this.registerForm.username || !this.registerForm.password) {
        uni.showToast({
          title: '请填写完整信息',
          icon: 'none'
        })
        return
      }
      
      try {
        // 调用注册API
        const result = await register(this.registerForm)
        if (result.code === 200) {
          uni.showToast({
            title: '注册成功',
            icon: 'success'
          })
          this.closeRegisterPopup()
          
          // 自动填充登录表单
          this.loginForm.username = this.registerForm.username
          this.loginForm.password = this.registerForm.password
        } else {
          uni.showToast({
            title: result.message,
            icon: 'none'
          })
        }
      } catch (error) {
        uni.showToast({
          title: '注册失败',
          icon: 'none'
        })
      }
    }
  }
}
</script>

<style scoped>
.login-container {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 40rpx;
}

.login-header {
  text-align: center;
  margin-bottom: 80rpx;
}

.title {
  font-size: 60rpx;
  font-weight: bold;
  color: white;
  display: block;
  margin-bottom: 20rpx;
}

.subtitle {
  font-size: 28rpx;
  color: rgba(255, 255, 255, 0.8);
}

.login-form {
  background: white;
  border-radius: 20rpx;
  padding: 60rpx 40rpx;
  box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
}

.input-group {
  margin-bottom: 40rpx;
}

.input-field {
  width: 100%;
  height: 80rpx;
  border: 2rpx solid #e0e0e0;
  border-radius: 10rpx;
  padding: 0 20rpx;
  font-size: 28rpx;
  box-sizing: border-box;
}

.input-field:focus {
  border-color: #667eea;
}

.login-btn {
  width: 100%;
  height: 80rpx;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  border-radius: 10rpx;
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 40rpx;
}

.login-btn:disabled {
  opacity: 0.6;
}

.register-link {
  text-align: center;
}

.register-link text {
  color: #667eea;
  font-size: 26rpx;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.register-popup {
  width: 600rpx;
  background: white;
  border-radius: 20rpx;
  padding: 40rpx;
  margin: 40rpx;
}

.popup-title {
  text-align: center;
  font-size: 36rpx;
  font-weight: bold;
  margin-bottom: 40rpx;
}

.popup-buttons {
  display: flex;
  justify-content: space-between;
  margin-top: 40rpx;
}

.cancel-btn, .confirm-btn {
  width: 45%;
  height: 70rpx;
  border-radius: 10rpx;
  font-size: 28rpx;
}

.cancel-btn {
  background: #f5f5f5;
  color: #666;
  border: none;
}

.confirm-btn {
  background: #667eea;
  color: white;
  border: none;
}
</style>

18.2 登录页面功能说明

  • 双向数据绑定: 使用v-model绑定表单数据
  • 事件处理: @click绑定点击事件
  • 条件渲染: v-if控制注册弹窗显示
  • 本地存储: uni.setStorageSync保存token和用户信息
  • 页面跳转: uni.reLaunch跳转到聊天页面
  • 消息提示: uni.showToast显示操作结果

第十九步:创建聊天列表页面

19.1 创建聊天列表页面

在pages/chat目录下创建chat.vue:

<template>
  <view class="chat-container">
    <!-- 聊天头部 -->
    <view class="chat-header">
      <text class="header-title">聊天列表</text>
      <text @click="logout" class="logout-btn">退出</text>
    </view>
    
    <!-- 用户列表 -->
    <view class="user-list">
      <view 
        v-for="user in userList" 
        :key="user.id"
        @click="openChat(user)"
        class="user-item"
      >
        <!-- 用户头像 -->
        <view class="avatar">
          <text>{{ user.nickname.charAt(0) }}</text>
        </view>
        
        <!-- 用户信息 -->
        <view class="user-info">
          <text class="username">{{ user.nickname }}</text>
          <text class="last-message">点击开始聊天</text>
        </view>
        
        <!-- 在线状态 -->
        <view class="status" :class="{ online: user.online }"></view>
      </view>
    </view>
    
    <!-- 空状态 -->
    <view v-if="userList.length === 0" class="empty-state">
      <text>暂无其他用户</text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      userInfo: null,              // 当前用户信息
      userList: [                  // 用户列表(模拟数据)
        { id: 1, nickname: '用户1', online: true },
        { id: 2, nickname: '用户2', online: false }
      ]
    }
  },
  
  onLoad() {
    this.checkLogin()
    this.loadUserInfo()
  },
  
  methods: {
    /**
     * 检查登录状态
     */
    checkLogin() {
      const token = uni.getStorageSync('token')
      if (!token) {
        uni.reLaunch({
          url: '/pages/login/login'
        })
      }
    },
    
    /**
     * 加载用户信息
     */
    loadUserInfo() {
      this.userInfo = uni.getStorageSync('userInfo')
      // 过滤掉当前用户
      if (this.userInfo) {
        this.userList = this.userList.filter(user => user.id !== this.userInfo.id)
      }
    },
    
    /**
     * 打开聊天窗口
     * @param {object} user 用户对象
     */
    openChat(user) {
      uni.navigateTo({
        url: `/pages/chatDetail/chatDetail?userId=${user.id}&nickname=${user.nickname}`
      })
    },
    
    /**
     * 退出登录
     */
    logout() {
      uni.showModal({
        title: '提示',
        content: '确定要退出登录吗?',
        success: (res) => {
          if (res.confirm) {
            // 清除本地存储
            uni.clearStorageSync()
            // 跳转到登录页
            uni.reLaunch({
              url: '/pages/login/login'
            })
          }
        }
      })
    }
  }
}
</script>

<style scoped>
.chat-container {
  height: 100vh;
  background: #f5f5f5;
}

.chat-header {
  background: white;
  padding: 20rpx 30rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1rpx solid #e0e0e0;
}

.header-title {
  font-size: 36rpx;
  font-weight: bold;
}

.logout-btn {
  color: #ff4757;
  font-size: 28rpx;
}

.user-list {
  padding: 20rpx 0;
}

.user-item {
  background: white;
  padding: 30rpx;
  margin-bottom: 2rpx;
  display: flex;
  align-items: center;
}

.avatar {
  width: 80rpx;
  height: 80rpx;
  border-radius: 50%;
  background: #667eea;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 20rpx;
}

.avatar text {
  color: white;
  font-size: 32rpx;
  font-weight: bold;
}

.user-info {
  flex: 1;
}

.username {
  font-size: 32rpx;
  font-weight: bold;
  display: block;
  margin-bottom: 10rpx;
}

.last-message {
  font-size: 26rpx;
  color: #999;
}

.status {
  width: 20rpx;
  height: 20rpx;
  border-radius: 50%;
  background: #ccc;
}

.status.online {
  background: #2ed573;
}

.empty-state {
  text-align: center;
  padding: 100rpx 0;
  color: #999;
}
</style>
```## 第二十
步:创建聊天详情页面

### 20.1 创建聊天详情页面
在pages/chatDetail目录下创建chatDetail.vue:

```vue
<template>
  <view class="chat-detail">
    <!-- 消息列表 -->
    <scroll-view 
      class="message-list" 
      :scroll-top="scrollTop"
      scroll-y="true"
      :scroll-into-view="scrollIntoView"
    >
      <view 
        v-for="message in messages" 
        :key="message.id"
        :id="'msg-' + message.id"
        class="message-item"
        :class="{ 'own-message': message.fromUserId === userInfo.id }"
      >
        <!-- 消息内容 -->
        <view class="message-content">
          <text>{{ message.content }}</text>
        </view>
        
        <!-- 消息时间 -->
        <view class="message-time">
          {{ formatTime(message.sendTime) }}
        </view>
      </view>
    </scroll-view>
    
    <!-- 输入区域 -->
    <view class="input-area">
      <input 
        v-model="inputMessage" 
        placeholder="输入消息..." 
        class="message-input"
        @confirm="sendMessage"
        confirm-type="send"
      />
      <button @click="sendMessage" class="send-btn">发送</button>
    </view>
  </view>
</template>

<script>
import { getChatHistory } from '@/utils/api.js'
import { connectWebSocket, sendWebSocketMessage, closeWebSocket } from '@/utils/websocket.js'

export default {
  data() {
    return {
      userId: null,                // 对方用户ID
      nickname: '',                // 对方昵称
      userInfo: null,              // 当前用户信息
      messages: [],                // 消息列表
      inputMessage: '',            // 输入的消息
      scrollTop: 0,                // 滚动位置
      scrollIntoView: ''           // 滚动到指定元素
    }
  },
  
  onLoad(options) {
    // 获取页面参数
    this.userId = parseInt(options.userId)
    this.nickname = options.nickname
    this.userInfo = uni.getStorageSync('userInfo')
    
    // 设置导航栏标题
    uni.setNavigationBarTitle({
      title: this.nickname
    })
    
    // 初始化
    this.loadChatHistory()
    this.initWebSocket()
  },
  
  onUnload() {
    // 页面卸载时关闭WebSocket连接
    closeWebSocket()
  },
  
  methods: {
    /**
     * 加载聊天历史记录
     */
    async loadChatHistory() {
      try {
        const result = await getChatHistory(this.userId)
        if (result.code === 200) {
          this.messages = result.data
          this.scrollToBottom()
        }
      } catch (error) {
        console.error('加载聊天记录失败:', error)
      }
    },
    
    /**
     * 初始化WebSocket连接
     */
    initWebSocket() {
      const token = uni.getStorageSync('token')
      connectWebSocket(token, (message) => {
        // 处理接收到的消息
        if (message.type === 'chat') {
          // 只显示与当前聊天对象相关的消息
          if (message.fromUserId === this.userId || message.toUserId === this.userId) {
            this.messages.push({
              id: message.messageId,
              fromUserId: message.fromUserId,
              toUserId: message.toUserId,
              content: message.content,
              sendTime: message.sendTime
            })
            this.scrollToBottom()
          }
        }
      })
    },
    
    /**
     * 发送消息
     */
    sendMessage() {
      if (!this.inputMessage.trim()) {
        return
      }
      
      // 构造消息对象
      const message = {
        type: 'chat',
        toUserId: this.userId,
        content: this.inputMessage
      }
      
      // 发送WebSocket消息
      sendWebSocketMessage(message)
      
      // 添加到本地消息列表(乐观更新)
      this.messages.push({
        id: Date.now(), // 临时ID
        fromUserId: this.userInfo.id,
        toUserId: this.userId,
        content: this.inputMessage,
        sendTime: new Date().toISOString()
      })
      
      // 清空输入框并滚动到底部
      this.inputMessage = ''
      this.scrollToBottom()
    },
    
    /**
     * 格式化时间显示
     * @param {string} timeStr 时间字符串
     * @returns {string} 格式化后的时间
     */
    formatTime(timeStr) {
      const time = new Date(timeStr)
      const now = new Date()
      const diff = now - time
      
      if (diff < 60000) { // 1分钟内
        return '刚刚'
      } else if (diff < 3600000) { // 1小时内
        return Math.floor(diff / 60000) + '分钟前'
      } else if (diff < 86400000) { // 24小时内
        return Math.floor(diff / 3600000) + '小时前'
      } else {
        // 超过24小时显示具体日期
        return time.toLocaleDateString()
      }
    },
    
    /**
     * 滚动到底部
     */
    scrollToBottom() {
      this.$nextTick(() => {
        if (this.messages.length > 0) {
          const lastMessage = this.messages[this.messages.length - 1]
          this.scrollIntoView = 'msg-' + lastMessage.id
        }
      })
    }
  }
}
</script>

<style scoped>
.chat-detail {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: #f5f5f5;
}

.message-list {
  flex: 1;
  padding: 20rpx;
}

.message-item {
  margin-bottom: 30rpx;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.message-item.own-message {
  align-items: flex-end;
}

.message-content {
  max-width: 70%;
  padding: 20rpx 30rpx;
  border-radius: 20rpx;
  background: white;
  word-wrap: break-word;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.own-message .message-content {
  background: #667eea;
  color: white;
}

.message-time {
  font-size: 22rpx;
  color: #999;
  margin-top: 10rpx;
}

.input-area {
  background: white;
  padding: 20rpx;
  display: flex;
  align-items: center;
  border-top: 1rpx solid #e0e0e0;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.message-input {
  flex: 1;
  height: 70rpx;
  border: 1rpx solid #e0e0e0;
  border-radius: 35rpx;
  padding: 0 30rpx;
  margin-right: 20rpx;
  font-size: 28rpx;
}

.send-btn {
  width: 120rpx;
  height: 70rpx;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 35rpx;
  font-size: 28rpx;
}
</style>

20.2 聊天详情页面功能说明

  • 消息列表: 使用scroll-view实现滚动
  • 消息气泡: 区分自己和对方的消息样式
  • 实时通信: WebSocket接收和发送消息
  • 时间格式化: 智能显示消息时间
  • 自动滚动: 新消息自动滚动到底部
  • 乐观更新: 发送消息立即显示,提升用户体验

第二十一步:添加静态资源

21.1 添加聊天图标

在static目录下添加以下图标文件:

  1. chat.png - 聊天图标(未选中状态)
  2. chat-active.png - 聊天图标(选中状态)

可以使用在线图标生成工具或设计软件创建,建议尺寸为64x64像素。

21.2 图标说明

  • iconPath: TabBar未选中时的图标
  • selectedIconPath: TabBar选中时的图标
  • 支持PNG、JPG格式
  • 建议使用PNG格式以支持透明背景

第二十二步:测试和调试

22.1 启动前端项目

  1. 在HBuilderX中打开uniappIm项目
  2. 点击"运行" -> “运行到浏览器” -> “Chrome”
  3. 或者运行到手机模拟器进行测试

22.2 功能测试流程

  1. 注册测试:

    • 打开应用,点击"点击注册"
    • 输入用户名、密码、昵称
    • 点击注册按钮
  2. 登录测试:

    • 输入注册的用户名和密码
    • 点击登录按钮
    • 成功后跳转到聊天列表页
  3. 聊天测试:

    • 在聊天列表中点击用户
    • 进入聊天详情页
    • 输入消息并发送
    • 使用另一个浏览器窗口登录不同用户进行对话测试

22.3 调试技巧

  • 浏览器开发者工具: 查看网络请求和WebSocket连接
  • console.log: 在代码中添加日志输出
  • uni.showToast: 显示调试信息
  • 后端日志: 查看SpringBoot控制台输出

第二十三步:部署和优化

23.1 后端部署

  1. 打包应用:

    mvn clean package -DskipTests
    
  2. 运行JAR包:

    java -jar target/Study-Im-1.0-SNAPSHOT.jar
    
  3. 配置生产环境:

    • 修改application.yml中的数据库和Redis配置
    • 使用环境变量或配置文件管理敏感信息

23.2 前端部署

  1. H5部署:

    • 在HBuilderX中选择"发行" -> “网站-PC Web或手机H5”
    • 将生成的dist目录部署到Web服务器
  2. 小程序部署:

    • 选择"发行" -> “小程序-微信”
    • 使用微信开发者工具上传代码
  3. App部署:

    • 选择"发行" -> “原生App-云打包”
    • 配置应用信息和证书

23.3 性能优化建议

  1. 后端优化:

    • 数据库索引优化
    • Redis缓存策略
    • 连接池配置
    • 日志级别调整
  2. 前端优化:

    • 图片压缩和懒加载
    • 消息分页加载
    • WebSocket重连策略
    • 本地存储优化

总结

通过以上23个步骤,我们完成了一个完整的即时通讯系统的开发:

后端技术要点

  1. SpringBoot框架: 快速构建Web应用
  2. JPA数据访问: 简化数据库操作
  3. Redis缓存: 用户状态管理
  4. JWT认证: 安全的用户身份验证
  5. Netty WebSocket: 高性能实时通信

前端技术要点

  1. UniApp框架: 跨平台开发
  2. Vue.js: 响应式数据绑定
  3. WebSocket: 实时双向通信
  4. 本地存储: 用户状态持久化
  5. 组件化开发: 可维护的代码结构

系统特性

  • ✅ 用户注册和登录
  • ✅ 实时消息发送和接收
  • ✅ 消息历史记录
  • ✅ 用户在线状态
  • ✅ 自动重连机制
  • ✅ 跨平台支持

这个项目为学习SpringBoot、Netty和UniApp提供了一个完整的实践案例,涵盖了现代Web应用开发的核心技术栈。

附录:项目中每个类的详细作用说明

🏗️ 架构设计说明

在深入了解每个类之前,先理解整个系统的架构:

┌─────────────────┐    HTTP/WebSocket    ┌─────────────────┐
│   UniApp前端    │ ◄─────────────────► │  SpringBoot后端  │
│                 │                      │                 │
│ ├─ 登录页面     │                      │ ├─ 控制器层     │
│ ├─ 聊天列表     │                      │ ├─ 业务逻辑层   │
│ ├─ 聊天详情     │                      │ ├─ 数据访问层   │
│ └─ WebSocket    │                      │ └─ Netty服务器  │
└─────────────────┘                      └─────────────────┘
                                                   │
                                         ┌─────────┴─────────┐
                                         │                   │
                                    ┌────▼────┐         ┌───▼───┐
                                    │  MySQL  │         │ Redis │
                                    │ 数据库   │         │ 缓存  │
                                    └─────────┘         └───────┘

🎯 后端类详细说明

1. 主启动类

ImApplication.java - 应用程序入口
@SpringBootApplication
public class ImApplication

存在意义

  • 🚀 系统启动器:整个SpringBoot应用的启动入口点
  • 🔧 自动配置触发器:通过@SpringBootApplication注解启动Spring的自动配置机制
  • 📦 组件扫描根节点:从这个类所在的包开始扫描所有Spring组件

在项目中的角色

  • 生命周期管理者:控制整个应用的启动和关闭
  • 配置聚合点:汇聚所有的配置信息并初始化Spring容器
  • 服务协调者:协调HTTP服务器(Tomcat)和WebSocket服务器(Netty)同时启动

为什么需要这个类

  • SpringBoot需要一个明确的入口点来启动应用
  • 提供了统一的应用生命周期管理
  • 简化了复杂的Spring配置过程

2. 实体类层 (Entity Layer)

User.java - 用户实体类
@Entity
@Table(name = "users")
public class User

存在意义

  • 👤 用户数据模型:定义了系统中用户的数据结构
  • 🗃️ 数据库映射:通过JPA注解与数据库表建立映射关系
  • 🔐 业务实体:承载用户相关的所有业务属性

在项目中的角色

  • 数据载体:存储用户的基本信息(用户名、密码、昵称等)
  • 状态管理器:管理用户的在线状态(在线/离线/忙碌)
  • 认证基础:为用户登录认证提供数据基础

为什么需要这个类

  • 即时通讯系统必须要有用户概念,需要区分不同的聊天参与者
  • 需要持久化存储用户信息,方便用户登录和身份识别
  • 用户状态管理是即时通讯的核心功能之一

关键字段说明

  • id: 用户唯一标识,用于消息关联
  • username/password: 登录凭证
  • nickname: 聊天时显示的名称
  • status: 当前在线状态,影响其他用户看到的状态
  • lastLoginTime: 用于统计和状态判断
Message.java - 消息实体类
@Entity
@Table(name = "messages")
public class Message

存在意义

  • 💬 消息数据模型:定义了聊天消息的完整数据结构
  • 📝 通信记录:保存所有的聊天历史,实现消息持久化
  • 🔗 关系纽带:连接发送者和接收者,建立通信关系

在项目中的角色

  • 消息载体:承载消息内容、时间、状态等信息
  • 历史记录器:保存聊天历史,支持消息回溯
  • 状态跟踪器:跟踪消息的发送、送达、已读状态

为什么需要这个类

  • 即时通讯的核心就是消息传递,必须有消息实体
  • 用户需要查看历史聊天记录
  • 需要跟踪消息状态,提供更好的用户体验

关键字段说明

  • fromUserId/toUserId: 建立消息的发送和接收关系
  • content: 消息的实际内容
  • type: 支持不同类型的消息(文本、图片、文件等)
  • status: 消息状态跟踪(已发送、已送达、已读)
  • sendTime/readTime: 时间戳,用于排序和状态判断

3. 数据传输对象层 (DTO Layer)

ApiResponse.java - 统一响应格式
public class ApiResponse<T>

存在意义

  • 📋 响应标准化:为所有API接口提供统一的响应格式
  • 🎯 错误处理统一:标准化成功和失败的响应结构
  • 🔧 前端适配:让前端能够统一处理所有接口响应

在项目中的角色

  • 响应包装器:包装所有API的返回数据
  • 状态传达者:通过code字段明确告知操作结果
  • 数据载体:通过泛型承载不同类型的响应数据

为什么需要这个类

  • 前端需要统一的方式来判断接口调用是否成功
  • 便于错误处理和用户提示
  • 提高代码的可维护性和一致性

设计优势

  • 使用泛型支持任意类型的数据返回
  • 提供静态方法简化响应对象的创建
  • 标准化的错误信息传递
LoginRequest.java - 登录请求DTO
public class LoginRequest

存在意义

  • 🔐 登录数据载体:专门承载用户登录时提交的数据
  • 数据验证:通过注解进行输入数据的合法性验证
  • 🛡️ 安全边界:明确定义登录接口接受的数据范围

在项目中的角色

  • 数据接收器:接收前端提交的登录表单数据
  • 验证执行者:执行用户名和密码的基础验证
  • 业务传递者:将验证后的数据传递给业务逻辑层

为什么需要这个类

  • 登录是系统的关键入口,需要专门的数据结构
  • 分离关注点,登录数据不应该直接使用User实体
  • 便于添加登录特有的验证规则
RegisterRequest.java - 注册请求DTO
public class RegisterRequest

存在意义

  • 📝 注册数据载体:专门处理用户注册时的数据提交
  • 🔍 注册验证:对注册数据进行专门的验证处理
  • 🎨 灵活扩展:可以包含注册特有的字段(如验证码、邀请码等)

在项目中的角色

  • 注册数据接收器:接收用户注册表单数据
  • 业务数据转换器:将DTO数据转换为User实体
  • 扩展预留:为未来的注册功能扩展预留空间

为什么需要这个类

  • 注册和登录虽然都涉及用户,但业务逻辑不同
  • 注册可能需要额外的字段(昵称、邮箱验证等)
  • 保持代码的清晰性和可维护性
MessageDto.java - 消息传输DTO
public class MessageDto

存在意义

  • 📤 消息传输载体:专门用于前后端之间的消息数据传输
  • 🔄 数据转换桥梁:在Message实体和前端数据之间进行转换
  • 📊 信息聚合:聚合消息相关的用户信息,减少前端查询

在项目中的角色

  • 数据适配器:将数据库实体适配为前端需要的格式
  • 信息整合器:整合消息和用户信息,提供完整的显示数据
  • 传输优化器:只传输前端需要的字段,优化网络传输

为什么需要这个类

  • 前端显示消息时需要发送者和接收者的用户名
  • 避免前端进行复杂的数据关联查询
  • 控制传输的数据量,提高性能

4. 数据访问层 (Repository Layer)

UserRepository.java - 用户数据访问接口
public interface UserRepository extends JpaRepository<User, Long>

存在意义

  • 🗄️ 数据访问抽象:为用户相关的数据库操作提供抽象接口
  • 🔍 查询方法定义:定义用户相关的各种查询方法
  • 🚀 自动实现:利用Spring Data JPA的自动实现机制

在项目中的角色

  • 数据库操作代理:代理所有用户相关的数据库操作
  • 查询方法提供者:提供根据用户名查找、检查用户存在等方法
  • 事务边界:自动处理数据库事务

为什么需要这个类

  • 用户登录需要根据用户名查找用户
  • 注册时需要检查用户名是否已存在
  • 提供标准的CRUD操作,简化数据访问代码

关键方法说明

  • findByUsername(): 登录时根据用户名查找用户
  • existsByUsername(): 注册时检查用户名是否已存在
  • 继承的CRUD方法:save(), findById(), delete()等
MessageRepository.java - 消息数据访问接口
public interface MessageRepository extends JpaRepository<Message, Long>

存在意义

  • 💬 消息数据访问:专门处理消息相关的数据库操作
  • 📚 聊天历史查询:提供复杂的聊天记录查询功能
  • 🔗 关系查询:处理用户之间的消息关联查询

在项目中的角色

  • 消息存储器:保存新发送的消息到数据库
  • 历史查询器:查询两个用户之间的聊天历史
  • 消息管理器:提供消息的各种管理功能

为什么需要这个类

  • 即时通讯需要保存所有消息记录
  • 用户需要查看历史聊天记录
  • 需要支持复杂的消息查询(按时间、按用户等)

关键方法说明

  • findChatHistory(): 查询两个用户之间的完整聊天记录
  • findByToUserIdOrderBySendTimeDesc(): 查询发给指定用户的消息

5. 业务逻辑层 (Service Layer)

UserService.java - 用户业务逻辑服务
@Service
public class UserService

存在意义

  • 🧠 业务逻辑中心:处理所有用户相关的业务逻辑
  • 🔐 认证服务提供者:提供用户注册、登录、认证等核心功能
  • 🔄 状态管理器:管理用户的在线状态和会话状态

在项目中的角色

  • 认证协调者:协调用户认证流程,整合JWT、Redis等组件
  • 业务规则执行者:执行用户相关的业务规则(如用户名唯一性)
  • 状态同步器:同步用户状态到数据库和Redis

为什么需要这个类

  • 用户认证涉及多个组件的协调(数据库、JWT、Redis)
  • 需要统一的地方处理用户相关的业务逻辑
  • 为控制器层提供高级的业务操作接口

核心方法说明

  • register(): 用户注册流程,包括验证、保存、生成token
  • login(): 用户登录流程,包括验证、状态更新、token生成
  • logout(): 用户退出流程,清理状态信息
  • isUserOnline(): 检查用户在线状态

业务逻辑复杂性

  • 注册时需要检查用户名唯一性
  • 登录时需要验证密码、更新状态、生成JWT、设置Redis
  • 需要处理Redis连接失败的备用方案
MessageService.java - 消息业务逻辑服务
@Service
public class MessageService

存在意义

  • 💬 消息业务中心:处理所有消息相关的业务逻辑
  • 🔄 数据转换器:在实体对象和DTO之间进行转换
  • 📊 消息聚合器:聚合消息和用户信息,提供完整的消息数据

在项目中的角色

  • 消息处理器:处理消息的保存、查询、转换等操作
  • 数据整合器:整合消息数据和用户数据,提供完整信息
  • 业务规则执行者:执行消息相关的业务规则

为什么需要这个类

  • 消息处理涉及复杂的数据转换和聚合
  • 需要统一的地方处理消息相关的业务逻辑
  • 为WebSocket和HTTP接口提供统一的消息服务

核心方法说明

  • saveMessage(): 保存消息到数据库
  • getChatHistory(): 获取聊天历史,包含用户信息
  • convertToDto(): 将Message实体转换为MessageDto
InMemoryUserStatusService.java - 内存用户状态服务
@Service
public class InMemoryUserStatusService

存在意义

  • 🔄 Redis备用方案:当Redis不可用时提供用户状态管理
  • 💾 内存状态管理:使用内存数据结构管理用户在线状态
  • 自动清理机制:定时清理过期的用户状态

在项目中的角色

  • 状态备份器:作为Redis的备用状态存储
  • 容错保障:确保Redis故障时系统仍能正常工作
  • 性能优化器:提供快速的内存状态查询

为什么需要这个类

  • 提高系统的可用性,避免Redis故障导致系统不可用
  • 在开发环境中可能没有Redis,需要备用方案
  • 演示了如何设计容错机制

设计特点

  • 使用ConcurrentHashMap保证线程安全
  • 定时任务自动清理过期状态
  • 与Redis接口保持一致,便于切换

6. 控制器层 (Controller Layer)

AuthController.java - 认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController

存在意义

  • 🚪 认证入口:提供用户认证相关的HTTP接口
  • 🔗 前后端桥梁:连接前端认证请求和后端认证服务
  • 📋 接口标准化:提供标准化的RESTful认证接口

在项目中的角色

  • 请求接收器:接收前端的登录、注册请求
  • 数据验证器:验证请求数据的合法性
  • 响应生成器:生成标准化的API响应

为什么需要这个类

  • 前端需要HTTP接口来进行用户认证
  • 需要统一的地方处理认证相关的HTTP请求
  • 提供RESTful风格的认证API

接口设计

  • POST /api/auth/register: 用户注册
  • POST /api/auth/login: 用户登录
  • POST /api/auth/logout: 用户退出
MessageController.java - 消息控制器
@RestController
@RequestMapping("/api/messages")
public class MessageController

存在意义

  • 📨 消息接口提供者:提供消息相关的HTTP接口
  • 📚 历史查询入口:为前端提供聊天历史查询接口
  • 🔐 权限验证器:验证用户是否有权限查看特定消息

在项目中的角色

  • 历史服务器:提供聊天历史记录查询服务
  • 权限守护者:确保用户只能查看自己相关的消息
  • 数据提供者:为前端提供格式化的消息数据

为什么需要这个类

  • 前端需要HTTP接口来获取聊天历史
  • 需要验证用户权限,保护消息隐私
  • WebSocket主要用于实时通信,HTTP用于历史数据查询
HealthController.java - 健康检查控制器
@RestController
@RequestMapping("/api/health")
public class HealthController

存在意义

  • 🏥 系统健康监控:提供系统各组件的健康状态检查
  • 🔧 运维支持:为运维人员提供系统状态监控接口
  • 🚨 故障诊断:帮助快速定位系统故障

在项目中的角色

  • 状态检查器:检查数据库、Redis等组件的连接状态
  • 监控端点:为监控系统提供健康检查端点
  • 诊断工具:帮助开发和运维人员诊断问题

为什么需要这个类

  • 生产环境需要监控系统健康状态
  • 便于快速定位系统故障
  • 符合微服务架构的最佳实践

7. 网络通信层 (Netty Layer)

NettyServer.java - Netty服务器
@Component
public class NettyServer

存在意义

  • 🌐 WebSocket服务器:提供高性能的WebSocket连接服务
  • 🔧 网络配置管理:管理网络服务器的启动、配置和关闭
  • 高并发支持:利用Netty的高性能特性支持大量并发连接

在项目中的角色

  • 实时通信基础设施:为即时通讯提供底层网络支持
  • 连接管理器:管理所有WebSocket连接的生命周期
  • 性能优化器:通过Netty提供高性能的网络I/O

为什么需要这个类

  • 即时通讯需要实时双向通信,WebSocket是最佳选择
  • Netty提供比传统Servlet更好的性能和并发能力
  • 需要独立的WebSocket服务器来处理实时消息

技术特点

  • 使用NIO模型提高并发性能
  • 事件驱动架构,响应速度快
  • 支持大量并发连接
WebSocketHandler.java - WebSocket消息处理器
@Component
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>

存在意义

  • 📨 消息处理中心:处理所有WebSocket消息的接收和分发
  • 🔐 连接认证管理:管理WebSocket连接的身份认证
  • 🔄 消息路由器:将消息路由到正确的接收者

在项目中的角色

  • 消息分发器:接收消息并转发给目标用户
  • 连接管理器:管理用户连接的建立、维护和断开
  • 协议处理器:处理不同类型的WebSocket消息

为什么需要这个类

  • WebSocket连接需要专门的处理器来处理消息
  • 需要实现消息的实时转发功能
  • 需要管理用户连接和身份认证

核心功能

  • 用户身份认证(通过JWT token)
  • 实时消息转发
  • 心跳检测和连接管理
  • 错误处理和异常恢复

消息类型处理

  • auth: 身份认证消息
  • chat: 聊天消息
  • heartbeat: 心跳检测消息

8. 配置层 (Configuration Layer)

RedisConfig.java - Redis配置类
@Configuration
public class RedisConfig

存在意义

  • ⚙️ Redis配置中心:统一配置Redis相关的所有设置
  • 🔧 序列化配置:配置Redis数据的序列化和反序列化方式
  • 🎯 Bean管理:将Redis相关的Bean注册到Spring容器

在项目中的角色

  • 配置提供者:为Redis操作提供配置信息
  • 序列化管理器:管理Redis数据的存储格式
  • 连接配置器:配置Redis连接参数

为什么需要这个类

  • Redis默认配置可能不适合项目需求
  • 需要配置合适的序列化方式以支持复杂对象存储
  • 统一管理Redis相关配置,便于维护

配置要点

  • Key使用String序列化,便于查看和调试
  • Value使用JSON序列化,支持复杂对象
  • 配置连接池参数优化性能

9. 工具类层 (Utility Layer)

JwtUtil.java - JWT工具类
@Component
public class JwtUtil

存在意义

  • 🔐 JWT操作封装:封装JWT token的生成、验证、解析等操作
  • 🛡️ 安全工具:为系统提供安全的用户身份验证机制
  • 🔧 配置集中化:集中管理JWT相关的配置参数

在项目中的角色

  • Token生成器:为用户登录生成JWT token
  • Token验证器:验证token的有效性和完整性
  • 信息提取器:从token中提取用户信息

为什么需要这个类

  • 用户认证需要安全可靠的token机制
  • JWT是无状态认证的最佳选择
  • 需要统一的地方管理token相关操作

核心功能

  • 生成包含用户信息的JWT token
  • 验证token的签名和有效期
  • 从token中提取用户ID和用户名
  • 检查token是否过期

🎨 前端类详细说明

1. 工具类层

api.js - API接口封装
// API配置和请求封装

存在意义

  • 🌐 接口统一管理:统一管理所有后端API接口调用
  • 🔧 请求标准化:标准化HTTP请求的发送方式
  • 🛡️ 认证集成:自动添加JWT token到请求头

在项目中的角色

  • 接口代理:代理所有后端接口调用
  • 认证管理器:自动处理用户认证token
  • 错误处理器:统一处理接口调用错误

为什么需要这个文件

  • 避免在每个页面重复写接口调用代码
  • 统一管理API地址,便于环境切换
  • 自动处理认证token,简化页面代码
websocket.js - WebSocket封装
// WebSocket连接和消息处理封装

存在意义

  • 🔌 WebSocket管理:封装WebSocket连接的建立、维护和关闭
  • 💓 心跳机制:实现心跳检测,保持连接稳定
  • 🔄 自动重连:连接断开时自动尝试重连

在项目中的角色

  • 连接管理器:管理WebSocket连接的整个生命周期
  • 消息路由器:处理不同类型的WebSocket消息
  • 稳定性保障:通过心跳和重连机制保证连接稳定

为什么需要这个文件

  • WebSocket连接管理比较复杂,需要专门封装
  • 即时通讯需要稳定的连接,必须有心跳和重连机制
  • 统一处理WebSocket消息,避免代码重复

2. 页面组件层

login.vue - 登录页面
<template>登录界面</template>

存在意义

  • 🚪 系统入口:用户进入系统的第一个页面
  • 🔐 身份验证界面:提供用户登录和注册的界面
  • 🎨 用户体验:提供友好的登录体验

在项目中的角色

  • 认证入口:用户身份验证的起始点
  • 数据收集器:收集用户的登录凭证
  • 状态管理器:管理登录状态和用户信息

为什么需要这个页面

  • 即时通讯系统需要识别用户身份
  • 需要友好的界面让用户输入登录信息
  • 作为系统的安全入口,控制访问权限
chat.vue - 聊天列表页面
<template>聊天列表</template>

存在意义

  • 📋 用户列表展示:显示可以聊天的用户列表
  • 🏠 主页面:用户登录后的主要操作界面
  • 🔄 状态显示:显示其他用户的在线状态

在项目中的角色

  • 导航中心:用户选择聊天对象的中心页面
  • 状态展示器:展示用户的在线状态
  • 功能入口:提供退出登录等功能入口

为什么需要这个页面

  • 用户需要选择聊天对象
  • 需要显示其他用户的在线状态
  • 作为应用的主页面,提供导航功能
chatDetail.vue - 聊天详情页面
<template>聊天界面</template>

存在意义

  • 💬 聊天核心界面:实现实际的聊天功能
  • 📚 消息展示器:显示聊天历史和实时消息
  • ⌨️ 消息输入器:提供消息输入和发送功能

在项目中的角色

  • 通信界面:用户进行实时通信的主要界面
  • 消息管理器:管理消息的显示、发送和接收
  • WebSocket客户端:与后端WebSocket服务器通信

为什么需要这个页面

  • 这是即时通讯的核心功能页面
  • 需要实时显示和发送消息
  • 需要良好的用户体验来展示聊天内容

🔗 类之间的协作关系

数据流向图

前端页面 → Controller → Service → Repository → Database
    ↓         ↓         ↓         ↓
WebSocket ← Handler ← Service ← Entity ← Redis

依赖关系图

Controller 依赖 Service
Service 依赖 Repository + Util
Repository 依赖 Entity
Handler 依赖 Service + Util
Config 配置 所有组件

职责分离原则

  • Controller: 只负责HTTP请求处理,不包含业务逻辑
  • Service: 包含所有业务逻辑,协调各个组件
  • Repository: 只负责数据访问,不包含业务逻辑
  • Entity: 只是数据载体,不包含业务方法
  • DTO: 只用于数据传输,不包含业务逻辑
  • Util: 提供通用工具方法,无状态

这种设计确保了:

  • 🔧 高内聚:每个类都有明确的职责
  • 🔗 低耦合:类之间的依赖关系清晰简单
  • 🔄 易维护:修改一个功能不会影响其他功能
  • 🧪 易测试:每个类都可以独立测试
  • 📈 易扩展:可以轻松添加新功能而不影响现有代码
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值