原型void ChatWindow::sendTextMessage()
执行流程
- 读取并规范文本
- 校验非空
- 调用 requestSendChat
- 添加本地消息气泡
- 清空输入
工程说明
发送与渲染分离,服务端历史同步再通过消息键去重。
关联接口
ClientLogic::requestSendChatChatWindow::appendMessageToUI
查看完整实现
chatwindow.cpp
void ChatWindow::sendTextMessage()
{
if (!m_logic || !msgEdit) {
return;
}
QString msg = msgEdit->text().trimmed();
if (msg.isEmpty()) {
return;
}
qint64 now = QDateTime::currentSecsSinceEpoch();
QString displaySender = "我";
if (!parseCustomGamePayload(msg).isEmpty()) {
QString content = normalizeCustomGameInviteContent(msg);
m_logic->requestSendChat(m_targetId, content);
appendCustomGameInviteMessage(displaySender, content, true, now);
msgEdit->clear();
msgEdit->setFocus();
return;
}
m_logic->requestSendChat(m_targetId, msg);
appendMessageToUI(displaySender, msg, true, MessageBubbleWidget::Text, now);
msgEdit->clear();
msgEdit->setFocus();
}
原型void ChatWindow::chooseAndUploadFile()
执行流程
- 选择文件
- 校验路径与大小
- 创建上传器
- 设置 token/session/scene
- 监听进度
- 完成后发送聊天元数据
工程说明
文件本体和消息通知分别走 TCP 与 UDP。
关联接口
TcpFileUploader::startUploadClientLogic::requestSendChatChatWindow::appendMessageToUI
查看完整实现
chatwindow.cpp
void ChatWindow::chooseAndUploadFile()
{
if (!m_logic) {
return;
}
QString filePath = QFileDialog::getOpenFileName(
this,
"选择要发送的文件",
"",
"游戏配置/文件 (*.json *.p2pgameconfig *.p2pgamepkg *.p2psave *.png *.jpg *.jpeg *.rar);;所有文件 (*.*)"
);
if (filePath.isEmpty()) {
return;
}
FilePreviewDialog previewDlg(filePath, this);
if (previewDlg.exec() != QDialog::Accepted) {
return;
}
QString finalPath = previewDlg.getFilePath();
if (finalPath.isEmpty()) {
return;
}
QFileInfo fileInfo(finalPath);
if (!fileInfo.exists() || !fileInfo.isFile()) {
QMessageBox::warning(this, "文件不存在", "选择的文件不存在或不是普通文件。");
return;
}
QString fileTypeHint = detectUploadFileTypeHint(finalPath);
MessageBubbleWidget::MessageType bubbleType = MessageBubbleWidget::File;
if (fileTypeHint.isEmpty() &&
(finalPath.endsWith(".png", Qt::CaseInsensitive) ||
finalPath.endsWith(".jpg", Qt::CaseInsensitive) ||
finalPath.endsWith(".jpeg", Qt::CaseInsensitive))) {
bubbleType = MessageBubbleWidget::Image;
}
if (fileTypeHint == "custom_game_config") {
appendCustomGameConfigMessage("我",
finalPath,
true,
QDateTime::currentSecsSinceEpoch(),
true);
} else {
appendMessageToUI("我",
bubbleType == MessageBubbleWidget::File ? fileInfo.fileName() : finalPath,
true,
bubbleType,
QDateTime::currentSecsSinceEpoch());
}
TcpFileUploader* uploader = new TcpFileUploader(
FILE_SERVER_HOST,
FILE_SERVER_PORT,
m_logic->getCurrentUser(),
m_logic->getAuthToken(),
m_targetId,
finalPath,
this
);
uploader->setSessionId(m_logic->getSessionId());
uploader->setTransferScene("private");
if (!fileTypeHint.isEmpty()) {
uploader->setFileTypeHint(fileTypeHint);
}
connect(uploader, &TcpFileUploader::uploadProgress,
this,
[=](qint64 sent, qint64 total) {
if (total > 0) {
qDebug() << QString("上传进度: %1%").arg((sent * 100) / total);
}
});
connect(uploader, &TcpFileUploader::uploadFinished,
this,
[=](bool success, QString msg) {
uploader->deleteLater();
if (!success) {
appendMessageToUI("系统",
"【传输失败】 " + msg,
false,
MessageBubbleWidget::Text,
QDateTime::currentSecsSinceEpoch());
return;
}
qDebug() << "[File] upload finished";
});
uploader->startUpload();
}
原型void GroupChatWindow::sendGroupFile()
执行流程
- 选择文件
- 创建群聊上传器
- 写入 group_id 与 scene
- 显示上传进度
- 发送 group_chat 文件消息
关联接口
TcpFileUploader::setTransferSceneGroupChatWindow::appendGroupFileMessageBusinessHandler::handleGroupChat
查看完整实现
groupchatwindow.cpp
void GroupChatWindow::sendGroupFile()
{
if (!m_logic) {
QMessageBox::warning(this, "群文件", "客户端业务对象不可用,无法发送文件。");
return;
}
if (m_logic->getCurrentUser().isEmpty() ||
m_logic->getAuthToken().isEmpty() ||
m_logic->getSessionId().isEmpty()) {
QMessageBox::warning(this,
"登录态失效",
"当前登录会话已失效,请重新登录后再发送群文件。");
return;
}
QString filePath = QFileDialog::getOpenFileName(
this,
"选择要发送到群聊的文件",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
);
if (filePath.trimmed().isEmpty()) {
return;
}
QFileInfo info(filePath);
if (!info.exists() || !info.isFile()) {
QMessageBox::warning(this, "群文件", "请选择一个有效的本地文件。");
return;
}
if (info.size() <= 0) {
QMessageBox::warning(this, "群文件", "不能发送空文件。");
return;
}
TcpFileUploader* uploader = new TcpFileUploader(
tcpServerIp(m_logic),
TCP_FILE_PORT,
m_logic->getCurrentUser(),
m_logic->getAuthToken(),
QString::number(m_groupId),
filePath,
this
);
uploader->setSessionId(m_logic->getSessionId());
uploader->setTransferScene("group");
QString lowerName = info.fileName().toLower();
if (lowerName.endsWith(".p2pgameconfig") ||
lowerName.endsWith("_config.json") ||
lowerName == "room_config.json") {
uploader->setFileTypeHint("custom_game_config");
}
sendFileBtn->setEnabled(false);
statusLabel->setText(QString("群ID:%1 角色:%2 正在上传群文件:%3")
.arg(m_groupId)
.arg(m_myRole)
.arg(info.fileName()));
connect(uploader, &TcpFileUploader::uploadProgress,
this,
[=](qint64 sent, qint64 total) {
statusLabel->setText(QString("群ID:%1 角色:%2 群文件上传中:%3 / %4")
.arg(m_groupId)
.arg(m_myRole)
.arg(readableFileSize(sent))
.arg(readableFileSize(total)));
});
connect(uploader, &TcpFileUploader::uploadFinished,
this,
[=](bool success, const QString& msg) {
sendFileBtn->setEnabled(true);
statusLabel->setText(QString("群ID:%1 角色:%2").arg(m_groupId).arg(m_myRole));
if (!success) {
QMessageBox::warning(this, "群文件上传失败", msg);
return;
}
appendTextMessage("系统",
"群文件已上传,等待服务器广播文件消息。",
false,
QDateTime::currentSecsSinceEpoch());
});
uploader->startUpload();
}
原型void BusinessHandler::handleGroupChat(const std::string& json_str, struct sockaddr_in client_addr)
调用时机
dispatchTask 识别 group_chat 时调用。
参数
| 参数 | 说明 |
|---|
json_str | 群聊消息 JSON |
client_addr | 发送者地址 |
执行流程
- 解析消息
- 统一鉴权
- 校验 group_members
- 生成/读取 msg_id
- 写入 MySQL
- 遍历在线成员转发
关联接口
DBManager::isGroupMemberDBManager::saveGroupMessageGroupChatWindow::markMessageSeen
查看完整实现
business_handler.cpp
void BusinessHandler::handleGroupChat(const std::string& json_str,
struct sockaddr_in client_addr)
{
try {
json j = json::parse(json_str);
std::string from;
if (!checkAuth(j, client_addr, from)) {
return;
}
json resp;
resp["cmd"] = "group_chat_resp";
uint64_t groupId = 0;
if (!getUint64Field(j, "group_id", groupId)) {
resp["success"] = false;
resp["msg"] = "group_id 非法或缺失";
net_server->sendData(resp.dump(), client_addr);
return;
}
if (!j.contains("content") || !j["content"].is_string()) {
resp["success"] = false;
resp["msg"] = "缺少 content";
net_server->sendData(resp.dump(), client_addr);
return;
}
std::string content = j["content"].get<std::string>();
std::string msgType = getStringField(j, "msg_type", "text");
if (content.empty()) {
resp["success"] = false;
resp["msg"] = "消息不能为空";
net_server->sendData(resp.dump(), client_addr);
return;
}
if (content.size() > 8192) {
resp["success"] = false;
resp["msg"] = "群消息过长";
net_server->sendData(resp.dump(), client_addr);
return;
}
if (msgType.empty() || msgType.size() > 32) {
msgType = "text";
}
if (!DBManager::getInstance().isGroupMember(groupId, from)) {
resp["success"] = false;
resp["msg"] = "你不是该群成员";
resp["group_id"] = groupId;
net_server->sendData(resp.dump(), client_addr);
return;
}
int64_t now = nowSeconds();
std::string msgId = makeGroupMsgId(groupId, from);
std::string outMsg;
bool ok = DBManager::getInstance().saveGroupMessage(
msgId,
groupId,
from,
msgType,
content,
now,
outMsg
);
resp["success"] = ok;
resp["msg"] = outMsg;
resp["group_id"] = groupId;
resp["msg_id"] = msgId;
net_server->sendData(resp.dump(), client_addr);
if (!ok) {
std::cout << "⚠️ [群聊失败] [" << from << "] group_id="
<< groupId << " 原因: " << outMsg << "\n";
return;
}
json push;
push["cmd"] = "group_msg";
push["group_id"] = groupId;
push["sender"] = from;
push["content"] = content;
push["msg_type"] = msgType;
push["msg_id"] = msgId;
push["created_at"] = now;
std::vector<std::string> members =
DBManager::getInstance().getGroupMemberIds(groupId);
int pushCount = 0;
for (const std::string& memberId : members) {
if (memberId == from) {
continue;
}
sockaddr_in target_addr{};
if (net_server->getUserAddress(memberId, target_addr)) {
net_server->sendData(push.dump(), target_addr);
pushCount++;
}
}
std::cout << "💬 [群聊] [" << from << "] -> group_id="
<< groupId
<< " type=" << msgType
<< " msg=" << clipForLog(content)
<< ",实时推送 " << pushCount << " 个在线成员\n";
} catch (const std::exception& e) {
std::cerr << "群聊消息异常: " << e.what() << "\n";
}
}
原型void BusinessHandler::handleCreateGameRoom(const std::string& json_str, struct sockaddr_in client_addr)
参数
| 参数 | 说明 |
|---|
json_str | 创建房间请求 |
client_addr | 房主地址 |
执行流程
- 认证房主
- 校验群成员
- 校验 game_id
- 调用 DBManager 创建房间
- 返回 room_id 与配置
关联接口
DBManager::createGameRoomClientLogic::requestCreateGameRoomGroupChatWindow::appendGameInviteMessage
查看完整实现
business_handler.cpp
void BusinessHandler::handleCreateGameRoom(const std::string& json_str,
struct sockaddr_in client_addr)
{
try {
json j = json::parse(json_str);
std::string from;
if (!checkAuth(j, client_addr, from)) {
return;
}
json resp;
resp["cmd"] = "create_game_room_resp";
uint64_t groupId = 0;
if (!getUint64Field(j, "group_id", groupId)) {
resp["success"] = false;
resp["msg"] = "group_id 非法或缺失";
net_server->sendData(resp.dump(), client_addr);
return;
}
std::string gameId = getStringField(j, "game_id");
std::string gameName = getStringField(j, "game_name", gameId);
if (!isSafeSimpleId(gameId, 64)) {
resp["success"] = false;
resp["msg"] = "game_id 非法";
resp["group_id"] = groupId;
net_server->sendData(resp.dump(), client_addr);
return;
}
if (gameName.empty() || gameName.size() > 64) {
gameName = gameId;
}
if (!DBManager::getInstance().isGroupMember(groupId, from)) {
resp["success"] = false;
resp["msg"] = "你不是该群成员,不能创建游戏房间";
resp["group_id"] = groupId;
resp["game_id"] = gameId;
resp["game_name"] = gameName;
net_server->sendData(resp.dump(), client_addr);
return;
}
std::string roomId;
std::string launcherUrl;
std::string outMsg;
bool ok = DBManager::getInstance().createGameRoom(
groupId,
from,
gameId,
gameName,
roomId,
launcherUrl,
outMsg
);
resp["success"] = ok;
resp["msg"] = outMsg;
resp["group_id"] = groupId;
resp["game_id"] = gameId;
resp["game_name"] = gameName;
resp["room_id"] = roomId;
resp["launcher_url"] = launcherUrl;
net_server->sendData(resp.dump(), client_addr);
if (ok) {
std::cout << "🎮 [游戏房间] 用户 [" << from << "] 在 group_id="
<< groupId << " 创建游戏 [" << gameId
<< "] room_id=" << roomId << "\n";
} else {
std::cout << "⚠️ [游戏房间失败] 用户 [" << from << "] 创建失败: "
<< outMsg << "\n";
}
} catch (const std::exception& e) {
std::cerr << "创建游戏房间异常: " << e.what() << "\n";
}
}
原型void GroupChatWindow::appendGameInviteMessage(const QString& sender, const QString& content, bool isMe, qint64 ts)
参数
| 参数 | 说明 |
|---|
sender | 邀请发送者 |
roomInfo | 房间与游戏参数 |
执行流程
- 读取房间参数
- 创建卡片控件
- 生成 Launcher 链接
- 绑定点击事件
- 插入消息列表
工程说明
卡片只承载入口,组件检测和游戏启动由 Launcher 负责。
关联接口
LauncherProtocolHandler::parseMainWindow::tryStartLauncher
查看完整实现
groupchatwindow.cpp
void GroupChatWindow::appendGameInviteMessage(const QString& sender,
const QString& content,
bool isMe,
qint64 ts)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(content.toUtf8(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
appendTextMessage(sender, content, isMe, ts);
return;
}
QJsonObject obj = doc.object();
if (obj["game_id"].toString() == "custom_game" ||
obj["type"].toString() == "custom_game_invite" ||
obj["launcher_url"].toString().startsWith("p2plauncher://custom_game/join", Qt::CaseInsensitive)) {
appendCustomGameInviteMessage(sender, content, isMe, ts);
return;
}
QString gameName = obj["game_name"].toString("未知游戏");
QString roomId = obj["room_id"].toString();
QString launcherUrl = obj["launcher_url"].toString();
QString desc = obj["description"].toString("点击加入游戏房间。");
QString timeText;
if (ts > 0) {
timeText = QDateTime::fromSecsSinceEpoch(ts).toString("HH:mm:ss");
}
QWidget* rowWidget = new QWidget(messageList);
QVBoxLayout* rowLayout = new QVBoxLayout(rowWidget);
rowLayout->setContentsMargins(8, 6, 8, 6);
QLabel* nameLabel = new QLabel(rowWidget);
nameLabel->setText(isMe ? QString("我 %1").arg(timeText)
: QString("%1 %2").arg(sender, timeText));
nameLabel->setStyleSheet(isMe ? "color: #1677ff; font-weight: bold;"
: "color: #333; font-weight: bold;");
QWidget* card = new QWidget(rowWidget);
card->setStyleSheet(
"background-color: white; border: 1px solid #d0d7de; "
"border-radius: 8px;"
);
QVBoxLayout* cardLayout = new QVBoxLayout(card);
cardLayout->setContentsMargins(12, 10, 12, 10);
cardLayout->setSpacing(8);
QLabel* titleLabel = new QLabel(QString("🎮 %1").arg(gameName), card);
titleLabel->setStyleSheet("font-size: 16px; font-weight: bold;");
QLabel* roomLabel = new QLabel(QString("房间 ID:%1").arg(roomId), card);
roomLabel->setStyleSheet("color: #555;");
QLabel* descLabel = new QLabel(desc, card);
descLabel->setWordWrap(true);
descLabel->setStyleSheet("color: #666;");
QPushButton* joinBtn = new QPushButton("启动 Launcher 加入房间", card);
joinBtn->setMinimumHeight(34);
joinBtn->setStyleSheet(
"QPushButton { background-color: #1677ff; color: white; border: none; "
"border-radius: 5px; padding: 6px 12px; font-weight: bold; }"
"QPushButton:hover { background-color: #4096ff; }"
);
cardLayout->addWidget(titleLabel);
cardLayout->addWidget(roomLabel);
cardLayout->addWidget(descLabel);
cardLayout->addWidget(joinBtn);
rowLayout->addWidget(nameLabel);
rowLayout->addWidget(card);
if (isMe) {
nameLabel->setAlignment(Qt::AlignRight);
}
QListWidgetItem* item = new QListWidgetItem(messageList);
item->setSizeHint(QSize(0, 150));
messageList->setItemWidget(item, rowWidget);
messageList->scrollToBottom();
connect(joinBtn, &QPushButton::clicked, this, [=]() {
if (launcherUrl.isEmpty()) {
QMessageBox::warning(this, "游戏房间", "Launcher 链接为空。");
return;
}
QUrl url(launcherUrl);
QUrlQuery query(url);
if (m_logic) {
QString userId = m_logic->getCurrentUser();
QString token = m_logic->getAuthToken();
if (userId.isEmpty() || token.isEmpty()) {
QMessageBox::warning(this,
"登录态缺失",
"当前客户端没有有效 user_id/token,无法安全进入游戏大厅。\n"
"请重新登录后再点击游戏卡片。");
return;
}
query.removeQueryItem("user_id");
query.removeQueryItem("token");
query.addQueryItem("user_id", userId);
query.addQueryItem("token", token);
qDebug() << "[GameInvite] open launcher"
<< "baseUrl=" << launcherUrl
<< "user_id=" << userId
<< "token_empty=" << token.isEmpty();
} else {
qDebug() << "[GameInvite] m_logic is null";
}
url.setQuery(query);
qDebug() << "[GameInvite] final url=" << url.toString();
bool ok = QDesktopServices::openUrl(url);
if (!ok) {
QMessageBox::warning(
this,
"无法启动 Launcher",
"无法打开 p2plauncher:// 链接。\n\n"
"请确认 P2PLauncher 已安装并注册 URL 协议。"
);
}
});
}