用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此在上述项目功能中用户子服务需要提供以下接口:
-
用户注册:用户输入用户名(昵称),以及密码进行用户名的注册
-
用户登录:用户通过用户名和密码进行登录
-
短信验证码获取:当用户通过手机号注册或登录的时候,需要获取短信验证码
-
手机号注册:用户输入手机号和短信验证码进行手机号的用户注册
-
手机号登录:用户输入手机号和短信验证码进行手机号的用户登录
-
用户信息获取:当用户登录之后,获取个人信息进行展示
-
头像修改:设置用户头像
-
昵称修改:设置用户昵称
-
签名修改:设置用户签名
-
手机号修改:修改用户的绑定手机号
模块划分
-
参数/配置文件解析模块:基于
gflags框架直接使用进行参数/配置文件解析。 -
日志模块:基于
spdlog框架封装的模块直接使用进行日志输出。 -
服务注册模块:基于
etcd框架封装的注册模块直接使用进行语音识别子服务的服务注册。 -
rpc服务模块:基于brpc框架搭建rpc服务器。 -
基于
etcd框架与brpc框架封装的服务发现与调用模块:-
连接文件管理子服务:获取用户信息的时候,用户头像是通过文件的形式存储在文件子服务中的。
-
连接消息管理子服务:在打开聊天会话的时候,需要获取最近的一条消息进行展示。
-
-
数据库数据操作模块:基于
odb-mysql数据管理封装的模块,实现关系型数据库中数据的操作。- 用户进行用户名/手机号注册的时候在数据库中进行新增信息。
- 用户修改个人信息的时候修改数据库中的记录。
- 用户登录的时候,在数据库中进行用户名密码的验证。
-
redis客户端模块:基于redis++封装的客户端进行内存数据库数据操作。- 当用户登录的时候需要为用户创建登录会话,会话信息保存在
redis服务器中。 - 当用户手机号进行获取/验证验证码的时候,验证码与对应信息保存在
redis服务器中。
- 当用户登录的时候需要为用户创建登录会话,会话信息保存在
-
ES客户端模块:基于elasticsearch框架实现访问客户端,向ES服务器中存储用户简息,以便于用户的搜索。 -
短信平台客户端模块:基于短信平台
SDK封装使用,用于向用户手机号发送指定验证码。
各类数据管理接口实现
一、关系数据库管理
① 用户数据表 user.hxx
- 包含字段:
- 主键
ID:自动生成 - 用户
ID:用户唯一标识 - 用户昵称:用户的昵称,也可用作登录用户名
- 用户签名:用户对自己的描述
- 登录密码:登录验证
- 绑定手机号:用户可以绑定手机号,绑定后可以通过手机号登录
- 用户头像文件
ID:头像文件存储的唯一标识,具体头像数据存储在文件子服务器中。
- 主键
- 提供的操作:
- 通过昵称获取用户信息
- 通过手机号获取用户信息
- 通过用户 ID 获取用信息
- 新增用户
- 更新用户信息
#pragma once
#include <string>
#include <cstddef>
#include <odb/nullable.hxx>
#include <odb/core.hxx>
namespace liren
{
#pragma db object table("user")
class User
{
public:
User(){}
// 用户名--新增用户:用户ID, 昵称,密码
User(const std::string &uid, const std::string &nickname, const std::string &password)
: _user_id(uid), _nickname(nickname), _password(password)
{}
// 手机号--新增用户:用户ID, 手机号, 随机昵称
User(const std::string &uid, const std::string &phone)
: _user_id(uid), _nickname(uid), _phone(phone)
{}
// 下面是一系列设置以及获取字段的接口
void user_id(const std::string &val) { _user_id = val; }
std::string user_id() { return _user_id; }
std::string nickname() {
if (_nickname) return *_nickname;
return std::string();
}
void nickname(const std::string &val) { _nickname = val; }
std::string description() {
if (!_description) return std::string();
return *_description;
}
void description(const std::string &val) { _description = val; }
std::string password() {
if (!_password) return std::string();
return *_password;
}
void password(const std::string &val) { _password = val; }
std::string phone() {
if (!_phone) return std::string();
return *_phone;
}
void phone(const std::string &val) { _phone = val; }
std::string avatar_id() {
if (!_avatar_id) return std::string();
return *_avatar_id;
}
void avatar_id(const std::string &val) { _avatar_id = val; }
private:
friend class odb::access;
#pragma db id auto
unsigned long _id;
#pragma db type("varchar(64)") index unique
std::string _user_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 - 不一定存在
};
}② 统一创建 mysql 对象的工厂类 mysql.hpp
因为后面还有其他模块也会创建 mysql 对象,故这里统一封装一个工厂类,减少代码冗余!
#pragma once
#include <string>
#include <memory>
#include <cstdlib>
#include <iostream>
#include <odb/database.hxx> // ODB ORM 框架中数据库抽象层定义
#include <odb/mysql/database.hxx> // ODB MySQL 数据库实现的定义
#include "logger.hpp" // 日志模块封装,用于输出调试和错误日志
namespace liren
{
// ODBFactory 类封装了 ODB 数据库对象的创建逻辑
// 通过该工厂方法可以创建 MySQL 数据库连接,并支持连接池功能
class ODBFactory
{
public:
// 静态工厂方法 create 用于创建并返回一个数据库连接对象
// 参数说明:
// - user: 数据库用户名
// - pswd: 数据库密码
// - host: 数据库主机地址
// - db: 数据库名称
// - cset: 数据库字符集(如 "utf8")
// - port: 数据库端口号
// - conn_pool_count: 连接池中连接的数量
static std::shared_ptr<odb::core::database> create(const std::string &user,
const std::string &pswd,
const std::string &host,
const std::string &db,
const std::string &cset,
int port,
int conn_pool_count)
{
// 创建一个 odb::mysql::connection_pool_factory 对象,用于管理数据库连接池
// 使用 std::unique_ptr 管理连接池工厂的生命周期,并传入连接池连接数量参数
std::unique_ptr<odb::mysql::connection_pool_factory> cpf(
new odb::mysql::connection_pool_factory(conn_pool_count, 0));
// 创建 MySQL 数据库对象,并传入相关参数:
// user, pswd, db, host, port: 数据库连接基本信息
// 空字符串 "" 表示默认的 socket 或其它参数(具体视 ODB 实现而定)
// cset: 数据库字符集
// 0: 可能表示客户端标志或其它配置信息(依据 ODB 接口文档)
// std::move(cpf): 转移连接池工厂所有权,以便数据库对象内部使用连接池
auto res = std::make_shared<odb::mysql::database>(user, pswd,
db, host, port, "", cset, 0, std::move(cpf));
return res;
}
};
}③ 用户表的数据库操作 mysql_user.hpp
#include "mysql.hpp" // MySQL 数据库相关头文件(封装了 MySQL 连接和操作)
#include "user.hxx" // 用户实体类声明
#include "user-odb.hxx" // 用户实体类的 ODB 映射声明
// 业务功能包括:
// 用户注册、用户登录、验证码获取、手机号注册、手机号登录、获取用户信息、用户信息修改等
// 同时支持通过昵称、手机号、用户ID以及多个用户ID获取用户信息
namespace liren
{
// UserTable 类封装了对用户表的数据库操作,提供插入、更新以及多种查询接口
class UserTable
{
public:
using ptr = std::shared_ptr<UserTable>;
// 构造函数:通过传入的数据库对象初始化 UserTable
UserTable(const std::shared_ptr<odb::core::database> &db)
: _db(db)
{}
// 插入用户记录
// 参数:user - 需要插入的用户对象(以智能指针形式传入)
// 返回值:成功返回 true,失败返回 false
bool insert(const std::shared_ptr<User> &user)
{
try {
odb::transaction trans(_db->begin()); // 开启一个数据库事务,确保操作的原子性
_db->persist(*user); // 持久化用户对象,将其写入数据库
trans.commit(); // 提交事务,保存数据
} catch (std::exception &e) {
LOG_ERROR("新增用户失败 {}:{}!", user->nickname(), e.what());
return false;
}
return true;
}
// 更新用户记录
// 参数:user - 需要更新的用户对象
// 返回值:成功返回 true,失败返回 false
bool update(const std::shared_ptr<User> &user)
{
try {
odb::transaction trans(_db->begin());
_db->update(*user); // 更新用户对象数据
trans.commit();
} catch (std::exception &e) {
LOG_ERROR("更新用户失败 {}:{}!", user->nickname(), e.what());
return false;
}
return true;
}
// 根据昵称查询用户记录
// 参数:nickname - 用户的昵称
// 返回值:若查询成功,返回查询到的用户对象;否则返回空指针
std::shared_ptr<User> select_by_nickname(const std::string &nickname)
{
std::shared_ptr<User> res;
try {
odb::transaction trans(_db->begin());
// 定义查询类型别名,便于构造查询条件
typedef odb::query<User> query;
typedef odb::result<User> result;
// 利用 ODB 提供的 query_one 方法查询符合条件的用户记录
res.reset(_db->query_one<User>(query::nickname == nickname));
trans.commit();
} catch (std::exception &e) {
LOG_ERROR("通过昵称查询用户失败 {}:{}!", nickname, e.what());
}
return res;
}
// 根据手机号查询用户记录
// 参数:phone - 用户的手机号
// 返回值:查询到的用户对象指针,如果查询失败则返回空指针
std::shared_ptr<User> select_by_phone(const std::string &phone)
{
std::shared_ptr<User> res;
try {
odb::transaction trans(_db->begin());
typedef odb::query<User> query;
typedef odb::result<User> result;
res.reset(_db->query_one<User>(query::phone == phone));
trans.commit();
} catch (std::exception &e) {
LOG_ERROR("通过手机号查询用户失败 {}:{}!", phone, e.what());
}
return res;
}
// 根据用户ID查询用户记录
// 参数:user_id - 用户的唯一标识ID
// 返回值:若查询成功,返回对应的用户对象;否则返回空指针
std::shared_ptr<User> select_by_id(const std::string &user_id)
{
std::shared_ptr<User> res;
try {
odb::transaction trans(_db->begin());
typedef odb::query<User> query;
typedef odb::result<User> result;
res.reset(_db->query_one<User>(query::user_id == user_id));
trans.commit();
} catch (std::exception &e) {
LOG_ERROR("通过用户ID查询用户失败 {}:{}!", user_id, e.what());
}
return res;
}
// 根据多个用户ID查询用户记录
// 参数:id_list - 用户ID列表,返回所有匹配的用户记录
// 返回值:用户对象的 vector,如果列表为空或查询失败,返回空 vector
std::vector<User> select_multi_users(const std::vector<std::string> &id_list)
{
// 如果传入的 id_list 为空,直接返回空 vector
if (id_list.empty()) {
return std::vector<User>();
}
std::vector<User> res;
try {
odb::transaction trans(_db->begin());
typedef odb::query<User> query;
typedef odb::result<User> result;
// 构造 SQL 中 IN 条件的字符串:例如 "user_id in ('id1','id2',...)"
std::stringstream ss;
ss << "user_id in (";
for (const auto &id : id_list) {
ss << "'" << id << "',";
}
// 移除最后多余的逗号,并添加右括号
std::string condition = ss.str();
condition.pop_back();
condition += ")";
// 利用自定义条件进行查询
result r(_db->query<User>(condition));
// 遍历查询结果,存入返回 vector 中
for (result::iterator i(r.begin()); i != r.end(); ++i) {
res.push_back(*i);
}
trans.commit();
} catch (std::exception &e) {
LOG_ERROR("通过用户ID批量查询用户失败:{}!", e.what());
}
return res;
}
private:
// 数据库对象指针,所有数据库操作均通过此对象执行
std::shared_ptr<odb::core::database> _db;
};
} 测试代码
先使用以下命令生成 odb 相关文件:(根据所在目录灵活调整)
odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time ../../../odb/user.hxx 然后记得先在数据库中创建一个库,这里统一创建 liren 数据库!
然后就写测试代码 main.cc 文件:
#include "../../../header/mysql_user.hpp"
#include "../../../odb/user.hxx"
#include "user-odb.hxx"
#include <gflags/gflags.h>
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
void insert(liren::UserTable &user) {
auto user1 = std::make_shared<liren::User>("uid1", "昵称1", "123456");
user.insert(user1);
auto user2 = std::make_shared<liren::User>("uid2", "15566667777");
user.insert(user2);
}
void update_by_id(liren::UserTable &user_tb) {
auto user = user_tb.select_by_id("uid1");
user->description("我是一个风一样的男子!!");
user_tb.update(user);
}
void update_by_phone(liren::UserTable &user_tb) {
auto user = user_tb.select_by_phone("15566667777");
user->password("22223333");
user_tb.update(user);
}
void update_by_nickname(liren::UserTable &user_tb) {
auto user = user_tb.select_by_nickname("uid2");
user->nickname("昵称2");
user_tb.update(user);
}
void select_users(liren::UserTable &user_tb) {
std::vector<std::string> id_list = {"uid1", "uid2"};
auto res = user_tb.select_multi_users(id_list);
for (auto user : res) {
std::cout << user.nickname() << std::endl;
}
}
int main(int argc, char *argv[])
{
google::ParseCommandLineFlags(&argc, &argv, true);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
auto db = liren::ODBFactory::create("root", "TThh1314520!", "127.0.0.1", "liren", "utf8", 0, 1);
liren::UserTable user(db);
// insert(user);
// update_by_id(user);
// update_by_phone(user);
// update_by_nickname(user);
select_users(user);
return 0;
} 然后就是 makefile 文件:
main : main.cc user-odb.cxx
g++ -std=c++17 $^ -o $@ -I/usr/include/mysql -I../../../odb/ -I./ -lodb-mysql -lodb -lodb-boost -lfmt -lspdlog -lgflags 正常操作后会有以下效果:
二、内存数据库管理
redis 创建与管理操作 data_redis.hpp
#include <sw/redis++/redis.h> // 引入 Redis++ 库,用于与 Redis 数据库交互
#include <iostream>
namespace liren
{
// RedisClientFactory 类用于创建 Redis 客户端对象
// 通过封装创建过程,简化外部调用时对 Redis 客户端的初始化
class RedisClientFactory
{
public:
// 静态工厂方法,用于根据传入参数创建 Redis 客户端
// 参数:
// - host: Redis 服务器地址
// - port: Redis 服务器端口
// - db: 使用的数据库编号
// - keep_alive: 是否启用长连接保持
// 返回值:
// - std::shared_ptr<sw::redis::Redis>:返回创建的 Redis 客户端智能指针
static std::shared_ptr<sw::redis::Redis> create(const std::string &host,
int port,
int db,
bool keep_alive)
{
// 配置连接选项
sw::redis::ConnectionOptions opts;
opts.host = host; // 设置 Redis 服务器地址
opts.port = port; // 设置 Redis 服务器端口
opts.db = db; // 指定使用的数据库编号
opts.keep_alive = keep_alive; // 设置是否启用长连接保持
// 创建 Redis 客户端对象,并封装到智能指针中返回
auto res = std::make_shared<sw::redis::Redis>(opts);
return res;
}
};
// 封装基于 Redis 的会话管理操作
// 用于保存和管理用户会话信息,支持添加、删除以及获取会话对应的用户ID
class Session
{
public:
using ptr = std::shared_ptr<Session>;
// 构造函数:传入 Redis 客户端对象,用于后续操作 Redis 数据库
Session(const std::shared_ptr<sw::redis::Redis> &redis_client)
: _redis_client(redis_client)
{}
// append 方法:为给定的会话ID(ssid)存储对应的用户ID(uid)
void append(const std::string &ssid, const std::string &uid) {
_redis_client->set(ssid, uid);
}
// remove 方法:删除指定会话ID(ssid)对应的 Redis 键值对
void remove(const std::string &ssid) {
_redis_client->del(ssid);
}
// uid 方法:根据会话ID(ssid)获取存储的用户ID
// 返回类型为 sw::redis::OptionalString,可以判断是否存在该键
sw::redis::OptionalString uid(const std::string &ssid) {
return _redis_client->get(ssid);
}
private:
std::shared_ptr<sw::redis::Redis> _redis_client; // 保存 Redis 客户端对象,所有 Redis 操作均通过此对象进行
};
// 封装用户在线状态的管理操作
// 通过 Redis 存储用户在线状态,支持添加、删除和检查状态是否存在
class Status
{
public:
using ptr = std::shared_ptr<Status>;
// 构造函数,传入 Redis 客户端对象
Status(const std::shared_ptr<sw::redis::Redis> &redis_client)
: _redis_client(redis_client)
{}
// append 方法:设置指定用户ID(uid)的在线状态
// 这里使用空字符串作为在线状态的标记
void append(const std::string &uid) {
_redis_client->set(uid, "");
}
// remove 方法:删除指定用户ID(uid)的在线状态记录
void remove(const std::string &uid) {
_redis_client->del(uid);
}
// exists 方法:检查指定用户ID(uid)是否存在在线状态记录
// 通过获取键值判断是否存在
bool exists(const std::string &uid) {
auto res = _redis_client->get(uid);
if (res) return true;
return false;
}
private:
std::shared_ptr<sw::redis::Redis> _redis_client; // Redis 客户端对象,用于执行状态相关的 Redis 操作
};
// 封装验证码操作
// 支持添加、删除验证码,并设置验证码的有效期(超时自动失效)
class Codes
{
public:
using ptr = std::shared_ptr<Codes>;
// 构造函数:传入 Redis 客户端对象
Codes(const std::shared_ptr<sw::redis::Redis> &redis_client)
: _redis_client(redis_client)
{}
// append 方法:添加验证码
// 参数:
// - cid: 验证码对应的键(通常用于标识)
// - code: 验证码内容
// - t: 有效期,默认为 300000 毫秒(5 分钟)
void append(const std::string &cid, const std::string &code,
const std::chrono::milliseconds &t = std::chrono::milliseconds(300000)) {
// 设置键值对,并带有超时时间 t
_redis_client->set(cid, code, t);
}
// remove 方法:删除指定验证码记录
void remove(const std::string &cid) {
_redis_client->del(cid);
}
// code 方法:根据验证码键(cid)获取验证码内容
// 返回类型为 sw::redis::OptionalString,用于判断是否成功获取验证码
sw::redis::OptionalString code(const std::string &cid) {
return _redis_client->get(cid);
}
private:
std::shared_ptr<sw::redis::Redis> _redis_client; // 保存 Redis 客户端对象,供验证码相关操作使用
};
} 测试代码
main.cc 文件:
#include "../../../header/data_redis.hpp" // 包含 Redis 操作封装头文件(定义了 Session、Status、Codes、RedisClientFactory 等类)
#include <gflags/gflags.h>
#include <thread>
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(ip, "127.0.0.1", "这是服务器的IP地址,格式:127.0.0.1");
DEFINE_int32(port, 6379, "这是服务器的端口, 格式: 8080");
DEFINE_int32(db, 0, "库的编号:默认0号");
DEFINE_bool(keep_alive, true, "是否进行长连接保活");
// session_test 函数用于测试 Session 类的功能:添加会话、删除会话和获取会话对应的用户ID
void session_test(const std::shared_ptr<sw::redis::Redis> &client)
{
// 创建 Session 对象,传入 Redis 客户端
liren::Session ss(client);
// 添加多个会话记录,键为会话ID,值为对应的用户ID
ss.append("会话ID1", "用户ID1");
ss.append("会话ID2", "用户ID2");
ss.append("会话ID3", "用户ID3");
ss.append("会话ID4", "用户ID4");
// 删除部分会话记录
ss.remove("会话ID2");
ss.remove("会话ID3");
// 分别获取各个会话对应的用户ID,并输出到控制台
auto res1 = ss.uid("会话ID1");
if (res1) std::cout << *res1 << std::endl;
auto res2 = ss.uid("会话ID2");
if (res2) std::cout << *res2 << std::endl;
auto res3 = ss.uid("会话ID3");
if (res3) std::cout << *res3 << std::endl;
auto res4 = ss.uid("会话ID4");
if (res4) std::cout << *res4 << std::endl;
}
// status_test 函数用于测试 Status 类的功能:添加在线状态、删除状态以及检测状态是否存在
void status_test(const std::shared_ptr<sw::redis::Redis> &client)
{
// 创建 Status 对象,传入 Redis 客户端
liren::Status status(client);
// 添加多个用户的在线状态记录(使用用户ID作为键,值为空字符串表示在线)
status.append("用户ID1");
status.append("用户ID2");
status.append("用户ID3");
// 删除其中一个用户的在线状态
status.remove("用户ID2");
// 检测各用户在线状态,并输出提示信息
if (status.exists("用户ID1")) std::cout << "用户1在线!" << std::endl;
if (status.exists("用户ID2")) std::cout << "用户2在线!" << std::endl;
if (status.exists("用户ID3")) std::cout << "用户3在线!" << std::endl;
}
// code_test 函数用于测试 Codes 类的功能:添加验证码、删除验证码以及查询验证码,并测试验证码超时效果
void code_test(const std::shared_ptr<sw::redis::Redis> &client)
{
// 创建 Codes 对象,传入 Redis 客户端
liren::Codes codes(client);
// 添加多个验证码记录,键为验证码ID,值为验证码内容,默认设置了超时时间(一般为 300 秒)
codes.append("验证码ID1", "验证码1");
codes.append("验证码ID2", "验证码2");
codes.append("验证码ID3", "验证码3");
// 删除其中一个验证码记录
codes.remove("验证码ID2");
// 分别查询各验证码记录,并输出查询结果
auto y1 = codes.code("验证码ID1");
auto y2 = codes.code("验证码ID2");
auto y3 = codes.code("验证码ID3");
if (y1) std::cout << *y1 << std::endl;
if (y2) std::cout << *y2 << std::endl;
if (y3) std::cout << *y3 << std::endl;
// 等待 4 秒(模拟验证码过期情况)
std::this_thread::sleep_for(std::chrono::seconds(4));
// 再次查询验证码记录,验证是否已过期
auto y4 = codes.code("验证码ID1");
auto y5 = codes.code("验证码ID2");
auto y6 = codes.code("验证码ID3");
if (!y4) std::cout << "验证码ID1不存在" << std::endl;
if (!y5) std::cout << "验证码ID2不存在" << std::endl;
if (!y6) std::cout << "验证码ID3不存在" << std::endl;
}
int main(int argc, char *argv[])
{
google::ParseCommandLineFlags(&argc, &argv, true);
// liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 通过 RedisClientFactory 创建 Redis 客户端对象,使用命令行参数设置 IP、端口、数据库编号和长连接保活选项
auto client = liren::RedisClientFactory::create(FLAGS_ip, FLAGS_port, FLAGS_db, FLAGS_keep_alive);
// 运行不同的测试函数,可根据需要取消注释相应的测试
session_test(client);
status_test(client);
code_test(client);
return 0;
} makefile 文件:
main : main.cc
g++ -std=c++17 $^ -o $@ -lhiredis -lredis++ -lgflags 三、文档数据库管理
用户信息的用户 ID、手机号、昵称字段需要在 ES 服务器额外进行一份存储,其目的是因为有用户搜索的功能,用户搜索通常会是一种字符串的模糊匹配方式,用传统的关系型数据库进行模糊匹配效率会极差,因此采用 ES 服务对索引字段进行分词后构建倒排索引,根据关键词进行搜索,效率会大大提升。
data_es.hpp
#include "elastic.hpp" // 引入 Elasticlient 客户端相关定义(ElasticSearch 操作封装)
#include "user.hxx" // 用户实体类定义(用于ES中操作的用户数据)
namespace liren
{
// ESClientFactory 类用于创建 Elasticsearch 客户端对象
// 封装了通过一组主机地址创建 Elasticlient 客户端的过程
class ESClientFactory
{
public:
// 静态方法 create:传入主机列表,返回一个 Elasticlient 客户端的智能指针
static std::shared_ptr<elasticlient::Client> create(const std::vector<std::string> host_list) {
return std::make_shared<elasticlient::Client>(host_list);
}
};
// ESUser 类封装了针对用户数据在 Elasticsearch 中的操作
// 包括创建用户索引和插入/更新用户数据,以及基于条件搜索用户数据
class ESUser
{
public:
using ptr = std::shared_ptr<ESUser>;
// 构造函数:接收一个 Elasticlient 客户端对象,后续所有操作都依赖此客户端
ESUser(const std::shared_ptr<elasticlient::Client> &client)
: _es_client(client)
{}
// createIndex 方法用于创建用户信息索引
// 通过链式调用定义各个字段的映射规则,并最终调用 create() 创建索引
bool createIndex()
{
bool ret = ESIndex(_es_client, "user")
.append("user_id", "keyword", "standard", true)
.append("nickname")
.append("phone", "keyword", "standard", true)
.append("description", "text", "standard", false)
.append("avatar_id", "keyword", "standard", false)
.create();
if (ret == false) {
LOG_INFO("用户信息索引创建失败!");
return false;
}
LOG_INFO("用户信息索引创建成功!");
return true;
}
// appendData 方法用于新增或更新用户数据
// 参数为用户ID、手机号、昵称、描述和头像ID
bool appendData(const std::string &uid,
const std::string &phone,
const std::string &nickname,
const std::string &description,
const std::string &avatar_id)
{
bool ret = ESInsert(_es_client, "user")
.append("user_id", uid)
.append("nickname", nickname)
.append("phone", phone)
.append("description", description)
.append("avatar_id", avatar_id)
.insert(uid); // 使用 uid 作为文档的唯一标识符插入数据
if (ret == false) {
LOG_ERROR("用户数据插入/更新失败!");
return false;
}
LOG_INFO("用户数据新增/更新成功!");
return true;
}
// search 方法用于基于关键字搜索用户数据
// 参数 key 为搜索关键字,uid_list 为排除的用户ID列表(must_not 条件)
// 返回值为符合条件的 User 对象的 vector
std::vector<User> search(const std::string &key, const std::vector<std::string> &uid_list)
{
std::vector<User> res;
// 使用 ESSearch 对象构造查询请求
// 通过 append_should_match 对多个字段进行模糊匹配
// append_must_not_terms 用于排除指定 uid_list 中的用户ID
Json::Value json_user = ESSearch(_es_client, "user")
.append_should_match("phone.keyword", key)
.append_should_match("user_id.keyword", key)
.append_should_match("nickname", key)
.append_must_not_terms("user_id.keyword", uid_list)
.search();
// 判断返回结果是否为数组类型
if (json_user.isArray() == false) {
LOG_ERROR("用户搜索结果为空,或者结果不是数组类型");
return res;
}
int sz = json_user.size();
LOG_DEBUG("检索结果条目数量:{}", sz);
// 遍历查询结果,构造 User 对象,并添加到返回 vector 中
for (int i = 0; i < sz; i++) {
User user;
user.user_id(json_user[i]["_source"]["user_id"].asString());
user.nickname(json_user[i]["_source"]["nickname"].asString());
user.description(json_user[i]["_source"]["description"].asString());
user.phone(json_user[i]["_source"]["phone"].asString());
user.avatar_id(json_user[i]["_source"]["avatar_id"].asString());
res.push_back(user);
}
return res;
}
private:
// 保存 Elasticlient 客户端对象,后续所有 ES 操作都通过该客户端进行
std::shared_ptr<elasticlient::Client> _es_client;
};
} 测试代码
#include "../../../header/data_es.hpp"
#include <gflags/gflags.h>
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(es_host, "http://127.0.0.1:9200/", "es服务器URL");
int main(int argc, char *argv[])
{
google::ParseCommandLineFlags(&argc, &argv, true);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 创建es客户端对象
auto es_client = liren::ESClientFactory::create({FLAGS_es_host});
// 创建es操作类对象,进行索引创建、增加数据以及查询
auto es_user = std::make_shared<liren::ESUser>(es_client);
es_user->createIndex();
es_user->appendData("用户ID1", "手机号1", "小猪佩奇", "这是一只小猪", "小猪头像1");
es_user->appendData("用户ID2", "手机号2", "小猪乔治", "这是一只小小猪", "小猪头像2");
auto res = es_user->search("小猪", {"用户ID1"});
for (auto &u : res) {
std::cout << "-----------------" << std::endl;
std::cout << u.user_id() << std::endl;
std::cout << u.phone() << std::endl;
std::cout << u.nickname() << std::endl;
std::cout << u.description() << std::endl;
std::cout << u.avatar_id() << std::endl;
}
return 0;
} makefile 文件:
main : main.cc
c++ $^ -o $@ -I../../../odb/ -I./ -lfmt -lspdlog -lgflags -lcpr -lelasticlient /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19用户管理子服务搭建流程
用户注册
-
从请求中取出昵称和密码
-
检查昵称是否合法(长度限制 3~15 之间)
-
检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)
-
根据昵称在数据库进行判断是否昵称已存在
-
向数据库新增数据
-
向 ES 服务器中新增用户信息
-
组织响应,进行成功与否的响应即可。
用户登录
- 从请求中取出昵称和密码
- 通过昵称获取用户信息,进行密码是否一致的判断
- 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
- 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
- 组织响应,返回生成的会话 ID
获取短信验证码
- 从请求中取出手机号码
- 验证手机号码格式是否正确(必须以 1 开始,第二位 3~9 之间,后边 9 个数字字符)
- 生成 4 位随机验证码
- 基于短信平台 SDK 发送验证码
- 构造验证码 ID,添加到 redis 验证码映射键值索引中
- 组织响应,返回生成的验证码 ID
手机号注册
- 从请求中取出手机号码和验证码
- 检查注册手机号码是否合法
- 从 redis 数据库中进行验证码 ID-验证码一致性匹配
- 通过数据库查询判断手机号是否已经注册过
- 向数据库新增用户信息
- 向 ES 服务器中新增用户信息
- 组织响应,返回注册成功与否
手机号登录
- 从请求中取出手机号码和验证码 ID,以及验证码。
- 检查注册手机号码是否合法
- 从 redis 数据库中进行验证码 ID-验证码一致性匹配
- 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
- 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
- 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
- 组织响应,返回生成的会话 ID
获取用户信息
- 从请求中取出用户 ID
- 通过用户 ID,从数据库中查询用户信息
- 根据用户信息中的头像 ID,从文件服务器获取头像文件数据,组织完整用户信息
- 组织响应,返回用户信息设置头像
- 从请求中取出用户 ID 与头像数据
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 上传头像文件到文件子服务,
- 将返回的头像文件 ID 更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置昵称
- 从请求中取出用户 ID 与新的昵称
- 判断昵称格式是否正确
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 将新的昵称更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置签名
- 从请求中取出用户 ID 与新的签名
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 将新的签名更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置绑定手机号
- 从请求中取出手机号码和验证码 ID,以及验证码。
- 检查注册手机号码是否合法
- 从 redis 数据库中进行验证码ID-验证码 一致性匹配
- 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
- 将新的手机号更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
user_client.cc(测试代码)