微服务即时通讯系统(服务端)——用户子服务实现逻辑全解析(4)

文章目录

微服务即时通讯系统:用户子服务实现逻辑全解析

在即时通讯(IM)系统中,用户子服务是整个架构的核心基础,负责用户身份管理、认证授权、信息维护等核心能力。本文将详细拆解基于 C++ 技术栈构建的用户子服务实现逻辑,包括核心功能设计、技术选型、数据流转流程及关键接口实现细节。

一、子服务核心定位与技术选型

1. 核心定位

用户子服务的核心能力通过 9 个核心接口落地,每个接口均遵循「参数校验 - 业务处理 - 数据持久化 - 响应反馈」的标准化流程,确保逻辑严谨性与数据一致性。

1. 用户注册(用户名 + 密码)
  1. 从请求中提取昵称和密码参数;
  2. 昵称合法性校验:仅支持字母、数字、连字符(-)、下划线(_),长度限制 3~15 位;
  3. 密码合法性校验:仅支持字母、数字,长度限制 6~15 位;
  4. 唯一性校验:通过昵称查询 MySQL 数据库,判断是否已被注册;
  5. 数据持久化:向 MySQL 新增用户记录(生成唯一用户 ID);
  6. 索引同步:向 Elasticsearch(ES)新增用户核心信息(用户 ID、昵称),支撑搜索功能;
  7. 结果响应:返回注册成功 / 失败状态及对应错误信息。
2. 用户登录(用户名 + 密码)
  1. 从请求中提取昵称和密码参数;
  2. 身份校验:通过昵称查询 MySQL 获取用户信息,比对密码一致性;
  3. 登录态校验:查询 Redis 中用户登录状态标记,避免重复登录;
  4. 会话创建:生成唯一会话 ID(UUID);
  5. 缓存更新:向 Redis 添加「会话 ID - 用户 ID」映射及「用户 ID - 登录状态」标记;
  6. 结果响应:返回登录成功状态及会话 ID(后续接口鉴权凭证)。
3. 获取短信验证码
  1. 从请求中提取手机号码参数;
  2. 手机号格式校验:必须以 1 开头,第二位为 3~9,后续为 9 位数字(共 11 位);
  3. 验证码生成:随机生成 4 位数字验证码;
  4. 短信发送:通过短信平台 SDK 向目标手机号发送验证码;
  5. 缓存存储:生成唯一验证码 ID,将「验证码 ID - 验证码」映射存入 Redis(设置 60 秒过期);
  6. 结果响应:返回验证码 ID(用于后续注册 / 登录校验)。
4. 手机号注册
  1. 从请求中提取手机号码、验证码 ID 及验证码参数;
  2. 手机号格式校验:遵循 11 位标准手机号规则;
  3. 验证码校验:通过验证码 ID 查询 Redis,比对验证码一致性(校验后删除该记录);
  4. 唯一性校验:通过手机号查询 MySQL,判断是否已被注册;
  5. 数据持久化:向 MySQL 新增用户记录(生成唯一用户 ID);
  6. 索引同步:向 ES 新增用户核心信息(用户 ID、手机号);
  7. 结果响应:返回注册成功 / 失败状态。
5. 手机号登录
  1. 从请求中提取手机号码、验证码 ID 及验证码参数;
  2. 手机号格式校验:遵循 11 位标准手机号规则;
  3. 验证码校验:通过验证码 ID 查询 Redis,比对验证码一致性(校验后删除该记录);
  4. 用户校验:通过手机号查询 MySQL,确认用户已注册;
  5. 登录态校验:查询 Redis 中用户登录状态标记,避免重复登录;
  6. 会话创建:生成唯一会话 ID,向 Redis 添加会话映射及登录状态标记;
  7. 结果响应:返回登录成功状态及会话 ID。
6. 获取用户信息
  1. 从请求中提取用户 ID 参数;
  2. 基础信息查询:通过用户 ID 查询 MySQL,获取昵称、手机号、签名、头像文件 ID 等基础信息;
  3. 头像数据获取:若存在头像文件 ID,调用文件服务获取头像二进制数据;
  4. 数据组装:整合基础信息与头像数据,构建完整用户信息;
  5. 结果响应:返回完整用户信息。
7. 设置头像
  1. 从请求中提取用户 ID 及头像二进制数据;
  2. 用户校验:通过用户 ID 查询 MySQL,确认用户存在;
  3. 头像上传:调用文件服务上传头像数据,获取文件唯一 ID;
  4. 数据库更新:将新的头像文件 ID 更新至 MySQL 用户记录;
  5. 索引同步:更新 ES 中用户头像关联信息;
  6. 结果响应:返回更新成功 / 失败状态。
8. 设置昵称
  1. 从请求中提取用户 ID 及新昵称参数;
  2. 昵称合法性校验:遵循「3~15 位、支持字母 / 数字 /-/_」规则;
  3. 用户校验:通过用户 ID 查询 MySQL,确认用户存在;
  4. 数据库更新:将新昵称更新至 MySQL 用户记录;
  5. 索引同步:更新 ES 中用户昵称信息(保障搜索准确性);
  6. 结果响应:返回更新成功 / 失败状态。
9. 设置签名
  1. 从请求中提取用户 ID 及新签名参数;
  2. 用户校验:通过用户 ID 查询 MySQL,确认用户存在;
  3. 数据库更新:将新签名更新至 MySQL 用户记录;
  4. 索引同步:更新 ES 中用户签名关联信息;
  5. 结果响应:返回更新成功 / 失败状态。
10. 设置绑定手机号
  1. 从请求中提取用户 ID、新手机号、验证码 ID 及验证码参数;
  2. 手机号格式校验:遵循 11 位标准手机号规则;
  3. 验证码校验:通过验证码 ID 查询 Redis,比对验证码一致性(校验后删除该记录);
  4. 唯一性校验:通过新手机号查询 MySQL,判断是否已被其他用户绑定;
  5. 数据库更新:将新手机号更新至 MySQL 用户记录;
  6. 索引同步:更新 ES 中用户手机号信息;
  7. 结果响应:返回更新成功 / 失败状态。

2. 关键技术选型

为满足高可用、高性能、可扩展的设计目标,技术栈选型如下:

模块功能 技术框架 / 工具 核心作用
RPC 通信 brpc 提供高性能 RPC 服务,支撑跨服务调用
配置解析 gflags 统一管理服务配置参数
日志输出 spdlog 高性能日志记录,便于问题排查
服务注册发现 etcd 实现服务注册、健康检查与服务发现
关系型存储 MySQL + ODB 持久化存储用户核心信息(结构化数据)
缓存存储 Redis + redis++ 存储登录会话、验证码、登录状态(高性能读写)
搜索引擎 Elasticsearch 支持用户信息模糊搜索(分词索引提升查询效率)
数据校验 自定义工具类 实现用户名、密码、手机号格式校验

二、数据模型设计

(一)MySQL:用户核心数据持久化实现

MySQL 作为关系型数据库,承担用户核心信息的持久化存储职责,所有用户注册、信息修改操作均需通过 MySQL 完成数据落地,核心设计围绕「结构化存储 + 唯一性约束 + CRUD 标准化」展开。

1. 数据模型映射(ODB ORM 实现)

通过 ODB 框架将 C++ 类与 MySQL 表结构关联,确保数据操作的类型安全与便捷性,核心映射代码如下:

cpp 复制代码
#pragma once
#include <odb/core.hxx>
#include <odb/forward.hxx>
#include <odb/nullable.hxx>
#include <odb/database.hxx>
#include <string>
namespace bite_im 
{
    #pragma db object table("user")
    class User
    {
        friend class odb::access;
        public:
            User()
            {}

            User(const std::string& uid
                ,const std::string& nickname
                ,const std::string& password)
                    : user_id_(uid)
                    , nickname_(nickname)
                    , password_(password)
            {}

            User(const std::string& uid
                ,const std::string& phone)
                    : user_id_(uid)
                    , phone_(phone)
                    ,nickname_(uid)
            {}

            unsigned long id()
            {
                return id_;
            }

            //用户id获取与修改
            std::string user_id()
            {
                if(user_id_)
                    return *user_id_;
                return std::string();
            }
            void user_id(const std::string& val)
            {
                user_id_ = val;
            }

            //用户昵称获取与修改
            std::string nickname()
            {
                if(nickname_)
                    return *nickname_;
                return std::string();
            }
            void nickname(const std::string& val)
            {
                nickname_ = val;
            }

            //用户签名获取与修改
            std::string description()
            {
                if(description_)
                    return *description_;
                return std::string();
            }
            void description(const std::string& val)
            {
                description_ = val;
            }

            //用户密码获取与修改
            std::string password()
            {
                if(password_)
                    return *password_;
                return std::string();
            }
            void password(const std::string& val)
            {
                password_ = val;
            }

            //用户手机号获取与修改
            std::string phone()
            {
                if(phone_)
                    return *phone_;
                return std::string();
            }
            void phone(const std::string& val)
            {
                phone_ = val;
            }

            //用户头像文件id获取与修改
            std::string avatar_id()
            {
                if(avatar_id_)
                    return *avatar_id_;
                return std::string();
            }
            void avatar_id(const std::string& val)
            {
                avatar_id_ = val;
            }

            ~User()
            {}
        private:
            #pragma db id auto
            unsigned long id_;  //自增主键
            #pragma db type("varchar(64)") index unique
            odb::nullable<std::string> user_id_;  //用户id
            #pragma db type("varchar(64)") index unique
            odb::nullable<std::string> nickname_;  //用户昵称
            odb::nullable<std::string> description_;  //用户签名
            #pragma db type("varchar(64)")
            odb::nullable<std::string> password_;  //登陆密码
            #pragma db type("varchar(64)") index unique
            odb::nullable<std::string> phone_;  //注册手机号
            #pragma db type("varchar(64)")
            odb::nullable<std::string> avatar_id_;  //头像文件id
    };
};
//odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time person.hxx
//odb -d mysql -q -s ../../odb/user.hxx
  • 关键约束:user_id_(用户唯一标识)、nickname_(昵称)、phone_number_(手机号)均设置唯一约束,避免重复数据;
  • 可空字段:支持手机号、头像、签名等信息后续补充,适配不同注册场景(用户名注册无需绑定手机号)。
2. 核心数据操作接口

基于 ODB 封装标准化 CRUD 操作,支撑所有业务接口的数据持久化需求:

cpp 复制代码
#pragma once
#include "../user/build/user-odb.hxx"
#include "../odb/user.hxx"
#include "LogTool.hpp"
#include <memory>
#include <odb/mysql/database.hxx>
#include <odb/mysql/query.hxx>
#include <odb/result.hxx>
#include <odb/transaction.hxx>
#include <sstream>
#include <string>
#include <vector>

namespace bite_im 
{
    typedef odb::query<bite_im::User> user_query;
    typedef odb::result<bite_im::User> user_result;
    class UserTable
    {
        public:
            UserTable(std::shared_ptr<odb::core::database>& db)
                : db_(db)
            {}

            bool db_check_nullptr()
            {
                return db_ == nullptr;
            }

            bool execute(std::vector<std::string>& sqls)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return false;
                }
                odb::transaction t(db_->begin());
                try
                {   
                    for(auto &sql : sqls)
                    {
                        db_->execute(sql);
                    }
                    LOG_INFO("执行sql语句成功!");
                    t.commit();
                    return true;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 插入失败-原因:{}", e.what());
                }
                return false;
            }

            bool insert(bite_im::User& user)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return false;
                }
                odb::transaction t(db_->begin());
                try
                {
                    db_->persist(user);
                    t.commit();
                    LOG_INFO("插入用户数据user_id:{}成功!", user.user_id());
                    return true;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 插入失败-原因:{}", e.what());
                }
                return false;
            }

            bool update(std::shared_ptr<bite_im::User>& user)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return false;
                }
                odb::transaction t(db_->begin());
                try
                {
                    db_->update(*user);
                    t.commit();
                    LOG_INFO("更新用户数据user_id:{}成功!", user->user_id());
                    return true;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 更新失败-user_id:{}, 原因:{}", user->user_id(), e.what());
                }
                return false;
            }

            std::shared_ptr<bite_im::User> select_by_nickname(const std::string& nickname)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return std::shared_ptr<bite_im::User>();
                }
                if(nickname.empty())
                {
                    LOG_ERROR("空nickname不能查找!");
                    return std::shared_ptr<bite_im::User>();
                }

                odb::transaction t(db_->begin());
                try
                {
                    std::shared_ptr<bite_im::User> res;
                    res.reset(db_->query_one<bite_im::User>(user_query::nickname == nickname));
                    t.commit();
                    //LOG_INFO("查找用户数据nickname:{}成功!", nickname);
                    return res;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 查找失败-nickname:{}, 原因:{}", nickname, e.what());
                }
                return std::shared_ptr<bite_im::User>();
            }

            std::shared_ptr<bite_im::User> select_by_phone(const std::string& phone)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return std::shared_ptr<bite_im::User>();
                }
                if(phone.empty())
                {
                    LOG_ERROR("空phone不能查找!");
                    return std::shared_ptr<bite_im::User>();
                }

                odb::transaction t(db_->begin());
                try
                {
                    std::shared_ptr<bite_im::User> res;
                    res.reset(db_->query_one<bite_im::User>(user_query::phone == phone));
                    t.commit();
                    //LOG_INFO("查找用户数据phone:{}成功!", phone);
                    return res;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 查找失败-phone:{}, 原因:{}", phone, e.what());
                }
                return std::shared_ptr<bite_im::User>();
            }

            std::shared_ptr<bite_im::User> select_by_id(const std::string& user_id)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return std::shared_ptr<bite_im::User>();
                }
                if(user_id.empty())
                {
                    LOG_ERROR("空user_id不能查找!");
                    return std::shared_ptr<bite_im::User>();
                }

                odb::transaction t(db_->begin());
                try
                {
                    std::shared_ptr<bite_im::User> res;
                    res.reset(db_->query_one<bite_im::User>(user_query::user_id == user_id));
                    t.commit();
                    //LOG_INFO("查找用户数据user_id:{}成功!", user_id);
                    return res;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 查找失败-user_id:{}, 原因:{}", user_id, e.what());
                }
                return std::shared_ptr<bite_im::User>();
            }

            std::vector<bite_im::User> select_multi_users(std::vector<std::string> id_list)
            {
                if(db_check_nullptr())
                {
                    LOG_ERROR("数据库未完成初始化!");
                    return {};
                }
                if (id_list.empty())
                {
                    LOG_DEBUG("查询 id 列表为空,返回空结果");
                    return {};
                }

                odb::transaction t(db_->begin());
                try
                {
                    std::vector<bite_im::User> res;
                    std::stringstream ss;
                    ss << "user_id in (";
                    for (size_t i = 0; i < id_list.size(); i++)
                    {
                        ss << "'" << id_list[i] << "',";
                    }
                    std::string condition = ss.str();
                    condition[condition.size()-1] = ')';
                    user_result result(db_->query<bite_im::User>(condition));
                    for(auto it : result)
                    {
                        res.push_back(it);
                    }
                    t.commit();
                    //LOG_INFO("查找复数用户数据成功!");
                    return res;
                }
                catch (const std::exception& e)
                {
                    t.rollback();
                    LOG_ERROR("UserTable 多id查找失败-原因:{}", e.what());
                }
                return {};
            }

            ~UserTable()
            {}
        private:
            std::shared_ptr<odb::core::database> db_;
    };
}
3. 业务场景联动
  • 注册场景(用户名 / 手机号):通过 insert 接口新增用户记录,依赖唯一约束自动拦截重复昵称 / 手机号;
  • 登录场景:通过 select_by_nickname/select_by_phone 查询用户信息,完成密码 / 身份校验;
  • 信息修改场景:通过 select_by_id 查询用户记录,修改后调用 update 接口落地更新。

(二)Redis:高性能缓存与状态管理

Redis 作为内存数据库,聚焦「高频读写 + 临时数据存储 + 状态标记」核心场景,支撑登录态管理、验证码校验等高性能需求,核心设计围绕「键值对结构 + 过期策略 + 快速查询」展开。

1. 缓存数据结构设计

按业务场景划分三类核心缓存,键名设计遵循「前缀 + 唯一标识」规则,确保命名规范与唯一性:

业务场景 键(Key)格式 值(Value) 过期策略 核心代码实现
登录会话映射 session:{会话ID} 用户 ID 无(登出时删除) redis->set("session:" + ssid, user_id);
登录状态标记 login_status:{用户ID} 空字符串(仅占位) 无(连接断开时删除) redis->set("login_status:" + user_id, "");
短信验证码 verify_code:{验证码ID} 4 位数字验证码 60 秒自动过期 redis->set_ex("verify_code:" + cid, vcode, std::chrono::seconds(60));
2. 核心缓存操作封装

基于 redis++ 封装专用操作类,隔离缓存逻辑与业务逻辑,核心代码如下:

cpp 复制代码
#pragma once
#include "LogTool.hpp"
#include <sw/redis++/redis++.h>
#include <memory>
#include <chrono>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <sw/redis++/redis.h>
#include <sw/redis++/utils.h>
#include <utility>  // 用于 std::move



namespace bite_im 
{
    class RedisClientBuilder {
    public:
        /**
        * @brief 构造函数(带默认参数,支持灵活配置)
        * @param host Redis 服务器地址(默认:本地 127.0.0.1)
        * @param port Redis 服务器端口(默认:6379,Redis 默认端口)
        * @param db 要连接的 Redis 数据库编号(默认:0,Redis 默认数据库)
        * @param timeout 连接超时时间(默认:10000ms = 10秒,避免无限阻塞)
        * @param keepalive 是否启用 TCP 保活(默认:true,防止连接被防火墙断开)
        * @throw std::invalid_argument 非法参数(如端口超出范围、超时为负)
        * @throw sw::redis::Error Redis 客户端创建失败(如连接超时、认证失败、服务未启动)
        */
        RedisClientBuilder(std::string host = "127.0.0.1",
            int32_t port = 6379,
            uint8_t db = 0,  
            std::chrono::milliseconds timeout = std::chrono::milliseconds(10000),
            bool keepalive = true) 
        {
            // 1. 参数合法性校验(提前拦截无效配置,避免底层报错)
            validate_params(host, port, db, timeout);

            // 2. 构建 Redis 连接配置对象
            sw::redis::ConnectionOptions conn_opts;
            conn_opts.host = std::move(host);          
            conn_opts.port = static_cast<uint16_t>(port);  
            conn_opts.db = db;                        
            conn_opts.connect_timeout = timeout;      
            conn_opts.keep_alive = keepalive;          

           
            redis_client_ = std::make_shared<sw::redis::Redis>(conn_opts);
            // 3. 可选:验证连接有效性(主动发起一次 ping,确保连接成功)
            ping();
        }

        //禁止拷贝和允许移动
        RedisClientBuilder(const RedisClientBuilder&) = delete;
        RedisClientBuilder& operator=(const RedisClientBuilder&) = delete;
        RedisClientBuilder(RedisClientBuilder&&) noexcept = default;
        RedisClientBuilder& operator=(RedisClientBuilder&&) noexcept = default;
        ~RedisClientBuilder() = default;

        /**
        * @brief 获取 Redis 客户端实例(核心接口)
        * @return std::shared_ptr<sw::redis::Redis> 线程安全的客户端智能指针
        * @throw std::logic_error 客户端未初始化(理论上不会触发,除非构造函数异常)
        */
        std::shared_ptr<sw::redis::Redis> get_client() const 
        {
            if (!redis_client_) 
            {
                throw std::logic_error("RedisClientBuilder: 客户端未初始化(构造函数执行失败)");
            }
            return redis_client_;
        }

        bool ping() const {
            if (!redis_client_) {
                throw std::logic_error("RedisClientBuilder: 客户端未初始化,无法执行 ping");
            }
            // redis++ 的 ping() 方法返回 "PONG" 字符串,此处简化为布尔值
            return redis_client_->ping() == "PONG";
        }

    private:
        void validate_params(const std::string& host, int32_t port, uint8_t db,
                            std::chrono::milliseconds timeout) const 
        {
            // 校验主机地址非空
            if (host.empty()) {
                throw std::invalid_argument("RedisClientBuilder: 服务器地址不能为空");
            }

            // 校验端口范围(1-65535)
            if (port < 1 || port > 65535) {
                throw std::invalid_argument("RedisClientBuilder: 端口号非法(必须在 1-65535 之间),当前值:" + std::to_string(port));
            }

            // 校验数据库编号(通常 Redis 默认配置支持 0-15,可根据实际配置调整)
            if (db > 15) {
                throw std::invalid_argument("RedisClientBuilder: 数据库编号非法(默认支持 0-15),当前值:" + std::to_string(db));
            }

            // 校验超时时间非负
            if (timeout.count() < 0) {
                throw std::invalid_argument("RedisClientBuilder: 超时时间不能为负数,当前值:" + std::to_string(timeout.count()) + "ms");
            }
        }
    private:
        /// Redis 客户端智能指针(自动管理生命周期,线程安全)
        std::shared_ptr<sw::redis::Redis> redis_client_;
    };

    class Session
    {
        public:
            Session(std::shared_ptr<sw::redis::Redis>& redis_client)
                : redis_client_(redis_client)
            {
                if (!redis_client_) 
                {
                    throw std::invalid_argument("Session: Redis客户端不能为空");
                }
            }

            bool append(const std::string& ssid, const std::string& uid)
            {
                if(ssid.empty() || uid.empty())
                {
                    LOG_ERROR("参数不能有空!");
                    return false;
                }
                try
                {
                    if(!redis_client_->set(ssid, uid))
                        throw std::runtime_error("新增ssid失败!-" + ssid);
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("append ssid 发生错误:{}", e.what());
                }
                return false;
            }

            bool remove(const std::string& ssid)
            {
                if(ssid.empty())
                {
                    LOG_ERROR("ssid未初始化!");
                    return false;
                }
                try
                {
                    size_t del_count = redis_client_->del(ssid);
                    if (del_count >= 1) 
                    {
                        LOG_INFO("Session::remove: 删除Session成功-ssid:{}", ssid);
                    }
                    else 
                    {
                        LOG_WARN("Session::remove: Session不存在-ssid:{}", ssid);
                    }
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("remove ssid 发生错误:{}", e.what());
                }
                return false;
            }

            sw::redis::OptionalString uid(const std::string& ssid)
            {
                if(ssid.empty())
                {
                    LOG_ERROR("ssid未初始化!");
                    return sw::redis::OptionalString();
                }

                try
                {
                    sw::redis::OptionalString redis_ret = redis_client_->get(ssid);
                    if (redis_ret) 
                    {
                        // 转换为std::optional(更标准,调用方无需依赖redis++类型)
                        std::string uid = *redis_ret;
                        LOG_INFO("Session::uid(): 查询成功-ssid:{}, uid:{}", ssid, uid);
                        return redis_ret;
                    } 
                    else 
                    {
                        LOG_WARN("Session::uid(): Session不存在-ssid:{}", ssid);
                        return std::nullopt;
                    }
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("uid() 发生错误:{}", e.what());
                }
                return sw::redis::OptionalString();
            }

            ~Session() = default;
        private:
            std::shared_ptr<sw::redis::Redis> redis_client_;
    };

    class Status
    {
        public:
            Status(std::shared_ptr<sw::redis::Redis>& redis_client)
                : redis_client_(redis_client)
            {
                if (!redis_client_) 
                {
                    throw std::invalid_argument("Status: Redis客户端不能为空");
                }
            }

            bool append(const std::string& uid)
            {
                if(uid.empty())
                {
                    LOG_ERROR("参数不能为空!");
                    return false;
                }
                try
                {
                    if(!redis_client_->set(uid, ""))
                        throw std::runtime_error("新增status uid失败!-" + uid);
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("append uid 发生错误:{}", e.what());
                }
                return false;
            }

            bool remove(const std::string& uid)
            {
                if(uid.empty())
                {
                    LOG_ERROR("uid未初始化!");
                    return false;
                }
                try
                {
                    size_t del_count = redis_client_->del(uid);
                    if (del_count >= 1) 
                    {
                        LOG_INFO("Status::remove: 删除Status成功-uid:{}", uid);
                    }
                    else 
                    {
                        LOG_WARN("Status::remove: Status不存在-uid:{}", uid);
                    }
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("remove uid 发生错误:{}", e.what());
                }
                return false;
            }

            bool exists(const std::string& uid)
            {
                if(uid.empty())
                {
                    LOG_ERROR("uid未初始化!");
                    return false;
                }

                try
                {
                    bool ret = redis_client_->exists(uid); // 直接调用 exists 命令
                    LOG_INFO("Status::exists: uid:{} 存在状态:{}", uid, ret ? "是" : "否");
                    return ret;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("exists() 发生错误:{}", e.what());
                }
                return false;
            }

            ~Status() = default;
        private:
            std::shared_ptr<sw::redis::Redis> redis_client_;
    };

    class Codes
    {
        public:
            Codes(std::shared_ptr<sw::redis::Redis>& redis_client)
                : redis_client_(redis_client)
            {
                if (!redis_client_) 
                {
                    throw std::invalid_argument("Codes: Redis客户端不能为空");
                }
            }

            bool append(const std::string &cid
                , const std::string &code
                , const std::chrono::milliseconds &t = std::chrono::milliseconds(300000))
            {
                if(cid.empty() || code.empty())
                {
                    LOG_ERROR("参数不能有空!");
                    return false;
                }
                try
                {
                    if(!redis_client_->set(cid, code, t))
                        throw std::runtime_error("新增cid失败!-" + cid);
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("append cid 发生错误:{}", e.what());
                }
                return false;
            }

            bool remove(const std::string& cid)
            {
                if(cid.empty())
                {
                    LOG_ERROR("cid未初始化!");
                    return false;
                }
                try
                {
                    size_t del_count = redis_client_->del(cid);
                    if (del_count >= 1) 
                    {
                        LOG_INFO("Codes::remove: 删除Codes成功-cid:{}", cid);
                    }
                    else 
                    {
                        LOG_WARN("Codes::remove: Codes不存在-cid:{}", cid);
                    }
                    return true;
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("remove cid 发生错误:{}", e.what());
                }
                return false;
            }

            sw::redis::OptionalString code(const std::string& cid)
            {
                if(cid.empty())
                {
                    LOG_ERROR("cid未初始化!");
                    return sw::redis::OptionalString();
                }

                try
                {
                    sw::redis::OptionalString redis_ret = redis_client_->get(cid);
                    if (redis_ret) 
                    {
                        // 转换为std::optional(更标准,调用方无需依赖redis++类型)
                        std::string code = *redis_ret;
                        LOG_INFO("Codes::code(): 查询成功-cid:{}, code:{}", cid, code);
                        return redis_ret;
                    } 
                    else 
                    {
                        LOG_WARN("Codes::code(): Codes不存在-cid:{}", cid);
                        return std::nullopt;
                    }
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("code() 发生错误:{}", e.what());
                }
                return sw::redis::OptionalString();
            }

            ~Codes() = default;
        private:
            std::shared_ptr<sw::redis::Redis> redis_client_;
    };
};
3. 业务场景联动
  • 登录流程:登录成功后通过 Session::append 存储会话、Status::append 标记登录状态;后续接口通过 Session::get_user_id 鉴权,通过 Status::exists 防重复登录;
  • 验证码流程:获取验证码时通过 Codes::append 存储(60 秒过期),校验时通过 Codes::code 查询比对,校验后通过 Codes::remove 删除,避免重复使用。

(三)Elasticsearch:用户搜索索引管理

Elasticsearch(ES)专注于用户信息的「模糊搜索」场景,通过分词索引提升查询效率,核心设计围绕「轻量存储 + 索引同步 + 搜索适配」展开,仅存储搜索必需字段,避免数据冗余。

1. 索引结构设计

ES 索引仅存储用户搜索核心字段,字段类型适配搜索场景(关键词类型支持精确匹配,文本类型支持分词模糊匹配),索引创建代码如下:

cpp 复制代码
#pragma once
#include "ESTool.hpp"
#include "LogTool.hpp"
#include "../odb/user.hxx"
#include <elasticlient/client.h>
#include <memory>
#include <vector>


namespace bite_im 
{
    class ESUser
    {
        public:
            ESUser(std::vector<std::string> hosturls = {"http://127.0.0.1:9200/"}
                ,int timeout_ms = 5000)
            {
                es_client_ = std::make_shared<elasticlient::Client>(hosturls, timeout_ms);
            }
            ESUser(std::shared_ptr<elasticlient::Client>& es_client)
            {
                es_client_ = es_client;
            }
            bool CreateIndex()
            {
                try
                {
                    ESTool::ESIndex esindex(es_client_);
                    esindex.addFieldMapping("user_id", "keyword")
                        .addFieldMapping("nickname", "text")
                        .addFieldMapping("phone", "keyword")
                        .addFieldMapping("description", "text", false, "standard")
                        .addFieldMapping("avatar_id", "keyword", false, "standard")
                        .setIndexSettings(1, 0);
                    if(!esindex.createIndex("user"))
                    {
                        LOG_WARN("创建用户索引失败:{}", esindex.getLastError());
                        return false;
                    }
                    LOG_INFO("创建用户索引成功");
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("创建用户索引异常:{}", e.what());
                    return false;
                }
                return true;
            }

            bool AppendData(const std::string& userid
                ,const std::string& nickname
                ,const std::string& phone
                ,const std::string& description
                ,const std::string& avatar_id)
            {
                try
                {
                    ESTool::ESInsert esinsert(es_client_);
                    esinsert.SetIndexName("user");
                    esinsert.append("user_id", userid)
                        .append("nickname", nickname)
                        .append("phone", phone)
                        .append("description", description)
                        .append("avatar_id", avatar_id);
                    if(!esinsert.insert(userid))
                    {
                        LOG_ERROR("添加用户数据失败:{}", esinsert.getLastError());
                        return false;
                    }
                    LOG_INFO("用户数据添加成功, userid:{}", userid);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("用户数据添加异常:{}, 异常userid:{}", e.what(), userid);
                    return false;
                }
                return true;
            }

            std::vector<bite_im::User> Search(const std::string& key
                , const std::vector<std::string>& no_uids)
            {
                std::vector<bite_im::User> users;
                try
                {
                    ESTool::ESSearch esserach(es_client_);
                    esserach.SetIndexName("user");
                    esserach.append_must_not("user_id", no_uids, "term");
                    esserach.append_should("nickname", key, "match")
                        .append_should("phone", key, "term")
                        .append_should("user_id", key, "term");
                    
                    auto results = esserach.query();
                    last_query_data_ = esserach.getLastQueryData();
                    if(results.isNull() || results.empty())
                    {
                        LOG_INFO("用户数据搜索成功, key:{}, 无结果返回", key);
                        LOG_TRACE("查询语句:{}", getlastQueryData());
                        return users;
                    }
                    if(results.isArray() == false)
                    {
                        LOG_WARN("用户数据搜索结果格式错误, key:{}", key);
                        return users;
                    }

                    int size = results.size();
                    if(size == 0)
                    {
                        LOG_INFO("用户数据搜索成功, key:{}, 无结果返回", key);
                        return users;
                    }
                    for(int i = 0; i < size; ++i)
                    {
                        bite_im::User user;
                        user.user_id(results[i]["_source"]["user_id"].asString());
                        user.nickname(results[i]["_source"]["nickname"].asString());
                        user.phone(results[i]["_source"]["phone"].asString());
                        user.description(results[i]["_source"]["description"].asString());
                        user.avatar_id(results[i]["_source"]["avatar_id"].asString());
                        users.push_back(user);
                    }
                    LOG_INFO("用户数据搜索成功, key:{}, 排除用户数量:{}", key, no_uids.size());
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("用户数据搜索异常:{}, 异常key:{}", e.what(), key);
                }
                return users;
            }

            std::string getlastQueryData()
            {
                return last_query_data_;
            }

            bool DeleteData(const std::string& userid)
            {
                try
                {
                    ESTool::ESDelete esdelete(es_client_);
                    esdelete.SetIndexName("user");
                    if(!esdelete.DeleteId(userid))
                    {
                        LOG_ERROR("删除用户数据失败:{}", esdelete.getLastError());
                        return false;
                    }
                    LOG_INFO("用户数据删除成功, userid:{}", userid);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("用户数据删除异常:{}, 异常userid:{}", e.what(), userid);
                    return false;
                }
                return true;
            }

            bool DeleteIndex()
            {
                try
                {
                    ESTool::ESDelete esdelete(es_client_);
                    esdelete.SetIndexName("user");
                    if(!esdelete.DeleteIndex())
                    {
                        LOG_ERROR("删除用户索引失败:{}", esdelete.getLastError());
                        return false;
                    }
                    LOG_INFO("用户索引删除成功");
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("用户索引删除异常:{}", e.what());
                    return false;
                }
                return true;
            }
            
            ~ESUser()
            {}
        private:
            std::shared_ptr<elasticlient::Client> es_client_;
            std::string last_query_data_;
    };
}
  • 分词策略:采用 ik_max_word 分词器(中文友好),支持昵称、签名的模糊搜索(如输入「张三」可匹配「张三 123」「爱张三」等);
  • 字段类型:user_idphone_number 设为 keyword 类型,适配精确查询;nicknamedescription 设为 text 类型,适配模糊搜索。
2. 核心索引操作封装

ES 操作与 MySQL 数据强同步,确保搜索结果的实时性,核心代码如下:

cpp 复制代码
// 新增/更新用户索引(注册/信息修改场景)
bool AppendData(const std::string& userid
                ,const std::string& nickname
                ,const std::string& phone
                ,const std::string& description
                ,const std::string& avatar_id)
{
    try
    {
        ESTool::ESInsert esinsert(es_client_);
        esinsert.SetIndexName("user");
        esinsert.append("user_id", userid)
            .append("nickname", nickname)
            .append("phone", phone)
            .append("description", description)
            .append("avatar_id", avatar_id);
        if(!esinsert.insert(userid))
        {
            LOG_ERROR("添加用户数据失败:{}", esinsert.getLastError());
            return false;
        }
        LOG_INFO("用户数据添加成功, userid:{}", userid);
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("用户数据添加异常:{}, 异常userid:{}", e.what(), userid);
        return false;
    }
    return true;
}

std::vector<bite_im::User> Search(const std::string& key
                                  , const std::vector<std::string>& no_uids)
{
    std::vector<bite_im::User> users;
    try
    {
        ESTool::ESSearch esserach(es_client_);
        esserach.SetIndexName("user");
        esserach.append_must_not("user_id", no_uids, "term");
        esserach.append_should("nickname", key, "match")
            .append_should("phone", key, "term")
            .append_should("user_id", key, "term");

        auto results = esserach.query();
        last_query_data_ = esserach.getLastQueryData();
        if(results.isNull() || results.empty())
        {
            LOG_INFO("用户数据搜索成功, key:{}, 无结果返回", key);
            LOG_TRACE("查询语句:{}", getlastQueryData());
            return users;
        }
        if(results.isArray() == false)
        {
            LOG_WARN("用户数据搜索结果格式错误, key:{}", key);
            return users;
        }

        int size = results.size();
        if(size == 0)
        {
            LOG_INFO("用户数据搜索成功, key:{}, 无结果返回", key);
            return users;
        }
        for(int i = 0; i < size; ++i)
        {
            bite_im::User user;
            user.user_id(results[i]["_source"]["user_id"].asString());
            user.nickname(results[i]["_source"]["nickname"].asString());
            user.phone(results[i]["_source"]["phone"].asString());
            user.description(results[i]["_source"]["description"].asString());
            user.avatar_id(results[i]["_source"]["avatar_id"].asString());
            users.push_back(user);
        }
        LOG_INFO("用户数据搜索成功, key:{}, 排除用户数量:{}", key, no_uids.size());
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("用户数据搜索异常:{}, 异常key:{}", e.what(), key);
    }
    return users;
}
3. 业务场景联动
  • 注册场景:用户注册成功后,调用 AppendData 接口向 ES 新增索引(存储用户 ID + 昵称 / 手机号);
  • 信息修改场景:用户修改昵称 / 签名 / 手机号后,调用 AppendData 接口同步更新 ES 索引,确保搜索结果准确;
  • 搜索场景:前端发起用户搜索请求时,调用 Search 接口,通过多字段模糊匹配返回符合条件的用户列表。

三、核心功能实现流程

1. 用户注册(用户名 + 密码)

流程拆解

  1. 参数校验:检查昵称(3-15 位,支持字母 / 数字 /_/-)和密码(6-15 位,支持字母 / 数字)格式合法性;
  2. 唯一性校验:通过昵称查询 MySQL,确认昵称未被占用;
  3. 数据持久化:
    • 生成唯一用户 ID(UUID);
    • 插入 MySQL 用户表(用户 ID、昵称、密码);
    • 同步用户信息到 Elasticsearch(用于搜索);
  4. 响应结果:返回注册成功 / 失败状态及错误信息。

关键代码片段

cpp 复制代码
bool validate_nickname(const std::string& nickname)
{
    if(nickname.size() >= 22)
    {
        return false;
    }
    return true;
}

bool validate_password(const std::string& password)
{
    if(password.size() < 6 || password.size() > 15)
    {
        LOG_WARN("用户设置密码长度不合法, 长度:{}", password.size());
        return false;
    }
    for(char c : password)
    {
        if(!((c >= '0' && c <= '9')
             || (c >= 'a' && c <= 'z')
             || (c >= 'A' && c <= 'Z')
             || c == '_' || c == '-' ))
        {
            LOG_WARN("用户设置密码包含非法字符:{}", c);
            return false;
        }
    }
    return true;
}

void UserRegister(google::protobuf::RpcController* controller,
                  const ::bite_im::UserRegisterReq* request,
                  ::bite_im::UserRegisterRsp* response,
                  ::google::protobuf::Closure* done) override
{
    brpc::ClosureGuard guard(done);
    LOG_DEBUG("收到用户注册请求!");
    //定义一个错误处理函数,当出错的时候被调用
    auto err_response = [this, response](const std::string &errmsg) -> void {
        response->set_success(false);
        response->set_errmsg(errmsg);
        return;
    };
    try
    {
        response->set_request_id(request->request_id());

        //1.参数校验
        std::string nickname = request->nickname();
        std::string password = request->password();
        if(!validate_nickname(nickname))
        {
            LOG_DEBUG("签名不合法,长度不能超过22个字符");
            err_response("昵称不合法,长度不能超过22个字符");
            return;
        }
        if(!validate_password(password))
        {
            LOG_DEBUG("密码不合法,长度6-15,仅支持数字字母下划线和减号");
            err_response("密码不合法,长度6-15,仅支持数字字母下划线和减号");
            return;
        }
        //2.检查昵称是否已被使用
        auto exist_user = mysql_user_->select_by_nickname(nickname);
        if(exist_user)
        {
            LOG_DEBUG("昵称已被使用,请更换昵称");
            err_response("昵称已被使用,请更换昵称");
            return;
        }
        //3.插入mysql用户数据
        bite_im::User new_user;
        new_user.user_id(bite_im::uuid());
        new_user.nickname(nickname);
        new_user.password(password);
        bool ret = mysql_user_->insert(new_user);
        if(ret == false)
        {
            LOG_DEBUG("昵称注册时数据库插入数据失败");
            err_response("数据库插入数据失败!");
            return;
        }
        //4.插入es搜索引擎
        ret = es_user_->AppendData(new_user.user_id(), nickname, "", "", "");
        if(ret == false)
        {
            LOG_DEBUG("昵称注册时ES插入数据失败");
            err_response("ES插入数据失败!");
            return;
        }
        //5.响应
        response->set_success(true);
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("用户昵称注册异常,异常原因:{}", e.what());
        err_response("触发昵称注册异常:" + std::string(e.what()));
    }
}

2. 用户登录(双模式支持)

支持「用户名 + 密码」和「手机号 + 验证码」两种登录模式,核心流程一致:

(1)用户名 + 密码登录
  1. 参数校验:获取昵称和密码;
  2. 身份校验:通过昵称查询 MySQL,比对密码一致性;
  3. 登录态校验:检查 Redis 中用户登录状态,防止重复登录;
  4. 会话创建:
    • 生成会话 ID(UUID);
    • Redis 中存储「会话 ID - 用户 ID」映射和「用户 ID - 登录状态」标记;
  5. 响应结果:返回会话 ID(后续接口鉴权凭证)。
cpp 复制代码
void UserLogin(google::protobuf::RpcController* controller,
                const ::bite_im::UserLoginReq* request,
                ::bite_im::UserLoginRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到用户登录请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    //1.校验用户登录账户密码
                    std::string nickname = request->nickname();
                    std::string password = request->password();
                    auto user_ptr = mysql_user_->select_by_nickname(nickname);
                    if(!user_ptr || user_ptr->password() != password)
                    {
                        LOG_DEBUG("用户{}登录账户或者密码不正确!", nickname);
                        err_response("用户登录账户或者密码不正确!");
                        return;
                    }

                    //2.检测用户登录状态
                    bool user_status = redis_status_->exists(user_ptr->user_id());
                    if(user_status)
                    {
                        LOG_DEBUG("用户{}已经登录账户!", nickname);
                        err_response("用户已经登录账户!");
                        return;
                    }

                    //3.创建用户登录状态
                    std::string ssid = bite_im::uuid();
                    bool ret = redis_session_->append(ssid, user_ptr->user_id());
                    if(!ret)
                    {
                        LOG_DEBUG("创建redis会话异常!nickname-{}", nickname);
                        err_response("redis会话异常!");
                        return;
                    }
                    ret = redis_status_->append(user_ptr->user_id());
                    if(!ret)
                    {
                        LOG_DEBUG("创建redis状态异常!nickname-{}", nickname);
                        err_response("redis状态异常!");
                        return;
                    }
                    //4.响应
                    response->set_success(true);
                    response->set_login_session_id(ssid);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("用户登录异常,异常原因:{}", e.what());
                    err_response("触发登录异常:" + std::string(e.what()));
                }
            }
(2)手机号 + 验证码登录
  1. 参数校验:校验手机号格式(11 位,以 1 开头,第二位 3-9);
  2. 验证码校验:通过验证码 ID 查询 Redis,比对验证码一致性(校验后删除验证码);
  3. 用户校验:通过手机号查询 MySQL,确认用户已注册;
  4. 后续流程与用户名登录一致(登录态校验、会话创建)。
cpp 复制代码
void PhoneLogin(google::protobuf::RpcController* controller,
                const ::bite_im::PhoneLoginReq* request,
                ::bite_im::PhoneLoginRsp* response,
                ::google::protobuf::Closure* done) override
{
    brpc::ClosureGuard guard(done);
    LOG_DEBUG("收到用户手机号登录请求!");
    //定义一个错误处理函数,当出错的时候被调用
    auto err_response = [this, response](const std::string &errmsg) -> void {
        response->set_success(false);
        response->set_errmsg(errmsg);
        return;
    };
    try
    {
        response->set_request_id(request->request_id());

        //1.参数校验
        std::string phone = request->phone_number();
        std::string cid = request->verify_code_id();
        std::string code = request->verify_code();
        if(check_phone_number(phone) == false)
        {
            err_response("手机号不合法!");
            return;
        }
        //2.检查手机号是否已被注册与验证码是否合法
        auto exist_user = mysql_user_->select_by_phone(phone);
        if(!exist_user)
        {
            LOG_DEBUG("{}-手机号-{}未被注册!", request->request_id(), phone);
            err_response("手机号未注册!");
            return;
        }
        auto check_code = redis_codes_->code(cid);
        if(!check_code || *check_code != code)
        {
            LOG_DEBUG("验证码不正确或者已经过期!");
            err_response("验证码不正确或者已经过期!");
            return;
        }
        redis_codes_->remove(cid);
        // 3.检验是否已经登录
        bool login_status = redis_status_->exists(exist_user->user_id());
        if (login_status)
        {
            LOG_DEBUG("{}-用户-{}已经登录!", request->request_id(), exist_user->user_id());
            err_response("用户已经登录!");
            return;
        }
        // 4.创建登录会话
        std::string ssid = bite_im::uuid();
        bool ret = redis_session_->append(ssid, exist_user->user_id());
        if(!ret)
        {
            LOG_DEBUG("{}-手机号登录redis会话异常!-phone:{}",request->request_id(), phone);
            err_response("redis会话异常!");
            return;
        }
        ret = redis_status_->append(exist_user->user_id());
        if(!ret)
        {
            LOG_DEBUG("{}-手机号登录redis状态异常!-phone:{}",request->request_id(), phone);
            err_response("redis状态异常!");
            return;
        }
        // 5.响应
        response->set_login_session_id(ssid);
        response->set_success(true);
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("用户手机号登录异常,异常原因:{}", e.what());
        err_response("触发登录手机号异常:" + std::string(e.what()));
    }
}

3. 用户信息管理(头像 / 昵称 / 签名 / 手机号修改)

所有信息修改流程遵循「校验 - 更新 - 同步」三步法,以头像修改为例:

  1. 用户校验:通过用户 ID 查询 MySQL,确认用户存在;
  2. 跨服务交互:调用文件服务上传头像文件,获取文件唯一 ID;
  3. 数据更新:
    • 更新 MySQL 用户表中的头像文件 ID;
    • 同步更新 Elasticsearch 中的用户信息;
  4. 响应结果:返回更新成功 / 失败状态。
cpp 复制代码
void SetUserAvatar(google::protobuf::RpcController* controller,
                const ::bite_im::SetUserAvatarReq* request,
                ::bite_im::SetUserAvatarRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到设置头像请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1. 检验用户是否存在
                    std::string uid = request->user_id();
                    auto user = mysql_user_->select_by_id(uid);
                    if(!user)
                    {
                        LOG_DEBUG("{}-用户不存在,不能修改用户数据!-user_id:{}",request->request_id(), uid);
                        err_response("用户不存在!");
                        return;
                    }
                    // 2.通过文件子服务上传用户头像数据
                    //选择信道
                    brpcChannelTool::channelptr ptr = service_manager_->Choose(file_service_name_);
                    //实例化文件rpc服务
                    bite_im::FileService_Stub stub(ptr.get());
                    //请求
                    bite_im::PutSingleFileReq req;
                    std::string rqid = request->request_id();
                    req.set_request_id(rqid); //请求id
                    req.mutable_file_data()->set_file_name(request->user_id());  //请求文件名
                    std::string data = request->avatar();
                    req.mutable_file_data()->set_file_size(data.size()); //文件大小
                    req.mutable_file_data()->set_file_content(data);  //文件内容
                    //响应
                    bite_im::PutSingleFileRsp rsp;
                    //发起rpc远程调用
                    brpc::Controller ctl;
                    stub.PutSingleFile(&ctl, &req, &rsp, nullptr);
                    if(ctl.Failed() == true)
                    {
                        LOG_DEBUG("{}-rpc文件子服务调用失败-{},不能上传文件数据!-user_id:{}",request->request_id(), ctl.ErrorText(), uid);
                        err_response("rpc文件子服务调用失败!");
                        return;
                    }
                    if(rsp.success() == false)
                    {
                        LOG_DEBUG("{}-文件子服务返回响应失败-{},不能上传文件数据!-user_id:{}",request->request_id(), rsp.errmsg(), uid);
                        err_response("文件子服务返回响应失败!");
                        return;
                    }
                    // 3. 更新文件id到数据库
                    user->avatar_id(rsp.file_info().file_id());
                    bool ret = mysql_user_->update(user);
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新头像数据到数据库失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新头像数据到数据库失败!");
                        return;
                    }
                    // 4. 更新用户数据到ES
                    ret = es_user_->AppendData(uid, user->nickname(), user->phone(), user->description(), user->avatar_id());
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新头像数据到ES失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新头像数据到ES失败!");
                        return;
                    }
                    // 5. 成功响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("设置头像异常,异常原因:{}", e.what());
                    err_response("触发设置头像异常:" + std::string(e.what()));
                }
            }

            void SetUserNickname(google::protobuf::RpcController* controller,
                const ::bite_im::SetUserNicknameReq* request,
                ::bite_im::SetUserNicknameRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到设置头像请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1. 检验用户是否存在
                    std::string uid = request->user_id();
                    auto user = mysql_user_->select_by_id(uid);
                    if(!user)
                    {
                        LOG_DEBUG("{}-用户不存在,不能修改用户数据!-user_id:{}",request->request_id(), uid);
                        err_response("用户不存在!");
                        return;
                    }
                    // 2.更改用户名
                    user->nickname(request->nickname());
                    // 3. 更新用户数据到数据库
                    bool ret = mysql_user_->update(user);
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新昵称数据到数据库失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新昵称数据到数据库失败!");
                        return;
                    }
                    // 4. 更新用户数据到ES
                    ret = es_user_->AppendData(uid, user->nickname(), user->phone(), user->description(), user->avatar_id());
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新昵称数据到ES失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新昵称数据到ES失败!");
                        return;
                    }
                    // 5. 成功响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("设置昵称修改异常,异常原因:{}", e.what());
                    err_response("触发设置昵称修改异常:" + std::string(e.what()));
                }
            }

            void SetUserDescription(google::protobuf::RpcController* controller,
                const ::bite_im::SetUserDescriptionReq* request,
                ::bite_im::SetUserDescriptionRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到设置签名请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1. 检验用户是否存在
                    std::string uid = request->user_id();
                    auto user = mysql_user_->select_by_id(uid);
                    if(!user)
                    {
                        LOG_DEBUG("{}-用户不存在,不能修改用户数据!-user_id:{}",request->request_id(), uid);
                        err_response("用户不存在!");
                        return;
                    }
                    // 2.更改签名
                    user->description(request->description());
                    // 3. 更新用户数据到数据库
                    bool ret = mysql_user_->update(user);
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新签名数据到数据库失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新签名数据到数据库失败!");
                        return;
                    }
                    // 4. 更新用户数据到ES
                    ret = es_user_->AppendData(uid, user->nickname(), user->phone(), user->description(), user->avatar_id());
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新签名数据到ES失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新签名数据到ES失败!");
                        return;
                    }
                    // 5. 成功响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("设置签名更改异常,异常原因:{}", e.what());
                    err_response("触发设置签名更改异常:" + std::string(e.what()));
                }
            }

            void SetUserPhoneNumber(google::protobuf::RpcController* controller,
                const ::bite_im::SetUserPhoneNumberReq* request,
                ::bite_im::SetUserPhoneNumberRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到设置手机号请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1. 检验用户是否存在
                    std::string uid = request->user_id();
                    auto user = mysql_user_->select_by_id(uid);
                    if(!user)
                    {
                        LOG_DEBUG("{}-用户不存在,不能修改用户数据!-user_id:{}",request->request_id(), uid);
                        err_response("用户不存在!");
                        return;
                    }
                    // 2.更改手机号
                    // 2.1参数校验
                    std::string phone = user->phone();
                    std::string cid = request->phone_verify_code_id();
                    std::string code = request->phone_verify_code();
                    if(check_phone_number(phone) == false)
                    {
                        err_response("手机号不合法!");
                        return;
                    }
                    // 2.2检查手机号是否已被注册与验证码是否合法
                    auto exist_user = mysql_user_->select_by_phone(phone);
                    if(!exist_user)
                    {
                        LOG_DEBUG("{}-手机号-{}未被注册!", request->request_id(), phone);
                        err_response("手机号未注册!");
                        return;
                    }
                    auto check_code = redis_codes_->code(cid);
                    if(!check_code || *check_code != code)
                    {
                        LOG_DEBUG("验证码不正确或者已经过期!");
                        err_response("验证码不正确或者已经过期!");
                        return;
                    }
                    redis_codes_->remove(cid);
                    user->phone(request->phone_number());
                    // 3. 更新用户数据到数据库
                    bool ret = mysql_user_->update(user);
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新手机号数据到数据库失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新手机号数据到数据库失败!");
                        return;
                    }
                    // 4. 更新用户数据到ES
                    ret = es_user_->AppendData(uid, user->nickname(), user->phone(), user->description(), user->avatar_id());
                    if(ret == false)
                    {
                        LOG_DEBUG("{}-更新手机号数据到ES失败,不能上传文件数据!-user_id:{}",request->request_id(), uid);
                        err_response("更新手机号数据到ES失败!");
                        return;
                    }
                    // 5. 成功响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("设置手机号更改异常,异常原因:{}", e.what());
                    err_response("触发设置手机号更改异常:" + std::string(e.what()));
                }
            }

关键亮点

  • 头像文件存储在独立文件服务,用户子服务仅存储文件 ID,符合微服务职责拆分原则;
  • 所有信息修改均同步更新 ES,保证搜索数据一致性。

4. 用户信息查询(单用户 / 批量用户)

(1)单用户信息查询
  1. 通过用户 ID 查询 MySQL,获取基础信息(昵称、手机号、签名、头像文件 ID);
  2. 若存在头像文件 ID,调用文件服务获取头像二进制数据;
  3. 组装完整用户信息并返回。
cpp 复制代码
void GetUserInfo(google::protobuf::RpcController* controller,
                const ::bite_im::GetUserInfoReq* request,
                ::bite_im::GetUserInfoRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                LOG_DEBUG("收到用户信息获取请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1.获取用户id
                    std::string uid = request->user_id();
                    // 2.从MySQL数据库获取用户数据
                    auto user = mysql_user_->select_by_id(uid);
                    if(!user)
                    {
                        LOG_DEBUG("{}-用户不存在,不能获取用户数据!-user_id:{}",request->request_id(), uid);
                        err_response("用户不存在!");
                        return;
                    }
                    // 3.设置用户数据
                    auto rsp_user = response->mutable_user_info();
                    rsp_user->set_user_id(user->user_id());
                    rsp_user->set_nickname(user->nickname());
                    rsp_user->set_phone(user->phone());
                    rsp_user->set_description(user->description());
                    
                    // 4.获取用户头像数据
                    std::string avatar_id = user->avatar_id();
                    if(avatar_id.empty() == false)
                    {
                        brpcChannelTool::channelptr ptr = service_manager_->Choose(file_service_name_);
                        if(!ptr)
                        {
                            LOG_ERROR("请求id:{} - 未找到文件管理子服务节点 -file_service_name_:{} -user_id:{}!", 
                                request->request_id(), file_service_name_, uid);
                            err_response("未找到文件管理子服务节点!");
                            return;
                        }
                        bite_im::FileService_Stub stub(ptr.get());

                        //获取单个文件的请求
                        bite_im::GetSingleFileReq req;
                        req.set_request_id(request->request_id());
                        req.set_file_id(avatar_id);
                        //获取单个文件的回复
                        bite_im::GetSingleFileRsp rsp;
                        brpc::Controller ctl;
                        //发起rpc调用
                        stub.GetSingleFile(&ctl, &req, &rsp, nullptr);
                        if(ctl.Failed() == true || rsp.success() == false)
                        {
                            LOG_ERROR("请求id:{} - rpc调用文件服务失败 -file_service_name_:{} -失败原因:{}!", 
                                request->request_id(), file_service_name_, ctl.ErrorText());
                            err_response("rpc调用文件服务失败!");
                            return;
                        }
                        rsp_user->set_avatar(rsp.file_data().file_content());
                    }
                    // 5.响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("获取用户信息异常,异常原因:{}", e.what());
                    err_response("触发用户信息获取异常:" + std::string(e.what()));
                }
            }
(2)批量用户信息查询
  1. 批量获取用户 ID 列表,批量查询 MySQL;
  2. 收集所有用户的头像文件 ID,批量调用文件服务获取头像数据;
  3. 以用户 ID 为键,组装多用户信息映射并返回(提升批量查询效率)。
cpp 复制代码
void GetMultiUserInfo(google::protobuf::RpcController* controller,
                const ::bite_im::GetMultiUserInfoReq* request,
                ::bite_im::GetMultiUserInfoRsp* response,
                ::google::protobuf::Closure* done) override
            {
                brpc::ClosureGuard guard(done);
                 LOG_DEBUG("收到批量用户信息获取请求!");
                //定义一个错误处理函数,当出错的时候被调用
                auto err_response = [this, response](const std::string &errmsg) -> void {
                    response->set_success(false);
                    response->set_errmsg(errmsg);
                    return;
                };
                try
                {
                    response->set_request_id(request->request_id());
                    // 1.批量获取用户id
                    std::vector<std::string> uids;
                    for (size_t i = 0; i < request->users_id_size(); i++)
                    {
                        uids.push_back(request->users_id(i));
                    }
                    // 2.从MySQL数据库批量获取用户数据
                    std::vector<std::shared_ptr<bite_im::User>> user_list;
                    for (size_t i = 0; i < uids.size(); i++)
                    {
                        std::string uid = uids[i];
                        std::shared_ptr<bite_im::User> user = mysql_user_->select_by_id(uid);
                        if(!user)
                        {
                            LOG_DEBUG("{}-用户不存在,不能获取用户数据!-user_id:{}",request->request_id(), uid);
                            err_response("有用户不存在!");
                            return;
                        }
                        user_list.push_back(user);
                    }
                    
                    // 3.批量获取用户头像文件数据
                    brpcChannelTool::channelptr ptr = service_manager_->Choose(file_service_name_);
                    if(!ptr)
                    {
                        LOG_ERROR("请求id:{} - 未找到文件管理子服务节点 -file_service_name_:{}", 
                            request->request_id(), file_service_name_);
                        err_response("未找到文件管理子服务节点!");
                        return;
                    }
                    bite_im::FileService_Stub stub(ptr.get());
                    bite_im::GetMultiFileReq req;
                    req.set_request_id(request->request_id());
                    for (size_t i = 0; i < user_list.size(); i++)
                    {
                        std::string fileid = user_list[i]->avatar_id();
                        if(fileid.empty())
                            continue;
                        req.add_file_id_list(fileid);
                    }
                    bite_im::GetMultiFileRsp rsp;
                    brpc::Controller ctl;
                    stub.GetMultiFile(&ctl, &req, &rsp, nullptr);
                    if(ctl.Failed() == true || rsp.success() == false)
                    {
                        LOG_ERROR("请求id:{} - rpc调用文件服务失败 -file_service_name_:{} -失败原因:{}!", 
                            request->request_id(), file_service_name_, ctl.ErrorText());
                        err_response("rpc调用文件服务失败!");
                        return;
                    }
                    auto filedata_map = rsp.mutable_file_data();
                    auto users_info_map = response->mutable_users_info();
                    // 4.批量设置用户数据
                    for (size_t i = 0; i < user_list.size(); i++)
                    {
                        bite_im::UserInfo userinfo;
                        userinfo.set_user_id(user_list[i]->user_id());
                        userinfo.set_nickname(user_list[i]->nickname());
                        userinfo.set_phone(user_list[i]->phone());
                        userinfo.set_description(user_list[i]->description());
                        if(user_list[i]->avatar_id().empty())
                        {
                            (*users_info_map)[user_list[i]->user_id()] = userinfo;
                            continue;
                        }
                        std::string avatar = (*filedata_map)[user_list[i]->avatar_id()].file_content();
                        userinfo.set_avatar(avatar);
                        //设置进入响应
                        (*users_info_map)[user_list[i]->user_id()] = userinfo;
                    }

                    // 5.响应
                    response->set_success(true);
                }
                catch (const std::exception& e)
                {
                    LOG_ERROR("获取用户信息异常,异常原因:{}", e.what());
                    err_response("触发用户信息获取异常:" + std::string(e.what()));
                }
            }

四、跨服务交互设计

用户子服务需与两个核心外部服务交互,依赖 etcd 实现服务发现:

  1. 文件服务
    • 用途:上传 / 下载用户头像文件;
    • 交互方式:通过 brpc 调用文件服务的 PutSingleFile(上传)和 GetMultiFile(批量下载)接口;
    • 服务发现:通过 etcd 监控文件服务节点,动态选择可用节点。
  2. 消息服务
    • 用途:获取用户最近聊天消息(预留接口,支持会话列表展示);
    • 交互方式:通过 brpc 调用消息服务接口(本文代码中已预留服务调用框架)。

五、高可用与安全性设计

  1. 防重复登录:Redis 中存储用户登录状态标记,登录时校验,避免同一账号多端同时登录;
  2. 验证码安全:验证码有效期 60 秒,验证后立即删除,防止恶意复用;
  3. 参数合法性校验:所有输入参数(用户名、密码、手机号)均做格式校验,防止非法数据注入;
  4. 异常处理:所有核心流程均捕获异常,记录详细日志,返回友好错误信息;
  5. 服务注册发现:基于 etcd 实现服务注册与健康检查,支持服务节点动态扩容。

六、总结

本用户子服务的实现严格遵循微服务架构设计原则,核心亮点如下:

  1. 职责单一:专注用户身份与信息管理,不耦合其他业务逻辑;
  2. 分层存储:MySQL 存储核心数据、Redis 存储高频临时数据、ES 支撑搜索,各取所长;
  3. 高扩展性:通过 etcd 实现服务发现,支持多节点部署与动态扩容;
  4. 安全性强:完善的参数校验、登录态管理、验证码机制,保障系统安全;
  5. 跨服务兼容:基于 brpc 构建标准化 RPC 接口,便于与其他微服务集成。

该实现方案可直接支撑即时通讯系统的核心用户管理需求,同时预留了扩展空间(如第三方登录、用户标签管理等),具备较强的实用性与可扩展性。

服务端整体实现与设计架构

用户子服务的服务端实现遵循「配置驱动 + 模块化构建 + 标准化流程」设计思路,通过 gflags 统一管理配置、Builder 模式封装初始化流程、微服务架构实现跨服务协同,确保服务的可配置性、可扩展性与高可用性。

(一)核心设计理念

  1. 配置与业务解耦:通过 gflags 定义所有可配置参数(端口、数据库地址、日志级别等),支持启动时动态调整,无需修改代码即可适配不同部署环境;
  2. 模块化分层:将服务初始化拆分为配置解析、日志初始化、依赖组件构建、RPC 服务启动等独立步骤,每层职责单一,便于维护与扩展;
  3. 依赖注入:通过 Builder 类统一管理 MySQL、Redis、ES、etcd 等依赖组件的实例化,组件间通过接口交互,降低耦合度;
  4. 微服务协同:基于 etcd 实现服务注册与发现,无缝对接文件子服务,支撑跨服务的头像上传 / 下载等功能。

(二)核心配置定义(gflags 实现)

服务端所有可配置项通过 gflags 定义,覆盖日志、服务注册、RPC 通信、数据库、缓存、搜索引擎等核心模块,配置项命名规范、含义清晰,支持命令行传入或配置文件加载。

1. 配置项分类与说明

配置模块 核心配置项 含义说明 默认值
日志配置 log_mode 日志模式:false(调试模式)、true(发布模式) false
log_output_mode 输出方式:false(控制台)、true(文件) false
log_file 发布模式日志文件名 app.log
log_level 日志等级(对应 spdlog 级别) 2(INFO 级)
服务注册(etcd) registry_host etcd 服务地址 http://127.0.0.1:2379
base_service 服务监控根目录 /service
instance_name 当前服务实例名称 /user_service/instance
access_host 服务外部访问地址(IP: 端口) 127.0.0.1:10003
file_service_name 文件子服务在 etcd 的注册路径(用于服务发现) /service/file_service
RPC 服务 rpc_listen_port RPC 监听端口 10003
rpc_timeout RPC 调用超时时间(-1 表示无超时) -1
rpc_threads RPC IO 线程数 1
MySQL 数据库 dbuser/dbpassword 数据库用户名 / 密码 root/123456
dbname/dbhost/dbport 数据库名 / 地址 / 端口 user/127.0.0.1/3306
dbcharset 数据库字符集(支持 emoji 需 utf8mb4) utf8mb4
db_con_num 数据库连接池最大连接数 20
Elasticsearch eshost ES 服务地址 http://127.0.0.1:9200/
Redis 缓存 redis_host/redis_port Redis 地址 / 端口 127.0.0.1/6379
redis_db Redis 数据库编号 0
redis_timeout Redis 连接超时时间(毫秒) 10000
redis_keepalive 是否启用 TCP 长连接 true

2. 配置项核心代码

cpp 复制代码
// 日志配置
DEFINE_bool(log_mode, false, "日志模式: false(调试模式,默认), true(发布模式)");
DEFINE_bool(log_output_mode, false, "控制台模式: false, 文件模式:true");
DEFINE_string(log_file, "app.log", "发布模式下日志名称");
DEFINE_int32(log_level, 2, "发布模式下日志等级设置");

// etcd 服务注册与发现
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/user_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10003", "当前实例的外部访问地址");
DEFINE_string(file_service_name, "/service/file_service", "文件子服务在etcd的注册路径");

// RPC 服务配置
DEFINE_int32(rpc_listen_port, 10003, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");

// MySQL 配置
DEFINE_string(dbuser, "root", "数据库用户名");
DEFINE_string(dbpassword, "123456", "数据库密码");
DEFINE_string(dbname, "user", "数据库名称");
DEFINE_string(dbhost, "127.0.0.1", "数据库主机地址");
DEFINE_int32(dbport, 3306, "数据库端口");
DEFINE_string(dbcharset, "utf8mb4", "数据库字符集");
DEFINE_int32(db_con_num, 20, "数据库最大连接池数量");

// ES 配置
DEFINE_string(eshost, "http://127.0.0.1:9200/", "ES主机地址");

// Redis 配置
DEFINE_string(redis_host, "127.0.0.1", "Redis主机地址");
DEFINE_int32(redis_port, 6379, "Redis端口");
DEFINE_int32(redis_db, 0, "Redis数据库编号");
DEFINE_int64(redis_timeout, 10000, "Redis连接超时时间(毫秒)");
DEFINE_bool(redis_keepalive, true, "Redis是否启用长连接");

(三)服务初始化流程(Builder 模式)

服务端通过 UserServerBuilder 类封装所有初始化逻辑,采用「链式调用 + 分步构建」方式,确保初始化流程清晰、可扩展,核心流程如下:

1. 初始化步骤拆解

cpp 复制代码
int main(int argc, char* argv[])
{
    // 1. 解析命令行配置
    gflags::ParseCommandLineFlags(&argc, &argv, true);

    // 2. 初始化日志模块
    LogModule::Log::Init(FLAGS_log_mode
        , FLAGS_log_file
        , FLAGS_log_level
        , FLAGS_log_output_mode);

    // 3. 构建 UserServer 实例(Builder 模式)
    bite_im::UserServerBuilder usb;
    
    // 3.1 初始化服务管理器(用于跨服务调用文件子服务)
    usb.make_channel_manager_object(FLAGS_file_service_name);
    
    // 3.2 初始化 MySQL 客户端(用户核心数据存储)
    usb.make_mysql_object(FLAGS_dbuser
        , FLAGS_dbpassword
        , FLAGS_dbname
        , FLAGS_dbhost
        , FLAGS_dbport
        , FLAGS_dbcharset
        , FLAGS_db_con_num);
    
    // 3.3 初始化 ES 客户端(用户搜索索引)
    usb.make_es_object({FLAGS_eshost});
    
    // 3.4 初始化 etcd 监控(监听文件子服务上线/下线)
    usb.make_mon_object(FLAGS_registry_host
        , FLAGS_file_service_name);
    
    // 3.5 初始化 Redis 客户端(会话、验证码、登录状态存储)
    usb.make_redis_object(FLAGS_redis_host
        , FLAGS_redis_port
        , FLAGS_redis_db
        , std::chrono::milliseconds(FLAGS_redis_timeout)
        , FLAGS_redis_keepalive);
    
    // 3.6 初始化 RPC 服务(绑定端口、注册服务实现)
    usb.make_rpc_object(FLAGS_rpc_listen_port
        , FLAGS_rpc_timeout
        , FLAGS_rpc_threads);
    
    // 3.7 注册服务到 etcd(供其他服务发现)
    usb.make_regs_object(FLAGS_registry_host
        , FLAGS_base_service+FLAGS_instance_name
        , FLAGS_access_host);
    
    // 4. 构建并启动服务
    std::shared_ptr<bite_im::UserServer> server = usb.build();
    server->start();  // 阻塞运行,直到收到退出信号

    return 0;
}

2. Builder 模式核心优势

  • 流程可控:初始化步骤按依赖顺序排列(如先初始化服务管理器,再初始化 etcd 监控),避免依赖错误;
  • 扩展性强 :新增依赖组件(如消息队列、第三方存储)时,仅需在 Builder 中新增 make_xxx_object 方法,不影响现有逻辑;
  • 代码简洁 :将复杂的初始化逻辑封装在 Builder 类中,main 函数仅需调用链式方法,可读性强;
  • 组件复用:Builder 类统一管理所有依赖组件的实例化,确保组件单例复用,避免资源浪费。

(四)核心组件初始化细节

1. 日志模块初始化

基于 spdlog 封装的日志模块,支持调试 / 发布两种模式,可灵活配置输出方式(控制台 / 文件)和日志等级:

cpp 复制代码
LogModule::Log::Init(
    FLAGS_log_mode,        // 模式:调试/发布
    FLAGS_log_file,        // 日志文件名(发布模式)
    FLAGS_log_level,       // 日志等级(发布模式)
    FLAGS_log_output_mode  // 输出方式:控制台/文件
);
  • 调试模式:输出到控制台,日志等级默认 DEBUG,便于开发调试;
  • 发布模式:输出到文件,支持按大小 / 时间轮转(需 spdlog 配置),日志等级可配置为 INFO/WARN/ERROR,减少日志量。

2. 依赖组件初始化(MySQL/Redis/ES)

  • MySQL :通过 ODBFactory 创建数据库连接池,配置字符集、最大连接数等参数,确保数据库操作的高效性与稳定性;
  • Redis:基于 redis++ 构建客户端,启用长连接减少连接开销,设置超时时间避免阻塞;
  • ES:初始化 elasticlient 客户端,指定 ES 服务地址和超时时间,支撑用户搜索索引的读写操作。

3. 服务注册与发现(etcd)

  • 服务注册 :通过 RegisterEtcd 将当前服务的访问地址(access_host)注册到 etcd 的指定路径(base_service+instance_name),供其他服务(如消息服务)发现;
  • 服务发现 :通过 MonitorEtcd 监听文件子服务的注册路径(file_service_name),当文件服务上线 / 下线时,自动更新服务管理器中的可用节点列表,确保跨服务调用的可用性。

4. RPC 服务初始化

基于 brpc 框架构建 RPC 服务,核心配置包括监听端口、IO 线程数、超时时间:

cpp 复制代码
usb.make_rpc_object(
    FLAGS_rpc_listen_port,  // 监听端口:10003
    FLAGS_rpc_timeout,      // 超时时间:-1(无超时)
    FLAGS_rpc_threads       // IO 线程数:1(可根据服务器配置调整)
);
  • 服务注册:将 UserServiceImpl 实现类绑定到 RPC 服务,对外提供用户相关的 RPC 接口;
  • 线程模型:brpc 采用 Reactor 模式,IO 线程负责处理网络事件,业务逻辑在 IO 线程中执行(若业务复杂,可配置独立的业务线程池)。

(五)服务启动与运行

1. 服务启动

cpp 复制代码
std::shared_ptr<bite_im::UserServer> server = usb.build();
server->start();  // 阻塞调用,运行直到收到退出信号(如 SIGINT/SIGTERM)
  • server->start() 内部调用 brpc::Server::RunUntilAskedToQuit(),阻塞当前线程,持续监听 RPC 请求;
  • 支持优雅退出:当收到退出信号时,brpc 会自动关闭监听端口,释放资源,确保数据不丢失。

2. 运行时特性

  • 高并发支持:brpc 框架具备优秀的并发处理能力,IO 线程数可通过配置调整,适配不同的并发请求量;
  • 负载均衡:etcd 支持多实例注册,当用户服务部署多个节点时,其他服务通过 etcd 获取所有可用节点,实现负载均衡;
  • 故障自动转移:当某个用户服务节点故障下线时,etcd 会自动删除其注册信息,其他服务不再向该节点发送请求,实现故障自动转移。

客户端测试用例设计与实现

为确保用户子服务的所有核心功能稳定可靠,我们基于 Google Test(gtest)框架设计了一套完整的客户端测试用例,覆盖注册、登录、信息修改、信息查询等全业务流程。测试用例通过 RPC 调用真实服务端接口,模拟真实用户操作场景,验证接口功能正确性、参数校验有效性及跨服务交互可用性。

(一)测试框架与核心依赖

1. 技术选型

组件 技术工具 / 框架 核心作用
测试框架 Google Test(gtest) 提供测试用例组织、断言机制、测试执行等核心能力
RPC 通信 brpc 与服务端一致的 RPC 框架,确保测试调用与真实客户端行为一致
服务发现 etcd + brpcChannelTool 从 etcd 动态获取用户子服务地址,支持多实例部署场景下的测试
日志模块 spdlog + brpc 日志 输出测试过程日志,便于问题定位(如接口调用失败原因)
配置管理 gflags 统一管理测试环境配置(etcd 地址、服务注册路径等),适配不同测试环境

2. 核心配置定义

测试客户端通过 gflags 定义配置项,与服务端配置对齐,确保测试环境可灵活调整:

cpp 复制代码
// 日志配置(与服务端一致,便于日志统一分析)
DEFINE_bool(log_mode, false, "日志模式: false(调试模式,默认), true(发布模式)");
DEFINE_bool(log_output_mode, false, "控制台模式: false, 文件模式:true");
DEFINE_string(log_file, "test_app.log", "发布模式下日志名称");
DEFINE_int32(log_level, 0, "发布模式下日志等级设置");

// etcd 服务发现配置(需与服务端注册配置一致)
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(user_host, "/service/user_service", "用户服务在etcd的注册路径");
DEFINE_string(file_host, "/service/file_service", "文件服务在etcd的注册路径(跨服务测试依赖)");

(二)测试环境初始化流程

测试客户端启动时需完成日志初始化、服务发现初始化等准备工作,确保测试用例可正常调用服务端接口:

cpp 复制代码
int main(int argc, char* argv[]) 
{
    // 1. 初始化 gtest 框架
    testing::InitGoogleTest(&argc, argv);
    // 2. 解析命令行配置
    gflags::ParseCommandLineFlags(&argc, &argv, true);

    // 3. 配置日志(关闭 brpc 自带日志,使用自定义日志模块)
    logging::SetMinLogLevel(4);
    LogModule::Log::Init(FLAGS_log_mode, FLAGS_log_file, FLAGS_log_level, FLAGS_log_output_mode);

    // 4. 初始化 RPC 服务管理器(用于从 etcd 发现用户服务)
    manager = std::make_shared<brpcChannelTool::ServiceManager>();
    manager->set_is_follow(false);  // 关闭关注模式,适配所有服务节点

    // 5. 初始化 etcd 监视器(监听用户服务上线/下线)
    Etcd_Tool::monitor_t info;
    info.monitor_path_ = FLAGS_user_host;  // 监听用户服务注册路径
    // 服务上线回调:将服务节点添加到管理器
    info.put_on_ = std::bind(&brpcChannelTool::ServiceManager::Online, manager.get(), std::placeholders::_1, std::placeholders::_2);
    // 服务下线回调:将服务节点从管理器移除
    info.put_off_ = std::bind(&brpcChannelTool::ServiceManager::Offline, manager.get(), std::placeholders::_1, std::placeholders::_2);
    info.type_ = Etcd_Tool::DIR;  // 监听目录类型(多实例部署)
    m.PushMonitor(info);

    // 6. 等待服务发现初始化完成(确保测试用例执行前已找到可用服务)
    if (!m.WaitForInitialDiscovery()) 
    {
        LOG_ERROR("服务发现初始化失败,未找到用户子服务节点");
        return -1;
    }

    // 7. 执行所有测试用例
    return RUN_ALL_TESTS();
}

(三)核心测试用例设计

测试用例按业务模块分类,覆盖用户子服务所有核心接口,每个用例遵循「准备请求参数→调用 RPC 接口→断言响应结果」的标准化流程。

1. 注册模块测试(用户名 + 手机号)

(1)用户名注册测试

验证用户名、密码合法性校验及注册流程正确性:

cpp 复制代码
TEST(用户注册, 用户名注册测试)
{
    // 1. 从服务管理器获取用户服务 RPC 信道
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";

    // 2. 初始化 RPC 客户端存根
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 3. 构造请求参数(合法昵称+密码)
    bite_im::UserRegisterReq req;
    req.set_nickname("李四");  // 符合 3~15 位规则
    req.set_password("123456");  // 符合 6~15 位数字规则
    req.set_request_id(bite_im::uuid());  // 生成唯一请求 ID
    bite_im::UserRegisterRsp rsp;

    // 4. 调用 RPC 接口
    user_stub.UserRegister(&ctl, &req, &rsp, nullptr);

    // 5. 断言结果(接口调用成功+响应参数一致)
    ASSERT_EQ(ctl.Failed(), false) << "RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "注册失败:" << rsp.errmsg();
    ASSERT_EQ(rsp.request_id(), req.request_id()) << "请求 ID 响应不一致";
}
(2)手机号注册测试

验证手机号格式校验、验证码校验及注册流程正确性(依赖「获取短信验证码」接口):

cpp 复制代码
TEST(用户注册, 手机号注册测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl1, ctl2;

    // 第一步:获取短信验证码
    bite_im::PhoneVerifyCodeReq code_req;
    code_req.set_phone_number("13544448888");  // 合法手机号
    code_req.set_request_id(bite_im::uuid());
    bite_im::PhoneVerifyCodeRsp code_rsp;
    user_stub.GetPhoneVerifyCode(&ctl1, &code_req, &code_rsp, nullptr);
    ASSERT_EQ(ctl1.Failed(), false) << "获取验证码 RPC 调用失败:" << ctl1.ErrorText();
    ASSERT_EQ(code_rsp.success(), true) << "获取验证码失败:" << code_rsp.errmsg();

    // 第二步:使用验证码完成手机号注册
    bite_im::PhoneRegisterReq req;
    req.set_phone_number("13544448888");
    req.set_request_id(bite_im::uuid());
    req.set_verify_code_id(code_rsp.verify_code_id());  // 验证码 ID(从第一步响应获取)
    req.set_verify_code("7878");  // 测试用固定验证码(实际应从短信获取)
    bite_im::PhoneRegisterRsp rsp;
    user_stub.PhoneRegister(&ctl2, &req, &rsp, nullptr);

    // 断言结果
    ASSERT_EQ(ctl2.Failed(), false) << "手机号注册 RPC 调用失败:" << ctl2.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "手机号注册失败:" << rsp.errmsg();
    ASSERT_EQ(rsp.request_id(), req.request_id()) << "请求 ID 响应不一致";
}

2. 登录模块测试(用户名 + 手机号)

(1)用户名登录测试

验证用户名密码校验、登录态创建及会话 ID 返回正确性:

cpp 复制代码
TEST(用户登录, 用户名登录测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 构造登录请求(使用已注册的用户名+密码)
    bite_im::UserLoginReq req;
    req.set_nickname("李四");
    req.set_password("123456");
    req.set_request_id(bite_im::uuid());
    bite_im::UserLoginRsp rsp;

    // 调用登录接口
    user_stub.UserLogin(&ctl, &req, &rsp, nullptr);

    // 断言结果(需返回会话 ID)
    ASSERT_EQ(ctl.Failed(), false) << "登录 RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "登录失败:" << rsp.errmsg();
    ASSERT_EQ(rsp.request_id(), req.request_id()) << "请求 ID 响应不一致";
    ASSERT_FALSE(rsp.login_session_id().empty()) << "未返回会话 ID";
}
(2)手机号登录测试

验证手机号、验证码校验及登录态创建正确性:

cpp 复制代码
TEST(用户登录, 手机号登录测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl, ctl2;

    // 第一步:获取短信验证码
    bite_im::PhoneVerifyCodeReq code_req;
    code_req.set_phone_number("13544448888");
    code_req.set_request_id(bite_im::uuid());
    bite_im::PhoneVerifyCodeRsp code_rsp;
    user_stub.GetPhoneVerifyCode(&ctl, &code_req, &code_rsp, nullptr);
    ASSERT_EQ(ctl.Failed(), false) << "获取验证码失败:" << ctl.ErrorText();

    // 第二步:使用验证码登录
    bite_im::PhoneLoginReq req;
    req.set_phone_number("13544448888");
    req.set_request_id(bite_im::uuid());
    req.set_verify_code_id(code_rsp.verify_code_id());
    req.set_verify_code("7878");
    bite_im::PhoneLoginRsp rsp;
    user_stub.PhoneLogin(&ctl2, &req, &rsp, nullptr);

    // 断言结果
    ASSERT_EQ(ctl2.Failed(), false) << "手机号登录失败:" << ctl2.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "登录失败:" << rsp.errmsg();
    ASSERT_FALSE(rsp.login_session_id().empty()) << "未返回会话 ID";
}

3. 用户信息修改模块测试

覆盖昵称、签名、手机号、头像四类信息修改场景,验证数据更新正确性及跨服务交互(如头像上传依赖文件服务)。

(1)昵称修改测试
cpp 复制代码
TEST(用户数据修改, 用户名修改测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 构造修改请求(需传入已存在的用户 ID)
    bite_im::SetUserNicknameReq req;
    req.set_nickname("李四改王五");  // 新昵称(符合格式规则)
    req.set_user_id("13229c-dc19ee-0001");  // 已注册用户 ID
    req.set_request_id(bite_im::uuid());
    bite_im::SetUserNicknameRsp rsp;

    // 调用修改接口
    user_stub.SetUserNickname(&ctl, &req, &rsp, nullptr);

    // 断言结果
    ASSERT_EQ(ctl.Failed(), false) << "昵称修改 RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "昵称修改失败:" << rsp.errmsg();
}
(2)头像上传测试(跨服务依赖)

验证头像文件上传至文件服务、数据库更新头像 ID 全流程:

cpp 复制代码
TEST(用户数据修改, 头像数据上传测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 第一步:读取本地测试图片文件(二进制数据)
    bite_im::SetUserAvatarReq req;
    std::string body;
    bite_im::ReadFile("./testicon.jpg", body);  // 自定义工具函数:读取文件内容
    req.set_avatar(body);  // 传入头像二进制数据
    req.set_user_id("13229c-dc19ee-0001");  // 已注册用户 ID
    req.set_request_id(bite_im::uuid());
    bite_im::SetUserAvatarRsp rsp;

    // 第二步:调用头像设置接口(内部会调用文件服务上传文件)
    user_stub.SetUserAvatar(&ctl, &req, &rsp, nullptr);

    // 断言结果
    ASSERT_EQ(ctl.Failed(), false) << "头像上传 RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "头像上传失败:" << rsp.errmsg();
}

4. 用户信息查询模块测试

覆盖单用户、批量用户查询场景,验证信息完整性(含头像数据)。

(1)单用户信息查询测试
cpp 复制代码
TEST(用户数据获取, 单个用户数据获取测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 构造查询请求(传入已注册用户 ID)
    bite_im::GetUserInfoReq req;
    req.set_user_id("13229c-dc19ee-0001");
    req.set_request_id(bite_im::uuid());
    bite_im::GetUserInfoRsp rsp;

    // 调用查询接口
    user_stub.GetUserInfo(&ctl, &req, &rsp, nullptr);

    // 断言结果(响应成功+用户信息完整)
    ASSERT_EQ(ctl.Failed(), false) << "单用户查询 RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "单用户查询失败:" << rsp.errmsg();
    
    // 打印用户信息(辅助验证)
    bite_im::UserInfo info = rsp.user_info();
    LOG_INFO("用户昵称:{}", info.nickname());
    LOG_INFO("用户签名:{}", info.description());
    LOG_INFO("用户手机号:{}", info.phone());
    ASSERT_FALSE(info.avatar().empty()) << "未获取到头像数据";
}
(2)批量用户信息查询测试

验证批量查询效率及多用户信息完整性:

cpp 复制代码
TEST(用户数据获取, 批量用户数据获取测试)
{
    auto ptr = manager->Choose(FLAGS_user_host);
    ASSERT_TRUE(ptr != nullptr) << "未找到可用的用户服务节点";
    bite_im::UserService_Stub user_stub(ptr.get());
    brpc::Controller ctl;

    // 构造批量查询请求(传入多个已注册用户 ID)
    bite_im::GetMultiUserInfoReq req;
    req.add_users_id("44955d-b2cede-0000");
    req.add_users_id("13229c-dc19ee-0001");
    req.set_request_id(bite_im::uuid());
    bite_im::GetMultiUserInfoRsp rsp;

    // 调用批量查询接口
    user_stub.GetMultiUserInfo(&ctl, &req, &rsp, nullptr);

    // 断言结果
    ASSERT_EQ(ctl.Failed(), false) << "批量用户查询 RPC 调用失败:" << ctl.ErrorText();
    ASSERT_EQ(rsp.success(), true) << "批量用户查询失败:" << rsp.errmsg();
    
    // 验证所有请求的用户都返回了信息
    auto infomap = rsp.users_info();
    ASSERT_EQ(infomap.size(), 2) << "返回的用户数量与请求不一致";
    
    // 打印批量查询结果
    for(auto& it : infomap)
    {
        bite_im::UserInfo info = it.second;
        LOG_INFO("用户 ID:{},昵称:{},手机号:{}", info.user_id(), info.nickname(), info.phone());
    }
}

(四)测试用例设计亮点

  1. 全流程覆盖:覆盖用户从注册→登录→信息修改→信息查询的完整生命周期,确保端到端功能可用;
  2. 跨服务兼容:头像上传测试依赖文件服务,验证了微服务间协同的正确性;
  3. 参数合法性校验:测试用例中传入合法参数,间接验证服务端参数校验逻辑(如需测试非法参数,可新增用例传入超长昵称、非法手机号等);
  4. 可重复性:测试用例使用固定测试用户 ID 和验证码(测试环境),确保每次执行结果一致;
  5. 日志可追溯:每个用例输出详细日志,调用失败时可快速定位原因(如服务未启动、参数错误、跨服务调用失败等)。

(五)测试执行与结果验证

  1. 执行前提:启动 etcd、MySQL、Redis、ES、文件服务及用户子服务,确保所有依赖组件正常运行;
  2. 执行命令:编译测试代码后,直接运行测试程序,gtest 会自动执行所有未注释的测试用例;
  3. 结果判断 :所有用例执行通过(显示 OK)则说明服务端功能正常;若有失败用例,可通过日志查看具体失败原因(如 RPC 调用失败、响应结果不符合预期等)。
相关推荐
沐浴露z2 小时前
【微服务】基本概念介绍
java·微服务
草莓火锅3 小时前
用c++使输入的数字各个位上数字反转得到一个新数
开发语言·c++·算法
j_xxx404_3 小时前
C++ STL:阅读list源码|list类模拟|优化构造|优化const迭代器|优化迭代器模板|附源码
开发语言·c++
散峰而望3 小时前
C/C++输入输出初级(一) (算法竞赛)
c语言·开发语言·c++·算法·github
Red丶哞3 小时前
Docker 安装部署Prometheus
linux·云原生·容器·kubernetes
Jooou4 小时前
Spring事务实现原理深度解析:从源码到架构全面剖析
java·spring·架构·事务
紫神4 小时前
kubeedge安装并接入摄像头操作说明
云原生·kubernetes·edge
曾几何时`4 小时前
C++——this指针
开发语言·c++
記億揺晃着的那天4 小时前
六大 API 架构风格
架构·软件工程·graphql·rest api