聊天项目(28) 分布式服务通知好友申请

简介

本文介绍如何实现用户查找和好友申请功能。查找和申请好友会涉及前后端通信和rpc服务间调用。所以目前先从客户端入手,搜索用户后发送查找好友申请请求给服务器,服务器收到后判断是否存在,如果不存在则显示未找到,如果存在则显示查找到的结果

点击查询

客户端点击搜索列表的添加好友item后,先弹出一个模态对话框,上面有loading动作表示加载,直到服务器返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void SearchList::slot_item_clicked(QListWidgetItem *item)
{
QWidget *widget = this->itemWidget(item); //获取自定义widget对象
if(!widget){
qDebug()<< "slot item clicked widget is nullptr";
return;
}

// 对自定义widget进行操作, 将item 转化为基类ListItemBase
ListItemBase *customItem = qobject_cast<ListItemBase*>(widget);
if(!customItem){
qDebug()<< "slot item clicked widget is nullptr";
return;
}

auto itemType = customItem->GetItemType();
if(itemType == ListItemType::INVALID_ITEM){
qDebug()<< "slot invalid item clicked ";
return;
}

if(itemType == ListItemType::ADD_USER_TIP_ITEM){
if(_send_pending){
return;
}

if (!_search_edit) {
return;
}

waitPending(true);
auto search_edit = dynamic_cast<CustomizeEdit*>(_search_edit);
auto uid_str = search_edit->text();
QJsonObject jsonObj;
jsonObj["uid"] = uid_str;

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_SEARCH_USER_REQ,
jsonData);
return;
}

//清楚弹出框
CloseFindDlg();

}

_send_pending为新增的成员变量,如果为true则表示发送阻塞.构造函数中将其设置为false。

waitPending函数为根据pending状态展示加载框

1
2
3
4
5
6
7
8
9
10
11
12
13
void SearchList::waitPending(bool pending)
{
if(pending){
_loadingDialog = new LoadingDlg(this);
_loadingDialog->setModal(true);
_loadingDialog->show();
_send_pending = pending;
}else{
_loadingDialog->hide();
_loadingDialog->deleteLater();
_send_pending = pending;
}
}

当我们发送数据后服务器会处理,返回ID_SEARCH_USER_RSP包,所以客户端要实现对ID_SEARCH_USER_RSP包的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_handlers.insert(ID_SEARCH_USER_RSP, [this](ReqId id, int len, QByteArray data){
Q_UNUSED(len);
qDebug()<< "handle id is "<< id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if(jsonDoc.isNull()){
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if(!jsonObj.contains("error")){
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err ;
emit sig_login_failed(err);
return;
}

int err = jsonObj["error"].toInt();
if(err != ErrorCodes::SUCCESS){
qDebug() << "Login Failed, err is " << err ;
emit sig_login_failed(err);
return;
}

auto search_info = std::make_shared<SearchInfo>(jsonObj["uid"].toInt(),
jsonObj["name"].toString(), jsonObj["nick"].toString(),
jsonObj["desc"].toString(), jsonObj["sex"].toInt(), jsonObj["icon"].toString());

emit sig_user_search(search_info);
});

将搜索到的结果封装为search_info发送给SearchList类做展示, search_list中连接信号和槽

1
2
//连接搜索条目
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_user_search, this, &SearchList::slot_user_search);

slot_user_search槽函数弹出搜索结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void SearchList::slot_user_search(std::shared_ptr<SearchInfo> si)
{
waitPending(false);
if(si == nullptr){
_find_dlg = std::make_shared<FindFailDlg>(this);
}else{
//此处分两种情况,一种是搜多到已经是自己的朋友了,一种是未添加好友
//查找是否已经是好友 todo...
_find_dlg = std::make_shared<FindSuccessDlg>(this);
std::dynamic_pointer_cast<FindSuccessDlg>(_find_dlg)->SetSearchInfo(si);
}

_find_dlg->show();
}

FindSuccessDlg是找到的结果展示,FindFailDlg是未找到结果展示。以下为FindSuccessDlg的ui布局

https://cdn.llfc.club/1722655438089.jpg

具体声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FindSuccessDlg : public QDialog
{
Q_OBJECT

public:
explicit FindSuccessDlg(QWidget *parent = nullptr);
~FindSuccessDlg();
void SetSearchInfo(std::shared_ptr<SearchInfo> si);

private:
Ui::FindSuccessDlg *ui;
std::shared_ptr<SearchInfo> _si;
QWidget * _parent;

private slots:
void on_add_friend_btn_clicked();
};

具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
FindSuccessDlg::FindSuccessDlg(QWidget *parent) :
QDialog(parent), _parent(parent),
ui(new Ui::FindSuccessDlg)
{
ui->setupUi(this);
// 设置对话框标题
setWindowTitle("添加");
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
// 获取当前应用程序的路径
QString app_path = QCoreApplication::applicationDirPath();
QString pix_path = QDir::toNativeSeparators(app_path +
QDir::separator() + "static"+QDir::separator()+"head_1.jpg");
QPixmap head_pix(pix_path);
head_pix = head_pix.scaled(ui->head_lb->size(),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->head_lb->setPixmap(head_pix);
ui->add_friend_btn->SetState("normal","hover","press");
this->setModal(true);
}

FindSuccessDlg::~FindSuccessDlg()
{
qDebug()<<"FindSuccessDlg destruct";
delete ui;
}

void FindSuccessDlg::SetSearchInfo(std::shared_ptr<SearchInfo> si)
{
ui->name_lb->setText(si->_name);
_si = si;
}

void FindSuccessDlg::on_add_friend_btn_clicked()
{
//todo... 添加好友界面弹出
this->hide();
//弹出加好友界面
auto applyFriend = new ApplyFriend(_parent);
applyFriend->SetSearchInfo(_si);
applyFriend->setModal(true);
applyFriend->show();
}

类似的FindFailDlg也是这种思路,大家自己实现即可。

服务器查询逻辑

chatserver服务器要根据客户端发送过来的用户id进行查找,chatserver服务器需先注册ID_SEARCH_USER_REQ和回调函数

1
2
3
4
5
6
7
void LogicSystem::RegisterCallBacks() {
_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,
placeholders::_1, placeholders::_2, placeholders::_3);

_fun_callbacks[ID_SEARCH_USER_REQ] = std::bind(&LogicSystem::SearchInfo, this,
placeholders::_1, placeholders::_2, placeholders::_3);
}

SearchInfo根据用户uid查询具体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void LogicSystem::SearchInfo(std::shared_ptr<CSession> session, const short& msg_id, const string& msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto uid_str = root["uid"].asString();
std::cout << "user SearchInfo uid is " << uid_str << endl;

Json::Value rtvalue;

Defer deder([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_SEARCH_USER_RSP);
});

bool b_digit = isPureDigit(uid_str);
if (b_digit) {
GetUserByUid(uid_str, rtvalue);
}
else {
GetUserByName(uid_str, rtvalue);
}
}

到此客户端和服务器搜索查询的联调功能已经解决了。

客户端添加好友

当Client1搜索到好友后,点击添加弹出信息界面,然后点击确定即可向对方Client2申请添加好友,这个请求要先发送到Client1所在的服务器Server1,服务器收到后判断Client2所在服务器,如果Client2在Server1则直接在Server1中查找Client2的连接信息,没找到说明Client2未在内存中,找到了则通过Session发送tcp给对方。如果Client2不在Server1而在Server2上,则需要让Server1通过grpc接口通知Server2,Server2收到后继续判断Client2是否在线,如果在线则通知。

如下图,Client1想和Client2以及Client3分别通信,需要先将请求发给Client1所在的Server1,再考虑是否rpc调用。

https://cdn.llfc.club/1722844689701.jpg

客户端在ApplySure槽函数中添加好友请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ApplyFriend::SlotApplySure()
{
qDebug() << "Slot Apply Sure called" ;
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["uid"] = uid;
auto name = ui->name_ed->text();
if(name.isEmpty()){
name = ui->name_ed->placeholderText();
}

jsonObj["applyname"] = name;
auto bakname = ui->back_ed->text();
if(bakname.isEmpty()){
bakname = ui->back_ed->placeholderText();
}
jsonObj["bakname"] = bakname;
jsonObj["touid"] = _si->_uid;

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_ADD_FRIEND_REQ, jsonData);

this->hide();
deleteLater();
}

另一个客户端会收到服务器通知添加好友的请求,所以在TcpMgr里监听这个请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
_handlers.insert(ID_NOTIFY_ADD_FRIEND_REQ, [this](ReqId id, int len, QByteArray data) {
Q_UNUSED(len);
qDebug() << "handle id is " << id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if (jsonDoc.isNull()) {
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if (!jsonObj.contains("error")) {
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err;

emit sig_user_search(nullptr);
return;
}

int err = jsonObj["error"].toInt();
if (err != ErrorCodes::SUCCESS) {
qDebug() << "Login Failed, err is " << err;
emit sig_user_search(nullptr);
return;
}

int from_uid = jsonObj["applyuid"].toInt();
QString name = jsonObj["name"].toString();
QString desc = jsonObj["desc"].toString();
QString icon = jsonObj["icon"].toString();
QString nick = jsonObj["nick"].toString();
int sex = jsonObj["sex"].toInt();

auto apply_info = std::make_shared<AddFriendApply>(
from_uid, name, desc,
icon, nick, sex);

emit sig_friend_apply(apply_info);
});

服务调用

服务器要处理客户端发过来的添加好友的请求,并决定是否调用rpc通知其他服务。

先将AddFriendApply函数注册到回调map里

1
2
3
4
5
6
7
8
9
10
void LogicSystem::RegisterCallBacks() {
_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,
placeholders::_1, placeholders::_2, placeholders::_3);

_fun_callbacks[ID_SEARCH_USER_REQ] = std::bind(&LogicSystem::SearchInfo, this,
placeholders::_1, placeholders::_2, placeholders::_3);

_fun_callbacks[ID_ADD_FRIEND_REQ] = std::bind(&LogicSystem::AddFriendApply, this,
placeholders::_1, placeholders::_2, placeholders::_3);
}

接下来实现AddFriendApply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void LogicSystem::AddFriendApply(std::shared_ptr<CSession> session, const short& msg_id, const string& msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto uid = root["uid"].asInt();
auto applyname = root["applyname"].asString();
auto bakname = root["bakname"].asString();
auto touid = root["touid"].asInt();
std::cout << "user login uid is " << uid << " applyname is "
<< applyname << " bakname is " << bakname << " touid is " << touid << endl;

Json::Value rtvalue;
rtvalue["error"] = ErrorCodes::Success;
Defer defer([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, ID_ADD_FRIEND_RSP);
});

//先更新数据库
MysqlMgr::GetInstance()->AddFriendApply(uid, touid);

//查询redis 查找touid对应的server ip
auto to_str = std::to_string(touid);
auto to_ip_key = USERIPPREFIX + to_str;
std::string to_ip_value = "";
bool b_ip = RedisMgr::GetInstance()->Get(to_ip_key, to_ip_value);
if (!b_ip) {
return;
}

auto& cfg = ConfigMgr::Inst();
auto self_name = cfg["SelfServer"]["Name"];

//直接通知对方有申请消息
if (to_ip_value == self_name) {
auto session = UserMgr::GetInstance()->GetSession(touid);
if (session) {
//在内存中则直接发送通知对方
Json::Value notify;
notify["error"] = ErrorCodes::Success;
notify["applyuid"] = uid;
notify["name"] = applyname;
notify["desc"] = "";
std::string return_str = notify.toStyledString();
session->Send(return_str, ID_NOTIFY_ADD_FRIEND_REQ);
}

return;
}

std::string base_key = USER_BASE_INFO + std::to_string(uid);
auto apply_info = std::make_shared<UserInfo>();
bool b_info = GetBaseInfo(base_key, uid, apply_info);

AddFriendReq add_req;
add_req.set_applyuid(uid);
add_req.set_touid(touid);
add_req.set_name(applyname);
add_req.set_desc("");
if (b_info) {
add_req.set_icon(apply_info->icon);
add_req.set_sex(apply_info->sex);
add_req.set_nick(apply_info->nick);
}

//发送通知
ChatGrpcClient::GetInstance()->NotifyAddFriend(to_ip_value, add_req);
}

上面的函数中先更新数据库将申请写入数据库中

1
2
3
bool MysqlMgr::AddFriendApply(const int& from, const int& to) {
return _dao.AddFriendApply(from, to);
}

内部调用dao层面的添加好友请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool MysqlDao::AddFriendApply(const int& from, const int& to) {
auto con = pool_->getConnection();
if (con == nullptr) {
return false;
}

Defer defer([this, &con]() {
pool_->returnConnection(std::move(con));
});

try {
std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT INTO friend_apply (from_uid, to_uid) values (?,?) "
"ON DUPLICATE KEY UPDATE from_uid = from_uid, to_uid = to_uid "));
pstmt->setInt(1, from);
pstmt->setInt(2, to);
//执行更新
int rowAffected = pstmt->executeUpdate();
if (rowAffected < 0) {
return false;
}

return true;
}
catch (sql::SQLException& e) {
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}

return true;
}

添加完成后判断要通知的对端是否在本服务器,如果在本服务器则直接通过uid查找session,判断用户是否在线,如果在线则直接通知对端。

如果不在本服务器,则需要通过rpc通知对端服务器。rpc的客户端这么写即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
AddFriendRsp ChatGrpcClient::NotifyAddFriend(std::string server_ip, const AddFriendReq& req) {
AddFriendRsp rsp;
Defer defer([&rsp, &req]() {
rsp.set_error(ErrorCodes::Success);
rsp.set_applyuid(req.applyuid());
rsp.set_touid(req.touid());
});

auto find_iter = _pools.find(server_ip);
if (find_iter == _pools.end()) {
return rsp;
}

auto& pool = find_iter->second;
ClientContext context;
auto stub = pool->getConnection();
Status status = stub->NotifyAddFriend(&context, req, &rsp);
Defer defercon([&stub, this, &pool]() {
pool->returnConnection(std::move(stub));
});

if (!status.ok()) {
rsp.set_error(ErrorCodes::RPCFailed);
return rsp;
}

return rsp;
}

同样rpc的服务端也要实现,我们先将rpc客户端和服务端的逻辑都在ChatServer1写好,然后复制给ChatServer2即可。 rpc的服务实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Status ChatServiceImpl::NotifyAddFriend(ServerContext* context, const AddFriendReq* request,
AddFriendRsp* reply) {
//查找用户是否在本服务器
auto touid = request->touid();
auto session = UserMgr::GetInstance()->GetSession(touid);

Defer defer([request, reply]() {
reply->set_error(ErrorCodes::Success);
reply->set_applyuid(request->applyuid());
reply->set_touid(request->touid());
});

//用户不在内存中则直接返回
if (session == nullptr) {
return Status::OK;
}

//在内存中则直接发送通知对方
Json::Value rtvalue;
rtvalue["error"] = ErrorCodes::Success;
rtvalue["applyuid"] = request->applyuid();
rtvalue["name"] = request->name();
rtvalue["desc"] = request->desc();
rtvalue["icon"] = request->icon();
rtvalue["sex"] = request->sex();
rtvalue["nick"] = request->nick();

std::string return_str = rtvalue.toStyledString();

session->Send(return_str, ID_NOTIFY_ADD_FRIEND_REQ);

return Status::OK;
}

上面的代码也是判断要通知的客户端是否在内存中,如果在就通过session发送tcp请求。

将ChatServer1的代码拷贝给ChatServer2,重启两个服务,再启动两个客户端,一个客户端申请另一个客户端,通过查看客户端日志是能看到申请信息的。

申请显示

接下来被通知申请的客户端要做界面显示,我们实现被通知的客户端收到sig_friend_apply信号的处理逻辑。在ChatDialog的构造函数中连接信号和槽

1
2
//连接申请添加好友信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_friend_apply, this, &ChatDialog::slot_apply_friend);

实现申请好友的槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ChatDialog::slot_apply_friend(std::shared_ptr<AddFriendApply> apply)
{
qDebug() << "receive apply friend slot, applyuid is " << apply->_from_uid << " name is "
<< apply->_name << " desc is " << apply->_desc;

bool b_already = UserMgr::GetInstance()->AlreadyApply(apply->_from_uid);
if(b_already){
return;
}

UserMgr::GetInstance()->AddApplyList(std::make_shared<ApplyInfo>(apply));
ui->side_contact_lb->ShowRedPoint(true);
ui->con_user_list->ShowRedPoint(true);
ui->friend_apply_page->AddNewApply(apply);
}

这样就能显示新的申请消息和红点了。具体添加一个新的申请条目到申请好友页面的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ApplyFriendPage::AddNewApply(std::shared_ptr<AddFriendApply> apply)
{
//先模拟头像随机,以后头像资源增加资源服务器后再显示
int randomValue = QRandomGenerator::global()->bounded(100); // 生成0到99之间的随机整数
int head_i = randomValue % heads.size();
auto* apply_item = new ApplyFriendItem();
auto apply_info = std::make_shared<ApplyInfo>(apply->_from_uid,
apply->_name, apply->_desc,heads[head_i], apply->_name, 0, 0);
apply_item->SetInfo( apply_info);
QListWidgetItem* item = new QListWidgetItem;
//qDebug()<<"chat_user_wid sizeHint is " << chat_user_wid->sizeHint();
item->setSizeHint(apply_item->sizeHint());
item->setFlags(item->flags() & ~Qt::ItemIsEnabled & ~Qt::ItemIsSelectable);
ui->apply_friend_list->insertItem(0,item);
ui->apply_friend_list->setItemWidget(item, apply_item);
apply_item->ShowAddBtn(true);
//收到审核好友信号
connect(apply_item, &ApplyFriendItem::sig_auth_friend, [this](std::shared_ptr<ApplyInfo> apply_info) {
auto* authFriend = new AuthenFriend(this);
authFriend->setModal(true);
authFriend->SetApplyInfo(apply_info);
authFriend->show();
});
}

测试效果, 收到对方请求后如下图

https://cdn.llfc.club/1722851642815.jpg

登录加载申请

当用户登录后,服务器需要将申请列表同步给客户端, 写在登录逻辑里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   //从数据库获取申请列表
std::vector<std::shared_ptr<ApplyInfo>> apply_list;
auto b_apply = GetFriendApplyInfo(uid,apply_list);
if (b_apply) {
for (auto & apply : apply_list) {
Json::Value obj;
obj["name"] = apply->_name;
obj["uid"] = apply->_uid;
obj["icon"] = apply->_icon;
obj["nick"] = apply->_nick;
obj["sex"] = apply->_sex;
obj["desc"] = apply->_desc;
obj["status"] = apply->_status;
rtvalue["apply_list"].append(obj);
}
}

获取好友申请信息函数

1
2
3
4
bool LogicSystem::GetFriendApplyInfo(int to_uid, std::vector<std::shared_ptr<ApplyInfo>> &list) {
//从mysql获取好友申请列表
return MysqlMgr::GetInstance()->GetApplyList(to_uid, list, 0, 10);
}

dao层面实现获取申请列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
bool MysqlMgr::GetApplyList(int touid, 
std::vector<std::shared_ptr<ApplyInfo>>& applyList, int begin, int limit) {

return _dao.GetApplyList(touid, applyList, begin, limit);
}

bool MysqlDao::GetApplyList(int touid, std::vector<std::shared_ptr<ApplyInfo>>& applyList, int begin, int limit) {
auto con = pool_->getConnection();
if (con == nullptr) {
return false;
}

Defer defer([this, &con]() {
pool_->returnConnection(std::move(con));
});


try {
// 准备SQL语句, 根据起始id和限制条数返回列表
std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("select apply.from_uid, apply.status, user.name, "
"user.nick, user.sex from friend_apply as apply join user on apply.from_uid = user.uid where apply.to_uid = ? "
"and apply.id > ? order by apply.id ASC LIMIT ? "));

pstmt->setInt(1, touid); // 将uid替换为你要查询的uid
pstmt->setInt(2, begin); // 起始id
pstmt->setInt(3, limit); //偏移量
// 执行查询
std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());
// 遍历结果集
while (res->next()) {
auto name = res->getString("name");
auto uid = res->getInt("from_uid");
auto status = res->getInt("status");
auto nick = res->getString("nick");
auto sex = res->getInt("sex");
auto apply_ptr = std::make_shared<ApplyInfo>(uid, name, "", "", nick, sex, status);
applyList.push_back(apply_ptr);
}
return true;
}
catch (sql::SQLException& e) {
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}

好友认证界面

客户端需要实现好友认证界面,当点击同意对方好友申请后,弹出认证信息,点击确定后将认证同意的请求发给服务器,服务器再通知申请方,告知对方被申请人已经同意加好友了。认证界面和申请界面类似, 这个大家自己实现即可。

https://cdn.llfc.club/1722854446243.jpg

认证界面的函数和逻辑可以照抄申请好友的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
AuthenFriend::AuthenFriend(QWidget *parent) :
QDialog(parent),
ui(new Ui::AuthenFriend),_label_point(2,6)
{
ui->setupUi(this);
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
this->setObjectName("AuthenFriend");
this->setModal(true);
ui->lb_ed->setPlaceholderText("搜索、添加标签");
ui->back_ed->setPlaceholderText("燃烧的胸毛");

ui->lb_ed->SetMaxLength(21);
ui->lb_ed->move(2, 2);
ui->lb_ed->setFixedHeight(20);
ui->lb_ed->setMaxLength(10);
ui->input_tip_wid->hide();

_tip_cur_point = QPoint(5, 5);

_tip_data = { "同学","家人","菜鸟教程","C++ Primer","Rust 程序设计",
"父与子学Python","nodejs开发指南","go 语言开发指南",
"游戏伙伴","金融投资","微信读书","拼多多拼友" };

connect(ui->more_lb, &ClickedOnceLabel::clicked, this, &AuthenFriend::ShowMoreLabel);
InitTipLbs();
//链接输入标签回车事件
connect(ui->lb_ed, &CustomizeEdit::returnPressed, this, &AuthenFriend::SlotLabelEnter);
connect(ui->lb_ed, &CustomizeEdit::textChanged, this, &AuthenFriend::SlotLabelTextChange);
connect(ui->lb_ed, &CustomizeEdit::editingFinished, this, &AuthenFriend::SlotLabelEditFinished);
connect(ui->tip_lb, &ClickedOnceLabel::clicked, this, &AuthenFriend::SlotAddFirendLabelByClickTip);

ui->scrollArea->horizontalScrollBar()->setHidden(true);
ui->scrollArea->verticalScrollBar()->setHidden(true);
ui->scrollArea->installEventFilter(this);
ui->sure_btn->SetState("normal","hover","press");
ui->cancel_btn->SetState("normal","hover","press");
//连接确认和取消按钮的槽函数
connect(ui->cancel_btn, &QPushButton::clicked, this, &AuthenFriend::SlotApplyCancel);
connect(ui->sure_btn, &QPushButton::clicked, this, &AuthenFriend::SlotApplySure);
}

AuthenFriend::~AuthenFriend()
{
qDebug()<< "AuthenFriend destruct";
delete ui;
}

void AuthenFriend::InitTipLbs()
{
int lines = 1;
for(int i = 0; i < _tip_data.size(); i++){

auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度

if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {
lines++;
if (lines > 2) {
delete lb;
return;
}

_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

auto next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point,next_point, textWidth, textHeight);

_tip_cur_point = next_point;
}

}

void AuthenFriend::AddTipLbs(ClickedLabel* lb, QPoint cur_point, QPoint& next_point, int text_width, int text_height)
{
lb->move(cur_point);
lb->show();
_add_labels.insert(lb->text(), lb);
_add_label_keys.push_back(lb->text());
next_point.setX(lb->pos().x() + text_width + 15);
next_point.setY(lb->pos().y());
}

bool AuthenFriend::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->scrollArea && event->type() == QEvent::Enter)
{
ui->scrollArea->verticalScrollBar()->setHidden(false);
}
else if (obj == ui->scrollArea && event->type() == QEvent::Leave)
{
ui->scrollArea->verticalScrollBar()->setHidden(true);
}
return QObject::eventFilter(obj, event);
}

void AuthenFriend::SetApplyInfo(std::shared_ptr<ApplyInfo> apply_info)
{
_apply_info = apply_info;
ui->back_ed->setPlaceholderText(apply_info->_name);
}

void AuthenFriend::ShowMoreLabel()
{
qDebug()<< "receive more label clicked";
ui->more_lb_wid->hide();

ui->lb_list->setFixedWidth(325);
_tip_cur_point = QPoint(5, 5);
auto next_point = _tip_cur_point;
int textWidth;
int textHeight;
//重拍现有的label
for(auto & added_key : _add_label_keys){
auto added_lb = _add_labels[added_key];

QFontMetrics fontMetrics(added_lb->font()); // 获取QLabel控件的字体信息
textWidth = fontMetrics.width(added_lb->text()); // 获取文本的宽度
textHeight = fontMetrics.height(); // 获取文本的高度

if(_tip_cur_point.x() +textWidth + tip_offset > ui->lb_list->width()){
_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y()+textHeight+15);
}
added_lb->move(_tip_cur_point);

next_point.setX(added_lb->pos().x() + textWidth + 15);
next_point.setY(_tip_cur_point.y());

_tip_cur_point = next_point;

}

//添加未添加的
for(int i = 0; i < _tip_data.size(); i++){
auto iter = _add_labels.find(_tip_data[i]);
if(iter != _add_labels.end()){
continue;
}

auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度

if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {

_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point, next_point, textWidth, textHeight);

_tip_cur_point = next_point;

}

int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);

//qDebug()<<"after resize ui->lb_list size is " << ui->lb_list->size();
ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+diff_height);
}

void AuthenFriend::resetLabels()
{
auto max_width = ui->gridWidget->width();
auto label_height = 0;
for(auto iter = _friend_labels.begin(); iter != _friend_labels.end(); iter++){
//todo... 添加宽度统计
if( _label_point.x() + iter.value()->width() > max_width) {
_label_point.setY(_label_point.y()+iter.value()->height()+6);
_label_point.setX(2);
}

iter.value()->move(_label_point);
iter.value()->show();

_label_point.setX(_label_point.x()+iter.value()->width()+2);
_label_point.setY(_label_point.y());
label_height = iter.value()->height();
}

if(_friend_labels.isEmpty()){
ui->lb_ed->move(_label_point);
return;
}

if(_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()){
ui->lb_ed->move(2,_label_point.y()+label_height+6);
}else{
ui->lb_ed->move(_label_point);
}
}

void AuthenFriend::addLabel(QString name)
{
if (_friend_labels.find(name) != _friend_labels.end()) {
return;
}

auto tmplabel = new FriendLabel(ui->gridWidget);
tmplabel->SetText(name);
tmplabel->setObjectName("FriendLabel");

auto max_width = ui->gridWidget->width();
//todo... 添加宽度统计
if (_label_point.x() + tmplabel->width() > max_width) {
_label_point.setY(_label_point.y() + tmplabel->height() + 6);
_label_point.setX(2);
}
else {

}


tmplabel->move(_label_point);
tmplabel->show();
_friend_labels[tmplabel->Text()] = tmplabel;
_friend_label_keys.push_back(tmplabel->Text());

connect(tmplabel, &FriendLabel::sig_close, this, &AuthenFriend::SlotRemoveFriendLabel);

_label_point.setX(_label_point.x() + tmplabel->width() + 2);

if (_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()) {
ui->lb_ed->move(2, _label_point.y() + tmplabel->height() + 2);
}
else {
ui->lb_ed->move(_label_point);
}

ui->lb_ed->clear();

if (ui->gridWidget->height() < _label_point.y() + tmplabel->height() + 2) {
ui->gridWidget->setFixedHeight(_label_point.y() + tmplabel->height() * 2 + 2);
}
}

void AuthenFriend::SlotLabelEnter()
{
if(ui->lb_ed->text().isEmpty()){
return;
}

addLabel(ui->lb_ed->text());

ui->input_tip_wid->hide();
}

void AuthenFriend::SlotRemoveFriendLabel(QString name)
{
qDebug() << "receive close signal";

_label_point.setX(2);
_label_point.setY(6);

auto find_iter = _friend_labels.find(name);

if(find_iter == _friend_labels.end()){
return;
}

auto find_key = _friend_label_keys.end();
for(auto iter = _friend_label_keys.begin(); iter != _friend_label_keys.end();
iter++){
if(*iter == name){
find_key = iter;
break;
}
}

if(find_key != _friend_label_keys.end()){
_friend_label_keys.erase(find_key);
}


delete find_iter.value();

_friend_labels.erase(find_iter);

resetLabels();

auto find_add = _add_labels.find(name);
if(find_add == _add_labels.end()){
return;
}

find_add.value()->ResetNormalState();
}

//点击标已有签添加或删除新联系人的标签
void AuthenFriend::SlotChangeFriendLabelByTip(QString lbtext, ClickLbState state)
{
auto find_iter = _add_labels.find(lbtext);
if(find_iter == _add_labels.end()){
return;
}

if(state == ClickLbState::Selected){
//编写添加逻辑
addLabel(lbtext);
return;
}

if(state == ClickLbState::Normal){
//编写删除逻辑
SlotRemoveFriendLabel(lbtext);
return;
}

}

void AuthenFriend::SlotLabelTextChange(const QString& text)
{
if (text.isEmpty()) {
ui->tip_lb->setText("");
ui->input_tip_wid->hide();
return;
}

auto iter = std::find(_tip_data.begin(), _tip_data.end(), text);
if (iter == _tip_data.end()) {
auto new_text = add_prefix + text;
ui->tip_lb->setText(new_text);
ui->input_tip_wid->show();
return;
}
ui->tip_lb->setText(text);
ui->input_tip_wid->show();
}

void AuthenFriend::SlotLabelEditFinished()
{
ui->input_tip_wid->hide();
}

void AuthenFriend::SlotAddFirendLabelByClickTip(QString text)
{
int index = text.indexOf(add_prefix);
if (index != -1) {
text = text.mid(index + add_prefix.length());
}
addLabel(text);
//标签展示栏也增加一个标签, 并设置绿色选中
if (index != -1) {
_tip_data.push_back(text);
}

auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(text);
connect(lb, &ClickedLabel::clicked, this, &AuthenFriend::SlotChangeFriendLabelByTip);
qDebug() << "ui->lb_list->width() is " << ui->lb_list->width();
qDebug() << "_tip_cur_point.x() is " << _tip_cur_point.x();

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
qDebug() << "textWidth is " << textWidth;

if (_tip_cur_point.x() + textWidth+ tip_offset+3 > ui->lb_list->width()) {

_tip_cur_point.setX(5);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

auto next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point, next_point, textWidth,textHeight);
_tip_cur_point = next_point;

int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);

lb->SetCurState(ClickLbState::Selected);

ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+ diff_height );
}

void AuthenFriend::SlotApplySure()
{
qDebug() << "Slot Apply Sure ";
//添加发送逻辑
QJsonObject jsonObj;
auto uid = UserMgr::GetInstance()->GetUid();
jsonObj["fromuid"] = uid;
jsonObj["touid"] = _apply_info->_uid;
QString back_name = "";
if(ui->back_ed->text().isEmpty()){
back_name = ui->back_ed->placeholderText();
}else{
back_name = ui->back_ed->text();
}
jsonObj["back"] = back_name;

QJsonDocument doc(jsonObj);
QByteArray jsonData = doc.toJson(QJsonDocument::Compact);

//发送tcp请求给chat server
emit TcpMgr::GetInstance()->sig_send_data(ReqId::ID_AUTH_FRIEND_REQ, jsonData);

this->hide();
deleteLater();
}

void AuthenFriend::SlotApplyCancel()
{
this->hide();
deleteLater();
}

源码连接

https://gitee.com/secondtonone1/llfcchat

视频连接

https://www.bilibili.com/video/BV1Ex4y1s7cq/