微服务即时通讯系统的实现(客户端)----(3)
目录
- 1. 聊天界面逻辑
- 1.1 发送消息
- 1.2 接收消息
- 2. 个人信息详情逻辑
- 2.1 加载个人信息
- 2.2 修改昵称
- 2.3 修改签名
- 2.4 修改电话 (1) - 发起短信验证码
- 2.5 修改电话 (2) - 修改电话逻辑
- 2.6 修改头像
- 3. 用户详细信息界面逻辑
- 3.1 获取指定用户的信息
- 3.2 点击 "发送消息" 打开对应会话
- 3.3 删除好友
- 3.4 删除好友推送处理
- 3.5 发送好友申请
- 4. 主界面逻辑 (2)
- 4.1 收到好友申请
- 4.2 同意好友申请
- 4.3 拒绝好友申请
- 4.4 获取到好友申请处理结果
- 5. 小结
1. 聊天界面逻辑
1.1 发送消息
(1)客户端发送消息请求:
- 在 MessageEditArea 中创建 initSignalSlot 方法。关联上 sendTextBtn的槽函数:
void MessageEditArea::initSignalSlot()
{
DataCenter* dataCenter = DataCenter::getInstance();
// 处理按钮点击
connect(sendTextBtn, &QPushButton::clicked, this, &MessageEditArea::sendTextMessage);
}
- 实现 MessageEditArea::sendTextMessage函数:
void MessageEditArea::sendTextMessage()
{
// 1. 先确认当前是否有会话选中了. 如果没有会话被选中, 则啥都不做.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
if(dataCenter->getCurrentChatSessionId().isEmpty())
{
LOG() << "当前未选中任何会话, 不会发送消息!";
// 上述日志, 只是在开发阶段能看到. 程序发布出去了, 此时就无法看到了.
// 因此需要让普通用户, 也能看到 "提示"
Toast::showMessage("当前未选中会话, 不发送任何消息!");
return;
}
// 2. 获取到输入框的内容, 看输入框里是否有内容. 啥都没输入, 此时也不做任何操作.
const QString& content = textEdit->toPlainText().trimmed();
if(content.isEmpty())
{
LOG() << "输入框为空";
return;
}
// 3. 清空输入框已有内容
textEdit->setPlainText("");
// 4. 通过网络发送数据给服务器
dataCenter->sendTextMessageAsync(dataCenter->getCurrentChatSessionId(), content);
}
- 实现 DataCenter::sendTextMessageAsync函数:
void DataCenter::sendTextMessageAsync(const QString& chatSessionId, const QString& content)
{
netClient.sendMessage(loginSessionId, chatSessionId, MessageType::TEXT_TYPE, content.toUtf8(), "");
}
- 实现 NetClient::sendMessage函数以及接口定义:
message NewMessageReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
string chat_session_id = 4;
MessageContent message = 5;
}
message NewMessageRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// ⽅法实现,此⽅法同时⽀持四种消息的发送
// 此处的 extraInfo, 可以用来传递 "扩展信息" . 尤其是对于文件消息来说, 通过这个字段表示 "文件名"
// 其他类型的消息暂时不涉及, 就直接设为 "". 如果后续有消息类型需要, 都可以给这个参数, 赋予一定的特殊含义.
void NetClient::sendMessage(const QString &loginSessionId, const QString &chatSessionId, model::MessageType messageType,
const QByteArray &content, const QString& extraInfo)
{
// 1. 通过 protobuf 构造 body
bite_im::NewMessageReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setChatSessionId(chatSessionId);
// 构造 MessageContent
bite_im::MessageContent messageContent;
if(messageType == model::TEXT_TYPE)
{
messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::STRING);
bite_im::StringMessageInfo stringMessageInfo;
stringMessageInfo.setContent(content);
messageContent.setStringMessage(stringMessageInfo);
}
else if(messageType == model::IMAGE_TYPE)
{
messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::IMAGE);
bite_im::ImageMessageInfo imageMessageInfo;
imageMessageInfo.setFileId(""); // fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
imageMessageInfo.setImageContent(content);
messageContent.setImageMessage(imageMessageInfo);
}
else if(messageType == model::FILE_TYPE)
{
messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::FILE);
bite_im::FileMessageInfo fileMessageInfo;
fileMessageInfo.setFileId(""); // fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
fileMessageInfo.setFileSize(content.size());
fileMessageInfo.setFileName(extraInfo);
fileMessageInfo.setFileContents(content);
messageContent.setFileMessage(fileMessageInfo);
}
else if(messageType == model::SPEECH_TYPE)
{
messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::SPEECH);
bite_im::SpeechMessageInfo speechMessageInfo;
speechMessageInfo.setFileId(""); // fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
speechMessageInfo.setFileContents(content);
messageContent.setSpeechMessage(speechMessageInfo);
}
else
{
LOG() << "错误的消息类型! messageType=" << messageType;
}
pbReq.setMessage(messageContent);
// 序列化
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[发送消息] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", chatSessionId=" << pbReq.chatSessionId() << ", messageType=" << pbReq.message().messageType();
QNetworkReply* resp = this->sendHttpRequest("/service/message_transmit/new_message", body);
// 3. 处理 HTTP 响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 针对响应结果进行解析
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::NewMessageRsp>(resp, &ok, &reason);
// b) 判定响应是否正确
if(!ok)
{
LOG() << "[发送消息] 处理出错! reason=" << reason;
return;
}
// c) 此处只是需要记录 "成功失败" , 不需要把内容写入到 DataCenter 中.
// d) 通知调用者, 响应处理完毕
emit dataCenter->sendMessageDone(messageType, content, extraInfo);
// e) 打印日志
LOG() << "[发送消息] 响应处理完毕! requestId=" << pbResp->requestId();
});
}
(2)客户端收到消息响应:
- 定义 DataCenter 信号:
// 发送消息完成
void sendMessageDone(MessageType messageType, const QByteArray& content, constQString& extraInfo);
void sendMessageFailed(const QString& reason);
- 修改 MessageEditArea::initSignalSlot,新增信号槽连接:
// 处理发送消息的⽹络相应, 把⾃⼰发的内容添加到消息展⽰区
connect(dataCenter, &DataCenter::sendMessageDone, this, &MessageEditArea::addSelfMessage);
connect(dataCenter, &DataCenter::sendMessageFailed, this, [=](const QString& reason)
{
Toast::showMessage("发送消息失败! " + reason);
});
- 新增函数 MessageEditArea::addSelfMessage函数将消息添加到消息显示区:
void MessageEditArea::addSelfMessage(model::MessageType messageType, const QByteArray& content, const QString& extraInfo)
{
model::DataCenter* dataCenter = model::DataCenter::getInstance();
const QString& currentChatSessionId = dataCenter->getCurrentChatSessionId();
// 1. 构造出一个消息对象
Message message = Message::makeMessage(messageType, currentChatSessionId, *dataCenter->getMyself(), content, extraInfo);
dataCenter->addMessage(message);
// 2. 把这个新的消息, 显示到消息展示区
MainWidget* mainWidget = MainWidget::getInstance();
MessageShowArea* messageShowArea = mainWidget->getMessageShowArea();
messageShowArea->addMessage(false, message);
// 3. 控制消息显示区, 滚动条, 滚动到末尾.
messageShowArea->scrollToEnd();
// 4. 发送信号, 通知会话列表, 更新最后一条消息
emit dataCenter->updateLastMessage(currentChatSessionId);
}
- 给 DataCenter 定义信号。更新会话列表中的 “最后⼀条消息”:
// 更新会话列表中的最后⼀条消息
void updateLastMessage(const QString& chatSessionId);
- 在 SessionArea 的 SessionItem 构造函数中,连接上述信号并处理:
SessionItem::SessionItem(QWidget* owner, const QString& chatSessionId, const QIcon& avatar,
const QString& name, const QString& lastMessage)
:SessionFriendItem(owner, avatar, name, lastMessage)
,chatSessionId(chatSessionId)
,text(lastMessage)
{
// 处理更新最后一条信息的信号
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::updateLastMessage, this, &SessionItem::updateLastMessage);
// 需要显示出未读消息的数目, 为了支持客户端重启之后, 未读消息仍然能正确显示.
int unread = dataCenter->getUnread(chatSessionId);
if(unread > 0)
{
// 存在未读消息
this->messageLabel->setText(QString("[未读%1条] ").arg(unread) + text);
}
}
void SessionItem::updateLastMessage(const QString& chatSessionId)
{
model::DataCenter* dataCenter = model::DataCenter::getInstance();
// 1. 判定 chatSessionId 是否匹配
if(this->chatSessionId != chatSessionId)
{
// 当前 SessionItem 不是你正在发消息的 SessionItem!
return;
}
// chatSessionId 匹配, 真正更新最后一条消息!!
// 2. 把最后一条消息, 获取到.
QList<Message>* messageList = dataCenter->getRecentMessageList(chatSessionId);
if(messageList == nullptr || messageList->size() == 0)
{
// 当前会话没有任何消息, 无需更新
return;
}
const Message& lastMessage = messageList->back();
// 3. 明确显示的文本内容
// 由于消息有四种类型.
// 文本消息, 直接显示消息的内容; 图片消息, 直接显示 "[图片]"; 文件消息, 直接显示 "[文件]"; 语音消息, 直接显示 "[语音]"
if(lastMessage.messageType == model::TEXT_TYPE)
{
text = lastMessage.content;
}
else if(lastMessage.messageType == model::IMAGE_TYPE)
{
text = "[图片]";
}
else if(lastMessage.messageType == model::FILE_TYPE)
{
text = "[文件]";
}
else if(lastMessage.messageType == model::SPEECH_TYPE)
{
text = "[语音]";
}
else
{
LOG() << "错误的消息类型!";
return;
}
// 4. 把这个内容, 显示到界面上
// 针对这里的逻辑, 后续还需要考虑到 "未读消息" 情况. 关于未读消息的处理, 后续编写 "接收消息" 的时候再处理.
// 先判定, 当前消息的会话, 是不是正在选中的会话. 如果是, 不会更新任何未读消息.
// 如果不是, 看未读消息是否 > 0, 并且做出前缀的拼装
if(chatSessionId == dataCenter->getCurrentChatSessionId())
{
this->messageLabel->setText(text);
}
else
{
int unread = dataCenter->getUnread(chatSessionId);
if(unread > 0)
{
this->messageLabel->setText(QString("[未读%1条] ").arg(unread) + text);
}
}
}
- 实现对于未读消息数据的处理:
void DataCenter::clearUnread(const QString& chatSessionId)
{
(*unreadMessageCount)[chatSessionId] = 0;
// 手动保存一下结果到文件中.
saveDataFile();
}
void DataCenter::addUnread(const QString& chatSessionId)
{
++(*unreadMessageCount)[chatSessionId];
// 手动保存一下结果到文件中.
saveDataFile();
}
int DataCenter::getUnread(const QString& chatSessionId)
{
return (*unreadMessageCount)[chatSessionId];
}
- 补充 SessionItem 中的未读消息处理:
void SessionItem::active()
{
// 点击之后, 要加载会话的历史消息列表
LOG() << "点击 SessionItem 触发的逻辑! chatSessionId=" << chatSessionId;
// 加载会话历史消息, 即会涉及到当前内存的数据操作, 又会涉及到网络通信, 还涉及到界面的变更.
MainWidget* mainWidget = MainWidget::getInstance();
mainWidget->loadRecentMessage(chatSessionId);
// 清空未读消息的数据, 并且更新显示
model::DataCenter* dataCenter = model::DataCenter::getInstance();
dataCenter->clearUnread(chatSessionId);
// 更新界面的显示. 把会话消息预览这里, 前面的 "[未读x条]" 内容给干掉
this->messageLabel->setText(text);
}
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/message_transmit/new_message", [=](const QHttpServerRequest& req)
{
return this->sendMessage(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::newMessage(const QHttpServerRequest &req)
{
// 解析请求
bite_im::NewMessageReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 发送消息] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", chatSessionId=" << pbReq.chatSessionId() << ", messageType=" << pbReq.message().messageType();
if (pbReq.message().messageType() == bite_im::MessageTypeGadget::MessageType::STRING)
{
LOG() << "发送的消息内容=" << pbReq.message().stringMessage().content();
}
// 构造响应
bite_im::NewMessageRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
1.2 接收消息
(1)客户端实现逻辑:
- 在 NetClient 中实现 handleWsMessage 处理 websocket 收到的数据:
void NetClient::handleWsMessage(const model::Message& message)
{
// 这里要考虑两个情况
QList<model::Message>* messageList = dataCenter->getRecentMessageList(message.chatSessionId);
if(messageList == nullptr)
{
// 1. 如果当前这个消息所属的会话, 里面的消息列表, 没有在本地加载, 此时就需要通过网络先加载整个消息列表.
connect(dataCenter, &model::DataCenter::getRecentMessageListDoneNoUI, this, &NetClient::receiveMessage, Qt::UniqueConnection);
dataCenter->getRecentMessageListAsync(message.chatSessionId, false);
}
else
{
// 2. 如果当前这个消息所属的会话, 里面的消息已经在本地加载了, 直接把这个消息尾插到消息列表中即可.
messageList->push_back(message);
this->receiveMessage(message.chatSessionId);
}
}
- 实现 DataCenter::receiveMessage函数:
void NetClient::receiveMessage(const QString& chatSessionId)
{
// 先需要判定一下, 当前这个收到的消息对应的会话, 是否是正在被用户选中的 "当前会话"
// 当前会话, 就需要把消息, 显示到消息展示区, 也需要更新会话列表的消息预览
// 不是当前会话, 只需要更新会话列表中的消息预览, 并且更新 "未读消息数目"
if(chatSessionId == dataCenter->getCurrentChatSessionId())
{
// 收到的消息会话, 就是选中会话
// 在消息展示区, 新增一个消息
const model::Message& lastMessage = dataCenter->getRecentMessageList(chatSessionId)->back();
// 通过信号, 让 NetClient 模块, 能够通知界面(消息展示区)
emit dataCenter->receiveMessageDone(lastMessage);
}
else
{
// 收到的消息会话, 不是选中会话
// 更新未读消息数目
dataCenter->addUnread(chatSessionId);
}
// 统一更新会话列表的消息预览
emit dataCenter->updateLastMessage(chatSessionId);
}
- 定义 DataCenter 信号:
// 收到消息
void receiveMessageDone(const Message& message);
- 修改 MessageEditArea::initSignalSlot,添加信号槽:
// 处理收到网络上来自别人的响应情况
connect(dataCenter, &DataCenter::receiveMessageDone, this, &MessageEditArea::addOtherMessage);
- 实现 MessageEditArea::addOtherMessage函数:
void MessageEditArea::addOtherMessage(const model::Message &message)
{
// 1. 通过主界面, 拿到消息展示区.
MainWidget* mainWidget = MainWidget::getInstance();
MessageShowArea* messageShowArea = mainWidget->getMessageShowArea();
// 2. 把收到的新的消息, 添加到消息展示区
messageShowArea->addMessage(true, message);
// 3. 控制消息展示区的滚动条, 把窗口滚动到末尾
messageShowArea->scrollToEnd();
// 4. 提示一个收到消息
Toast::showMessage("收到新消息!");
}
(2)服务器实现逻辑:
- 在界面上创建⼀个按钮,表示 “发送文本消息”,并实现信号槽:
void Widget::on_pushButton_clicked()
{
WebsocketServer* websocketServer = WebsocketServer::getInstance();
emit websocketServer->sendTextResp();
}
- 给 WebsocketServer 创建信号 sendTextResp
signals:
void sendTextResp();
- 实现处理函数:注意此处的 connect 要放到 connect(&websocketServer,
&QWebSocketServer::newConnection, this, [=] () { } ) 当中这样才能捕获到 socket 对象:
connect(this, &WebsocketServer::sendTextResp, this, [=]()
{
// 此处就可以捕获到 socket 对象, 从而可以通过 socket 对象给客户端返回数据.
if(socket == nullptr || !socket->isValid())
{
LOG() << "socket 对象无效!";
return;
}
// 构造响应数据
QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
bite_im::MessageInfo messageInfo = makeTextMessageInfo(this->messageIndex++, "2000", avatar);
bite_im::NotifyNewMessage notifyNewMessage;
notifyNewMessage.setMessageInfo(messageInfo);
bite_im::NotifyMessage notifyMessage;
notifyMessage.setNotifyEventId("");
notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::CHAT_MESSAGE_NOTIFY);
notifyMessage.setNewMessageInfo(notifyNewMessage);
// 序列化
QByteArray body = notifyMessage.serialize(&this->serializer);
// 发送消息给客户端
socket->sendBinaryMessage(body);
LOG() << "发送文本消息响应";
});
- 在 QWebSocket::disconnected 处理函数中,添加解除信号槽的逻辑:
// 针对这个 socket 对象, 进行剩余信号的处理
connect(socket, &QWebSocket::disconnected, this, [=]()
{
qDebug() << "[websocket] 连接断开!";
disconnect(this, &WebsocketServer::sendTextResp, this, nullptr);
}
此处的 disconnect 非常重要。否则如果客户端重复连接服务器,服务器就会尝试针对上次已经释放的socket 对象进行处理,就会使程序崩溃。
2. 个人信息详情逻辑
2.1 加载个人信息
(1)直接从 DataCenter 中读取数据:在 SelfInfoWidget 构造函数中, 添加数据加载:
// 11. 加载数据到界面上
model::DataCenter* dataCenter = model::DataCenter::getInstance();
model::UserInfo* myself = dataCenter->getMyself();
if (myself != nullptr)
{
// 就把个人信息, 显示到界面上
avatarBtn->setIcon(myself->avatar);
idLabel->setText(myself->userId);
nameLabel->setText(myself->nickname);
descLabel->setText(myself->description);
phoneLabel->setText(myself->phone);
}
2.2 修改昵称
(1)客户端发送请求:
- 在 SelfInfoWidget 构造函数连接信号槽并实现切换显示状态:
void SelfInfoWidget::initSingalSlot()
{
connect(nameModifyBtn, &QPushButton::clicked, this, [=]()
{
// 把当前的 nameLabel 和 nameModifyBtn 隐藏起来
nameLabel->hide();
nameModifyBtn->hide();
layout->removeWidget(nameLabel);
layout->removeWidget(nameModifyBtn);
// 把 nameEdit 和 nameSubmitBtn 显示出来
nameEdit->show();
nameSubmitBtn->show();
layout->addWidget(nameEdit, 1, 2);
layout->addWidget(nameSubmitBtn, 1, 3);
// 把输入框的内容进行设置.
nameEdit->setText(nameLabel->text());
});
connect(nameSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickNameSubmitBtn);
}
- 实现 SelfInfoWidget::clickNameSubmitBtn函数:
void SelfInfoWidget::clickNameSubmitBtn()
{
// 1. 从输入框中, 拿到修改后的昵称
const QString& nickname = nameEdit->text();
if(nickname.isEmpty())
{
return;
}
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::changeNicknameDone, this, &SelfInfoWidget::clickNameSubmitBtnDone, Qt::UniqueConnection);
dataCenter->changeNicknameAsync(nickname);
}
- 实现 DataCenter::changeNickNameAsync函数:
// 修改昵称
void DataCenter::changeNickNameAsync(const QString &nickName)
{
netClient.changeNickName(loginSessionId, nickName);
}
- 实现 NetClient::changeNickName函数和接口定义:
//----------------------------
//⽤⼾昵称修改
message SetUserNicknameReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
string nickname = 4;
}
message SetUserNicknameRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// 函数实现:
void NetClient::changeNickname(const QString& loginSessionId, const QString& nickname)
{
// 1. 通过 protobuf 构造请求 body
bite_im::SetUserNicknameReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setNickname(nickname);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[修改用户昵称] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", nickname=" << pbReq.nickname();
// 2. 发送 http 请求
QNetworkReply* resp = sendHttpRequest("/service/user/set_nickname", body);
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::SetUserNicknameRsp>(resp, &ok, &reason);
// b) 判定是否出错
if(!ok)
{
LOG() << "[修改用户昵称] 出错! reason=" << reason;
return;
}
// c) 把数据设置到 DataCenter 里面. 这里的处理和前面不太一样.
dataCenter->resetNickname(nickname);
// d) 发送信号, 通知调用者, 这里处理完毕
emit dataCenter->changeNicknameDone();
// e) 打印日志
LOG() << "[修改用户昵称] 处理响应完毕! requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::resetNickName
void DataCenter::resetNickName(const QString& nickName)
{
myself->nickname = nickName;
}
- 定义 DataCenter 信号:
void changeNickNameDone();
- 实现 SelfInfoWidget::clickNameSubmitBtnDone函数:
void SelfInfoWidget::clickNameSubmitBtnDone()
{
// 对界面控件进行切换. 把刚才输入框切换回 label, 把提交按钮切换回编辑按钮.
// 同时还需要把输入框中的本文设置为 label 中的文本.
layout->removeWidget(nameEdit);
nameEdit->hide();
layout->addWidget(nameLabel, 1, 2);
nameLabel->show();
nameLabel->setText(nameEdit->text());
layout->removeWidget(nameSubmitBtn);
nameSubmitBtn->hide();
layout->addWidget(nameModifyBtn, 1, 3);
nameModifyBtn->show();
}
- 修改 MessageShowArea 的 MessageItem::makeMessageItem,自动更新消息展示区的消息中显示的昵称:
// 6. 当用户修改了昵称的时候, 同步修改此处的用户昵称.
if(!isLeft)
{
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::changeNicknameDone, messageItem, [=]()
{
nameLabel->setText(dataCenter->getMyself()->nickname + " | " + message.time);
});
connect(dataCenter, &model::DataCenter::changeAvatarDone, messageItem, [=]()
{
UserInfo* myself = dataCenter->getMyself();
avatarBtn->setIcon(myself->avatar);
});
}
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_nickname", [=](const QHttpServerRequest& req)
{
return this->setNickName(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::setNickname(const QHttpServerRequest& req)
{
// 解析请求
bite_im::SetUserNicknameReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 修改用户昵称] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", nickname=" << pbReq.nickname();
// 构造响应
bite_im::SetUserNicknameRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
2.3 修改签名
(1)客户端发送请求:
- 在 SelfInfoWidget 构造函数连接信号槽:
void SelfInfoWidget::initSingalSlot()
{
connect(descModifyBtn, &QPushButton::clicked, this, [=]()
{
descLabel->hide();
descModifyBtn->hide();
layout->removeWidget(descLabel);
layout->removeWidget(descModifyBtn);
descEdit->show();
descSubmitBtn->show();
layout->addWidget(descEdit, 2, 2);
layout->addWidget(descSubmitBtn, 2, 3);
descEdit->setText(descLabel->text());
});
connect(descSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickDescSubmitBtn);
}
- 实现 SelfInfoWidget::clickSignatureSubmitBtn函数:
void SelfInfoWidget::clickDescSubmitBtn()
{
// 1. 从输入框中, 拿到修改后的签名内容
const QString& desc = descEdit->text();
if(desc.isEmpty())
{
return;
}
// 2. 发送网络请求
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::changeDescriptionDone, this, &SelfInfoWidget::chickDescSubmitBtnDone, Qt::UniqueConnection);
dataCenter->changeDescriptionAsync(desc);
}
- 实现 DataCenter::changeDescriptionAsync函数:
void DataCenter::changeDescriptionAsync(const QString &description)
{
netClient.changeDescription(loginSessionId, description);
}
- 实现 NetClient::changeDescription和接口定义:
//----------------------------
//⽤⼾签名修改
message SetUserDescriptionReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
string description = 4;
}
message SetUserDescriptionRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// 函数实现
void NetClient::changeDescription(const QString& loginSessionId, const QString& desc)
{
// 1. 通过 protobuf 构造请求 body
bite_im::SetUserDescriptionReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setDescription(desc);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[修改签名] 发送请求 requestId=" << pbReq.requestId() << ", loginSessisonId=" << pbReq.sessionId()
<< ", desc=" << pbReq.description();
QNetworkReply* resp = this->sendHttpRequest("/service/user/set_description", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::SetUserDescriptionRsp>(resp, &ok, &reason);
// b) 判定响应是否成功
if(!ok)
{
LOG() << "[修改签名] 响应失败! reason=" << reason;
return;
}
// c) 把得到的结果, 写入 DataCenter
dataCenter->resetDescription(desc);
// d) 发送信号, 通知修改完成
emit dataCenter->changeDescriptionDone();
// e) 打印日志
LOG() << "[修改签名] 响应完成! requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::resetDescription函数:
void DataCenter::resetDescription(const QString &description)
{
myself->description = description;
}
- 定义 DataCenter 信号:
void changeDescriptionDone();
- 实现 SelfInfoWidget::chickDescSubmitBtnDone函数:
void SelfInfoWidget::chickDescSubmitBtnDone()
{
// 切换界面.
// 把 label 替换回输入框, 把编辑按钮替换回修改按钮
layout->removeWidget(descEdit);
descEdit->hide();
layout->addWidget(descLabel, 2, 2);
descLabel->show();
descLabel->setText(descEdit->text());
layout->removeWidget(descSubmitBtn);
descSubmitBtn->hide();
layout->addWidget(descModifyBtn, 2, 3);
descModifyBtn->show();
}
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_description", [=](const QHttpServerRequest& req)
{
return this->setDesc(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::setDesc(const QHttpServerRequest& req)
{
// 解析请求
bite_im::SetUserDescriptionReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 修改用户签名] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", desc=" << pbReq.description();
// 构造响应
bite_im::SetUserDescriptionRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
2.4 修改电话 (1) - 发起短信验证码
(1)客户端发送请求:
- 在 SelfInfoWidget 构造函数连接信号槽:
void SelfInfoWidget::initSingalSlot()
{
connect(phoneModifyBtn, &QPushButton::clicked, this, [=]()
{
phoneLabel->hide();
phoneModifyBtn->hide();
layout->removeWidget(phoneLabel);
layout->removeWidget(phoneModifyBtn);
phoneEdit->show();
phoneSubmitBtn->show();
layout->addWidget(phoneEdit, 3, 2);
layout->addWidget(phoneSubmitBtn, 3, 3);
verifyCodeTag->show();
verifyCodeEdit->show();
getVerifyCodeBtn->show();
layout->addWidget(verifyCodeTag, 4, 1);
layout->addWidget(verifyCodeEdit, 4, 2);
layout->addWidget(getVerifyCodeBtn, 4, 3);
phoneEdit->setText(phoneLabel->text());
});
connect(getVerifyCodeBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickGetVerifyCodeBtn);
connect(phoneSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickPhoneSubmitBtn);
}
- 实现 clickGetVerifyCodeBtn发送验证码:
- 注意:
- 需要在 SelfInfoWidget 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
- 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改手机号请求的时候手机号码不能从输入框读取!)。
void SelfInfoWidget::clickGetVerifyCodeBtn()
{
// 1. 获取到输入框中的手机号码
const QString& phone = phoneLabel->text();
if(phone == nullptr)
{
return;
}
// 2. 给服务器发起请求.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::getVerifyCodeDone, this, [=]()
{
// 不需要做其他的处理, 只需要提示一下, 验证码已经发送
Toast::showMessage("短信验证码已经发送");
});
dataCenter->getVerifyCodeAsync(phone);
// 3. 把刚才发送请求的手机号码, 保存起来.
// 后续点击提交按钮, 修改电话, 修改的号码, 不从输入框读取, 而是读取这个变量.
this->phoneToChange = phone;
// 4. 禁用发送验证码按钮, 并给出倒计时
this->getVerifyCodeBtn->setEnabled(false);
leftTime = 30;
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [=]()
{
if(leftTime <= 1)
{
// 倒计时结束了
getVerifyCodeBtn->setEnabled(true);
getVerifyCodeBtn->setText("获取验证码");
timer->stop();
timer->deleteLater();
return;
}
--leftTime;
getVerifyCodeBtn->setText(QString::number(leftTime) + "s");
});
timer->start(1000);
}
- 实现 DataCenter::getVerifyCodeAsync函数:
void DataCenter::getVerifyCodeAsync(const QString& phone)
{
netClient.getVerifyCode(phone);
}
- 实现 NetClient::getVerifyCode函数和接口定义:
//----------------------------
//⼿机号验证码获取
message PhoneVerifyCodeReq {
string request_id = 1;
string phone_number = 2;
}
message PhoneVerifyCodeRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
string verify_code_id = 4;
}
// 函数实现
void NetClient::getVerifyCode(const QString& phone)
{
// 1. 通过 protobuf 构造请求 body
bite_im::PhoneVerifyCodeReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setPhoneNumber(phone);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[获取手机验证码] 发送请求 requestId=" << pbReq.requestId() << ", phone=" << phone;
// 2. 发送 HTTP 请求
QNetworkReply* resp = this->sendHttpRequest("/service/user/get_phone_verify_code", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::PhoneVerifyCodeRsp>(resp, &ok, &reason);
// b) 判定响应是否成功
if(!ok)
{
LOG() << "[获取手机验证码] 失败! reason=" << reason;
return;
}
// c) 保存数据到 DataCenter
dataCenter->resetVerifyCodeId(pbResp->verifyCodeId());
// d) 发送信号, 通知调用者
emit dataCenter->getVerifyCodeDone();
// e) 打印日志
LOG() << "[获取手机验证码] 响应完成 requestId=" << pbResp->requestId();
});
}
服务器会在 redis 中保存 verify_code_id 和 verify_code,给后续的验证提供支持。
(2)客户端处理响应:
- 实现 DataCenter::resetVerifyCodeId函数:
void DataCenter::resetVerifyCodeId(std::shared_ptr<bite_im::PhoneVerifyCodeRsp> resp)
{
this->currentVerifyCodeId = resp->verifyCodeId();
}
- 定义 DataCenter 信号:
void getVerifyCodeDone();
这个信号暂时不使用。会在后续的 “手机号登录” 功能中使用。
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/get_phone_verify_code", [=](const QHttpServerRequest& req)
{
return this->getPhoneVerifyCode(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::getPhoneVerifyCode(const QHttpServerRequest& req)
{
// 解析请求
bite_im::PhoneVerifyCodeReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 获取短信验证码] requestId=" << pbReq.requestId() << ", phone=" << pbReq.phoneNumber();
// 构造响应 body
bite_im::PhoneVerifyCodeRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
pbResp.setVerifyCodeId("testVerifyCodeId");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
2.5 修改电话 (2) - 修改电话逻辑
(1)客户端发送请求:
- 实现 SelfInfoWidget::clickPhoneSubmitBtn函数:
- 注意:
- 需要在 SelfInfoWidget 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
- 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改⼿机号请求的时候手机号码不能从输入框读取!)。
void SelfInfoWidget::clickPhoneSubmitBtn()
{
// 1. 先判定, 当前验证码是否已经收到.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
QString verifyCodeId = dataCenter->getVerifyCodeId();
if(verifyCodeId.isEmpty())
{
// 服务器这边还没有返回验证码响应呢
// LOG() << "服务器尚未返回验证码! 稍后重试!";
Toast::showMessage("服务器尚未返回响应, 稍后重试!");
return;
}
// 如果当前已经拿到 verifyCodeId, 就可以清空 DataCenter 中存储的值. 确保下次点击提交按钮的时候, 上述逻辑仍然有效
dataCenter->resetVerifyCodeId("");
// 2. 获取到用户输入的验证码
QString verifyCode = verifyCodeEdit->text();
if(verifyCode.isEmpty())
{
Toast::showMessage("验证码不能为空!");
return;
}
verifyCodeEdit->setText(""); // 获取到验证码之后, 就可以清空了.
// 3. 发送请求, 把当前验证码信息, 发送给服务器
connect(dataCenter, &model::DataCenter::changePhoneDone, this, &SelfInfoWidget::clickPhoneSubmitBtnDone, Qt::UniqueConnection);
dataCenter->changePhoneAsync(this->phoneToChange, verifyCodeId, verifyCode);
// 4. 让验证码按钮的倒计时停止. 把 leftTime 设为 1, 就可以停止了
leftTime = 1;
}
- 实现 DataCenter::changePhoneAsync函数
void DataCenter::changePhoneAsync(const QString &phone, const QString& verifyCodeId, const QString& verifyCode)
{
netClient.changePhone(loginSessionId, phone, verifyCodeId, verifyCode);
}
- 实现 NetClient::changePhone函数和接口定义:
//----------------------------
//⽤⼾⼿机修改
message SetUserPhoneNumberReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
string phone_number = 4;
string phone_verify_code_id = 5;
string phone_verify_code = 6;
}
message SetUserPhoneNumberRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// 函数实现
void NetClient::changePhone(const QString& loginSessionId, const QString& phone, const QString& verifyCodeId, const QString& verifyCode)
{
// 1. 通过 protobuf 构造请求 body
bite_im::SetUserPhoneNumberReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setPhoneNumber(phone);
pbReq.setPhoneVerifyCodeId(verifyCodeId);
pbReq.setPhoneVerifyCode(verifyCode);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[修改手机号] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", phone=" << pbReq.phoneNumber() << ", verifyCodeId=" << pbReq.phoneVerifyCodeId() << ", verifyCode=" << pbReq.phoneVerifyCode();
// 2. 发送 http 请求
QNetworkReply* resp = sendHttpRequest("/service/user/set_phone", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::SetUserPhoneNumberRsp>(resp, &ok, &reason);
// b) 判定响应是否正确
if(!ok)
{
LOG() << "[修改手机号] 响应失败! reason=" << reason;
return;
}
// c) 把结果记录到 DataCenter 中
dataCenter->resetPhone(phone);
// d) 发送信号, 通知调用者完成
emit dataCenter->changePhoneDone();
// e) 打印日志
LOG() << "[修改手机号] 相应完成 requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::resetPhone函数:
void DataCenter::resetPhone(const QString &phone)
{
myself->phone = phone;
}
- 定义 DataCenter 信号:
// 修改手机号完成
void changePhoneDone();
- 实现 SelfInfoWidget::clickPhoneSubmitBtnDone函数:
void SelfInfoWidget::clickPhoneSubmitBtnDone()
{
layout->removeWidget(verifyCodeTag);
layout->removeWidget(verifyCodeEdit);
layout->removeWidget(getVerifyCodeBtn);
layout->removeWidget(phoneEdit);
layout->removeWidget(phoneSubmitBtn);
verifyCodeTag->hide();
verifyCodeEdit->hide();
getVerifyCodeBtn->hide();
phoneEdit->hide();
phoneSubmitBtn->hide();
layout->addWidget(phoneLabel, 3, 2);
phoneLabel->show();
phoneLabel->setText(this->phoneToChange);
layout->addWidget(phoneModifyBtn, 3, 3);
phoneModifyBtn->show();
}
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_phone", [=](const QHttpServerRequest& req)
{
return this->setPhone(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::setPhone(const QHttpServerRequest& req)
{
// 解析请求
bite_im::SetUserPhoneNumberReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 修改手机号] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId() << ", phone=" << pbReq.phoneNumber()
<< ", verifyCodeId=" << pbReq.phoneVerifyCodeId() << ", verifyCode=" << pbReq.phoneVerifyCode();
// 构造响应 body
bite_im::SetUserPhoneNumberRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
2.6 修改头像
(1)客户端发送请求:
- 在 SelfInfoWidget 构造函数连接信号槽:
connect(avatarBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickAvatarBtn);
}
- 实现 SelfInfoWidget::clickAvatar函数:
void SelfInfoWidget::clickAvatarBtn()
{
// 1. 弹出对话框, 选择文件
QString filter = "Image Files (*.png *.jpg *.jpeg)";
QString imagePath = QFileDialog::getOpenFileName(this, "选择头像", QDir::homePath(), filter);
if(imagePath.isEmpty())
{
// 用户取消了
LOG() << "用户未选择任何头像";
return;
}
// 2. 根据路径, 读取到图片的内容.
QByteArray imageBytes = model::loadFileToByteArray(imagePath);
// 3. 发送请求, 修改头像
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::changeAvatarDone, this, &SelfInfoWidget::clickAvatarBtnDone, Qt::UniqueConnection);
dataCenter->changeAvatarAsync(imageBytes);
}
- 实现 DataCenter::changeAvatarAsync函数:
void DataCenter::changeAvatarAsync(const QByteArray &avatar)
{
netClient.changeAvatar(loginSessionId, avatar);
}
- 实现 NetClient::changeAvatar函数和接口定义:
//----------------------------
//⽤⼾头像修改
message SetUserAvatarReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
bytes avatar = 4;
}
message SetUserAvatarRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// 函数实现
void NetClient::changeAvatar(const QString& loginSessionId, const QByteArray& avatar)
{
// 1. 通过 protobuf 构造请求 body
bite_im::SetUserAvatarReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setAvatar(avatar);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[修改头像] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();
// 2. 发送 http 请求
QNetworkReply* resp = sendHttpRequest("/service/user/set_avatar", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::SetUserAvatarRsp>(resp, &ok, &reason);
// b) 判定响应结果是否正确
if(!ok)
{
LOG() << "[修改头像] 响应出错! reason=" << reason;
return;
}
// c) 把数据设置到 DataCenter 中
dataCenter->resetAvatar(avatar);
// d) 发送信号
emit dataCenter->changeAvatarDone();
// e) 打印日志
LOG() << "[修改头像] 响应完成 requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::resetAvatar函数:
void DataCenter::resetAvatar(const QByteArray &avatar)
{
myself->avatar = makeIcon(avatar);
}
- 定义 DataCenter 信号:
void changeAvatarDone();
- 实现 SelfInfoWidget::clickAvatarDone函数来修改用户详情界面的头像:
void SelfInfoWidget::clickAvatarBtnDone()
{
// 把设置的头像, 更新到界面上.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
avatarBtn->setIcon(dataCenter->getMyself()->avatar);
}
- 修改主界面的头像。在 MainWidget::initData 中处理 changeAvatarDone 信号:
connect(dataCenter, &DataCenter::changeAvatarDone, this, [=]()
{
UserInfo* myself = dataCenter->getMyself();
userAvatar->setIcon(myself->avatar);
});
- 修改消息显示区的头像。在 ShowMessageArea 的MessageItem::makeMessageItem 中处理changeAvatarDone 信号。和修改名字放在⼀起:
// 6. 当用户修改了昵称的时候, 同步修改此处的用户昵称.
if(!isLeft)
{
model::DataCenter* dataCenter = model::DataCenter::getInstance();
connect(dataCenter, &model::DataCenter::changeNicknameDone, messageItem, [=]()
{
nameLabel->setText(dataCenter->getMyself()->nickname + " | " + message.time);
});
connect(dataCenter, &model::DataCenter::changeAvatarDone, messageItem, [=]()
{
UserInfo* myself = dataCenter->getMyself();
avatarBtn->setIcon(myself->avatar);
});
}
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_avatar", [=](const QHttpServerRequest& req)
{
return this->setAvatar(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::setAvatar(const QHttpServerRequest& req)
{
// 解析请求
bite_im::SetUserAvatarReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 修改头像] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();
// 构造响应 body
bite_im::SetUserAvatarRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
3. 用户详细信息界面逻辑
3.1 获取指定用户的信息
(1)客户端处理逻辑:
- 从对应的 Message 对象中获取到用户详细信息。不需要从服务器获取数据。在 UserInfoWidget 的构造函数中,添加逻辑获取数据:
// 9. 初始化按钮的禁用关系
// 判定依据就是拿着当前用户的 userId, 在 DataCenter 的好友列表中, 查询即可.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
auto* myFriend = dataCenter->findFriendById(this->userInfo.userId);
if(myFriend == nullptr)
{
// 不是好友
sendMessageBtn->setEnabled(false);
deleteFriendBtn->setEnabled(false);
}
else
{
// 是好友
applyBtn->setEnabled(false);
}
- 实现 DataCenter::findFriendById函数:
UserInfo* DataCenter::findFriendById(const QString& userId)
{
if(friendList == nullptr)
{
return nullptr;
}
for(auto& f : *friendList)
{
if(f.userId == userId)
{
return &f;
}
}
return nullptr;
}
3.2 点击 “发送消息” 打开对应会话
(1)客户端处理逻辑:直接调用之前的逻辑即可。在选中好友时有类似的逻辑,直接调用即可。不需要和服务器交互:
- 在 UserInfoWidget 构造函数中,连接信号槽:
void UserInfoWidget::initSignalSlot()
{
connect(sendMessageBtn, &QPushButton::clicked, this, [=]()
{
// 拿到主窗口指针, 通过主窗口中, 前面实现的 切换到会话 这样的功能, 直接调用即可.
MainWidget* mainWidget = MainWidget::getInstance();
mainWidget->switchSession(userInfo.userId);
// 把本窗口关闭掉
this->close();
});
}
3.3 删除好友
(1)客户端发送请求:
- 在 UserInfoWidget 构造函数中连接信号槽
connect(deleteFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickDeleteFriendBtn);
- 实现 UserInfoWidget::clickDeleteFriendBtn函数:
void UserInfoWidget::clickDeleteFriendBtn()
{
// 1. 弹出对话框, 让用户确认是否要真的删除
auto result = QMessageBox::warning(this, "确认删除", "确认删除当前好友?", QMessageBox::Ok | QMessageBox::Cancel);
if (result != QMessageBox::Ok) {
LOG() << "删除好友取消";
return;
}
// 2. 发送网络请求, 实现删除好友功能.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
dataCenter->deleteFriendAsync(userInfo.userId);
// 3. 关闭本窗口
this->close();
}
- 实现 DataCenter::deleteFriendAsync函数:
void DataCenter::deleteFriendAsync(const QString &userId)
{
netClient.deleteFriend(loginSessionId, userId);
}
- 实现 NetClient::deleteFriend函数和接口定义:
//--------------------------------------
//好友删除
message FriendRemoveReq {
string request_id = 1;
optional string user_id = 2;
optional string session_id = 3;
string peer_id = 4;
}
message FriendRemoveRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
}
// 函数实现
void NetClient::deleteFriend(const QString& loginSessionId, const QString& userId)
{
// 1. 构造请求 body
bite_im::FriendRemoveReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setPeerId(userId);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[删除好友] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", peerId=" << pbReq.peerId();
// 2. 发送 HTTP 请求
QNetworkReply* resp = this->sendHttpRequest("/service/friend/remove_friend", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::FriendRemoveRsp>(resp, &ok, &reason);
// b) 判定响应结果
if(!ok)
{
LOG() << "[删除好友] 响应失败! reason=" << reason;
return;
}
// c) 把结果写入 DataCenter. 把该删除的用户, 从好友列表中, 删除掉.
dataCenter->removeFriend(userId);
// d) 发送信号, 通知调用者当前好友删除完毕.
emit dataCenter->deleteFriendDone();
// e) 打印日志
LOG() << "[删除好友] 响应完成 requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::removeFriend函数删除好友和会话:
void DataCenter::removeFriend(const QString& userId)
{
// 遍历 friendList, 删除其中匹配的元素即可.
if(friendList == nullptr || chatSessionList == nullptr)
{
return;
}
friendList->removeIf([=](const UserInfo& userInfo)
{
// 返回 true 要删除的元素. false 直接跳过不删除.
return userInfo.userId == userId;
});
// 还要考虑会话列表.
// 没有好友, 保留会话, 后续往会话里发消息啥的, 就都不好处理了.
// 删除会话操作, 客户端和服务器分别都会删除.
chatSessionList->removeIf([=](const ChatSessionInfo& chatSessionInfo)
{
if(chatSessionInfo.userId == "")
{
// 群聊, 不受影响
return false;
}
if (chatSessionInfo.userId == userId)
{
// 当前这个会话要删除了, 并且要删除的会话又是选中的会话, 才真正清空当前会话
// 此处如果删除的会话, 正好是用户正在选中的会话, 此时就需要把当前选中会话的内容(标题和消息列表)都清空
if(chatSessionInfo.chatSessionId == this->currentChatSessionId)
{
emit this->clearCurrentSession();
}
return true;
}
return false;
});
}
- 定义 DataCenter 信号:
// 删除好友完成
void deleteFriendDone(const QString& userId);
void clearCurrentSession();
- 处理 deleteFriendDone 信号和 clearCurrentSession 信号:
- 在MainWidget::initData 中更新界面显示:
- 更新会话列表。
- 更新好友列表。
- 更新当前会话的消息列表。
/
/// 处理删除好友
/
connect(dataCenter, &DataCenter::deleteFriendDone, this, [=]()
{
// 更新会话列表和好友列表
this->updateFriendList();
this->updateChatSessionList();
LOG() << "删除好友完成";
});
connect(dataCenter, &DataCenter::clearCurrentSession, this, [=]()
{
sessionTitleLabel->setText("");
messageShowArea->clear();
dataCenter->setCurrentChatSessionId("");
LOG() << "清空当前会话完成";
});
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/friend/remove_friend", [=](const QHttpServerRequest& req)
{
return this->removeFriend(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::removeFriend(const QHttpServerRequest& req)
{
// 解析请求
bite_im::FriendRemoveReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 删除好友] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", peerId=" << pbReq.peerId();
// 构造响应 body
bite_im::FriendRemoveRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
3.4 删除好友推送处理
当A删除B好友时,B也会收到⼀个websocket的推送信息。
(1)客户端处理推送:
- 实现 NetClient::handleWsRemoveFriend函数。此处 deleteFriendDone 信号已经被处理过了:
void NetClient::handleWsRemoveFriend(const QString& userId)
{
// 1. 删除数据. DataCenter 好友列表的数据
dataCenter->removeFriend(userId);
// 2. 通知界面变化. 更新 好友列表 / 会话列表
emit dataCenter->deleteFriendDone();
}
(2)服务器实现逻辑:
- 在界面上添加按钮 “发送删除好友推送”,并实现槽函数:
void Widget::on_pushButton_9_clicked()
{
WebsocketServer* websocketServer = WebsocketServer::getInstance();
emit websocketServer->sendFriendRemove();
}
- 定义 WebsocketServer 信号并处理:
// 定义信号
void sendFriendRemove();
// 实现
connect(this, &WebsocketServer::sendFriendRemove, this, [=]()
{
if(socket == nullptr || !socket->isValid())
{
LOG() << "socket 对象无效";
return;
}
bite_im::NotifyMessage notifyMessage;
notifyMessage.setNotifyEventId("");
notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_REMOVE_NOTIFY);
bite_im::NotifyFriendRemove notifyFriendRemove;
notifyFriendRemove.setUserId("1000");
notifyMessage.setFriendRemove(notifyFriendRemove);
QByteArray body = notifyMessage.serialize(&serializer);
socket->sendBinaryMessage(body);
LOG() << "通知对方好友被删除 userId=1000";
});
- 断开连接时断开信号槽在 connect(socket, &QWebSocket::disconnected, this, [=] () {})中调用:
disconnect(this, &WebsocketServer::sendFriendRemove, this, nullptr);
3.5 发送好友申请
点击 "申请好友按钮"触发该效果。
(1)客户端发送请求:
- 在 UserInfoWidget 构造函数中连接信号槽:
connect(addFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickAddFriendBtn);
- 实现 UserInfoWidget::clickAddFriendBtn函数:
void UserInfoWidget::clickApplyBtn()
{
// 1. 发送好友申请
model::DataCenter* dataCenter = model::DataCenter::getInstance();
dataCenter->addFriendApplyAsync(userInfo.userId);
// 2. 关闭窗口
this->close();
}
- 实现 DataCenter::addFriendApplyAsync函数:
void DataCenter::addFriendApplyAsync(const QString &userId)
{
netClient.addFriend(loginSessionId, userId);
}
- 实现 NetClient::addFriendApply函数和接口定义:
//添加好友--发送好友申请
message FriendAddReq {
string request_id = 1;
optional string session_id = 2;
optional string user_id = 3;//申请⼈id
string respondent_id = 4;//被申请⼈id
}
message FriendAddRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
string notify_event_id = 4;//通知事件id
}
// 函数实现
void NetClient::addFriendApply(const QString &loginSessionId, const QString &userId)
{
// 1. 构造请求 body
bite_im::FriendAddReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setRespondentId(userId);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[添加好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", userId=" << userId;
// 2. 发送 HTTP 请求
QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_apply", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);
// b) 判定响应是否正确
if(!ok)
{
LOG() << "[添加好友申请] 响应失败! reason=" << reason;
return;
}
// c) 记录结果到 DataCenter, 此处不需要记录任何数据
// d) 发送信号, 通知调用者
emit dataCenter->addFriendApplyDone();
// e) 打印日志
LOG() << "[添加好友申请] 响应完毕 requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 定义 DataCenter 信号:
void addFriendApplyDone();
- 在 MainWidget::initData 中, 处理 addFriendApplyDone 信号.
connect(dataCenter, &DataCenter::addFriendApplyDone, this, [=]()
{
Toast::showMessage("好友申请已发送!");
});
(3)服务器实现逻辑:
- 注册路由
httpServer.route("/service/friend/add_friend_apply", [=](const QHttpServerRequest& req)
{
return this->addFriendApply(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::addFriendApply(const QHttpServerRequest& req)
{
// 解析请求
bite_im::FriendAddReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 添加好友申请] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", userId=" << pbReq.respondentId();
// 构造响应 body
bite_im::FriendAddRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
pbResp.setNotifyEventId("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
4. 主界面逻辑 (2)
4.1 收到好友申请
(1)客户端处理推送:通过 websocket 收到 “好友申请通知”,并进行处理
- 实现 NetClient::handleWsAddFriendApplyReq函数:
void NetClient::handleWsAddFriendApply(const model::UserInfo &userInfo)
{
// 1. DataCenter 中有一个 好友申请列表. 需要把这个数据添加到好友申请列表中
QList<model::UserInfo>* applyList = dataCenter->getApplyList();
if(applyList == nullptr)
{
LOG() << "客户端没有加载到好友申请列表!";
return;
}
// 把新的元素放到列表前面
applyList->push_front(userInfo);
// 2. 通知界面更新.
emit dataCenter->receiveFriendApplyDone();
}
- 实现 DataCenter::getApplyList函数:
QList<UserInfo> *DataCenter::getApplyList()
{
return applyList;
}
- 定义 DataCenter 信号:
void receiveFriendApplyDone();
- 在MainWidget::initSignalSlot中处理上述信号:
connect(dataCenter, &DataCenter::receiveFriendApplyDone, this, [=]() {
Toast::showMessage("收到新的好友申请!");
// 如果当前选中的标签⻚正好是好友申请, 则更新申请列表
updateApplyList();
});
(2)服务器实现逻辑:
- 在界面上创建⼀个按钮,“发送好友申请推送”,并实现槽函数:
void Widget::on_pushButton_2_clicked()
{
WebsocketServer* websocketServer = WebsocketServer::getInstance();
emit websocketServer->sendFriendApply();
}
- 在 websocket 逻辑中处理上述信号:
connect(this, &WebsocketServer::sendAddFriendApply, this, [=]()
{
if(socket == nullptr || !socket->isValid())
{
LOG() << "socket 对象无效";
return;
}
bite_im::NotifyMessage notifyMessage;
notifyMessage.setNotifyEventId("");
notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_APPLY_NOTIFY);
QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
bite_im::UserInfo userInfo = makeUserInfo(100, avatar);
bite_im::NotifyFriendAddApply friendAddApply;
friendAddApply.setUserInfo(userInfo);
notifyMessage.setFriendAddApply(friendAddApply);
QByteArray body = notifyMessage.serialize(&serializer);
socket->sendBinaryMessage(body);
LOG() << "通知对方好友申请数据";
});
- 在断开 websocket 连接时断开上述信号槽:
disconnect(this, &WebsocketServer::sendFriendApply, this, nullptr);
4.2 同意好友申请
点击 “同意” 按钮触发下列逻辑。
(1)客户端发送请求:
- 在 ApplyItem::ApplyItem 的构造函数中连接信号槽:
connect(acceptBtn, &QPushButton::clicked, this, &ApplyItem::acceptFriend);
- 实现 ApplyItem::acceptFriend
void ApplyItem::acceptFriendApply()
{
// 发送网络请求, 告知服务器, 同意了.
model::DataCenter* dataCenter = model::DataCenter::getInstance();
// 同意谁的好友申请了.
// 针对这个操作, 信号处理, 是需要更新好友列表以及好友申请列表. 直接在主窗口中处理更合适的.
dataCenter->acceptFriendApplyAsync(this->userId);
}
- 实现 DataCenter::acceptFriendApplyAsync
void DataCenter::acceptFriendApplyAsync(const QString &userId)
{
netClient.acceptFriendApply(loginSessionId, userId);
}
- 实现 NetClient::acceptFriendApply函数和接口定义:
//好友申请的处理
message FriendAddProcessReq {
string request_id = 1;
string notify_event_id = 2;//通知事件id
bool agree = 3;//是否同意好友申请
string apply_user_id = 4; //申请⼈的⽤⼾id
optional string session_id = 5;
optional string user_id = 6;
}
// +++++++++++++++++++++++++++++++++
message FriendAddProcessRsp {
string request_id = 1;
bool success = 2;
string errmsg = 3;
optional string new_session_id = 4; // 同意后会创建会话,向⽹关返回会话信息,⽤于通知双⽅会话的建⽴,这个字段客⼾端不需要关注
}
// 函数实现
void NetClient::acceptFriendApply(const QString &loginSessionId, const QString &userId)
{
// 1. 构造请求 body
bite_im::FriendAddProcessReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setAgree(true);
pbReq.setApplyUserId(userId);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[同意好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", userId=" << pbReq.applyUserId();
// 2. 发送 HTTP 请求
QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_process", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);
// b) 判定响应结果是否正确
if(!ok)
{
LOG() << "[同意好友申请] 处理失败! reason=" << reason;
return;
}
// c) 此处做一个好友列表的更新
// 一个是把数据从好友申请列表中, 删除掉
// 另一个是把好友申请列表中的这个数据添加到好友列表中.
model::UserInfo applyUser = dataCenter->removeFromApplyList(userId);
QList<model::UserInfo>* friendList = dataCenter->getFriendList();
friendList->push_front(applyUser);
// d) 发送信号, 通知界面进行更新
emit dataCenter->acceptFriendApplyDone();
// e) 打印日志
LOG() << "[同意好友申请] 响应完成! requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::removeFromApplyList函数:
UserInfo DataCenter::removeFromApplyList(const QString& userId)
{
if(applyList == nullptr)
{
return UserInfo();
}
for(auto iter = applyList->begin(); iter != applyList->end(); ++iter)
{
if(iter->userId == userId)
{
// 复制以下这个要删除的对象. 以备进行返回.
UserInfo toDelete = *iter;
applyList->erase(iter);
return toDelete;
}
}
return UserInfo();
}
- 定义 DataCenter 信号:
// 发送同意好友申请完成
void acceptFriendApplyDone(const QString& userId, const QString& reason);
- 处理acceptFriendApplyDone信号:
connect(dataCenter, &DataCenter::acceptFriendApplyDone, this, [=]()
{
this->updateApplyList();
this->updateFriendList();
Toast::showMessage("好友已经添加完成");
});
(3)服务器实现逻辑:
- 注册路由
httpServer.route("/service/friend/add_friend_process", [=](const QHttpServerRequest& req)
{
return this->addFriendProcess(req);
});
- 处理函数实现:
QHttpServerResponse HttpServer::addFriendProcess(const QHttpServerRequest &req)
{
// 解析请求
bite_im::FriendAddProcessReq pbReq;
pbReq.deserialize(&serializer, req.body());
LOG() << "[REQ 添加好友申请处理] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", applyUserId=" << pbReq.applyUserId() << ", agree=" << pbReq.agree();
// 构造响应 body
bite_im::FriendAddProcessRsp pbResp;
pbResp.setRequestId(pbReq.requestId());
pbResp.setSuccess(true);
pbResp.setErrmsg("");
pbResp.setNewSessionId("");
QByteArray body = pbResp.serialize(&serializer);
// 构造 HTTP 响应
QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
resp.setHeader("Content-Type", "application/x-protobuf");
return resp;
}
4.3 拒绝好友申请
(1)客户端发送请求:
- 在 ApplyItem::ApplyItem 的构造函数中连接信号槽
connect(rejectBtn, &QPushButton::clicked, this, &ApplyItem::rejectFriendApply);
- 实现 ApplyItem::rejectFriendApply函数:
void ApplyItem::rejectFriendApply()
{
model::DataCenter* dataCenter = model::DataCenter::getInstance();
dataCenter->rejectFriendApplyAsync(this->userId);
}
- 实现 DataCenter::rejectFriendApplyAsync
void DataCenter::rejectFriendApplyAsync(const QString &userId)
{
netClient.rejectFriendApply(loginSessionId, userId);
}
- 实现 NetClient::rejectFriendApply并且接口定义 (和刚才的同意好友申请是同⼀套接口):
void NetClient::rejectFriendApply(const QString &loginSessionId, const QString &userId)
{
// 1. 构造请求 body
bite_im::FriendAddProcessReq pbReq;
pbReq.setRequestId(makeRequestId());
pbReq.setSessionId(loginSessionId);
pbReq.setAgree(false);
pbReq.setApplyUserId(userId);
QByteArray body = pbReq.serialize(&serializer);
LOG() << "[拒绝好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
<< ", userId=" << pbReq.applyUserId();
// 2. 发送 HTTP 请求
QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_process", body);
// 3. 处理响应
connect(resp, &QNetworkReply::finished, this, [=]()
{
// a) 解析响应
bool ok = false;
QString reason;
auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);
// b) 判定响应结果是否正确
if(!ok)
{
LOG() << "[拒绝好友申请] 处理失败! reason=" << reason;
return;
}
// c) 此处不需要更新好友列表, 需要把这个记录从好友申请列表中, 删除掉.
dataCenter->removeFromApplyList(userId);
// d) 发送信号, 通知界面进行更新
emit dataCenter->rejectFriendApplyDone();
// e) 打印日志
LOG() << "[拒绝好友申请] 响应完成! requestId=" << pbResp->requestId();
});
}
(2)客户端处理响应:
- 实现 DataCenter::removeFromApplyList函数。这个函数上面 “同意好友申请” 中已经实现过,直接调用即可
- 定义 DataCenter 信号
// 发送拒绝好友申请完成
void rejectFriendApplyDone(const QString& userId, const QString& reason);
- 处理rejectFriendApplyDone信号:
connect(dataCenter, &DataCenter::rejectFriendApplyDone, this, [=]()
{
// 需要更新好友申请列表. 刚才拒绝的这一项, 是需要删除掉的.
this->updateApplyList();
Toast::showMessage("好友申请已经拒绝");
});
(3)服务器实现逻辑:此处的逻辑和 “同意好友申请” 的服务器逻辑是⼀致的。
4.4 获取到好友申请处理结果
服务器通过 websocket 推送数据。
(1)客户端处理推送:
- 实现 NetClient::handleWsAddFriendProcess函数:
void NetClient::handleWsAddFriendProcess(const model::UserInfo &userInfo, bool agree)
{
if(agree)
{
// 对方同意了你的好友申请
QList<model::UserInfo>* friendList = dataCenter->getFriendList();
if(friendList == nullptr)
{
LOG() << "客户端没有加载好友列表";
return;
}
friendList->push_front(userInfo);
// 同时也更新一下界面
emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
}
else
{
// 对方未同意好友申请
emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
}
}
- 定义 DataCenter 信号:
// 一个信号处理是否同意申请信息
void receiveFriendProcessDone(const QString& nickname, bool agree);
- 在 MainWidget::initData 中处理上述信号:
/
/// 处理好友申请结果的推送数据
/
connect(dataCenter, &DataCenter::receiveFriendProcessDone, this, [=](const QString& nickname, bool agree)
{
if(agree)
{
// 同意
this->updateFriendList();
Toast::showMessage(nickname + " 已经同意了你的好友申请");
}
else
{
// 拒绝
Toast::showMessage(nickname + " 已经拒绝了你的好友申请");
}
});
(2)服务器实现逻辑:
- 创建按钮"发送好友通过通知" 和 "发送好友拒绝通知"并创建槽函数:
void Widget::on_pushButton_3_clicked()
{
WebsocketServer* websocketServer = WebsocketServer::getInstance();
emit websocketServer->sendFriendProcess(true);
}
void Widget::on_pushButton_4_clicked()
{
WebsocketServer* websocketServer = WebsocketServer::getInstance();
emit websocketServer->sendFriendProcess(false);
}
- 定义上述信号:
void sendFriendProcess(bool);
- 在 websocket 逻辑中,处理上述信号:
connect(this, &WebsocketServer::sendAddFriendProcess, this, [=](const bool agree)
{
if(socket == nullptr || !socket->isValid())
{
LOG() << "socket 对象无效!";
return;
}
bite_im::NotifyMessage notifyMessage;
notifyMessage.setNotifyEventId("");
notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_PROCESS_NOTIFY);
QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
bite_im::UserInfo userInfo = makeUserInfo(100, avatar);
bite_im::NotifyFriendAddProcess friendAddProcess;
friendAddProcess.setUserInfo(userInfo);
friendAddProcess.setAgree(agree);
notifyMessage.setFriendProcessResult(friendAddProcess);
QByteArray body = notifyMessage.serialize(&serializer);
socket->sendBinaryMessage(body);
LOG() << "通知好友申请的处理结果 userId=" << userInfo.userId() << ", agree=" << agree;
});
- 在 websocket 断开连接的逻辑中,断开上述信号槽:
disconnect(this, &WebsocketServer::sendFriendProcess, this, nullptr);
5. 小结
(1)以上的所有内容都是实现前后端交互接口,实现的格式基本上是一致的。
(2)剩下的前后端交互接口的实现见博客:https://blog.csdn.net/m0_65558082/article/details/143828479?spm=1001.2014.3001.5502。
客户端整体代码链接:https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client。