QT聊天项目DAY13

发布于:2025-06-05 ⋅ 阅读:(29) ⋅ 点赞:(0)

1. 重置密码

重置密码label也要实现浮动和点击效果,所以将忘记密码这个标签提升为ClickedLabel

1.1 ClickedLabel的复用

由于样式表(.qss) 文件中可以写入多个控件的状态UI,所以为了ClickedLabel能够复用,将成员变量的初始化方式修改为函数声明

1.2 定义忘记密码标签的样式表

#ForgetLabel[state='normal'],#ForgetLabel[state='selected']
{
	color:black;
}

#ForgetLabel[state='normal_hover'],#ForgetLabel[state='selected_hover']
{
	color:rgb(42,112,241);
}

样式表设置好以后,对忘记密码这个标签进行提升

在LoginWidget类中设置状态对应的字符串,方便UI的切换

生效

1.2 新建忘记密码界面类

1. 整体设置为垂直布局

2. 在垂直布局中拖进来水平布局

3. 以上面为模板,先拖进来6个水平布局,然后分别在水平布局中设置标签和文本编辑框,每个标签的最大高度和宽度写死,50 * 50

拖入弹簧就能够设置成上述的样子

4. 设置每个控件的名称如下

1.3 忘记密码对应的回调函数

在LoginWidget界面发射信号

在MainWindow界面绑定信号并切换界面

1.4 忘记密码界面类的错误标签刷新样式

1. 根据标签名称添加样式到样式表中

#Tip_Label[state='normal']
{
	color: green;
}

#Tip_Label[state='error']
{
	color: red;
}

2. 创建标签用来刷新标签的样式

3. 添加错误记录容器,用于存储和刷新文本标签的错误提示

这一段的逻辑和注册窗口里的逻辑一样,代码都是一样的,并且后续的文本框检验的逻辑大致上也一样

这里完全可以继承一个基类,在基类中写,回调函数也可以在基类中写,就比如用户名文本检验 CheckUserValid 可以在基类中声明虚函数,在子类中进行重载即可,甚至可以一部分在基类中写,一部分在子类中写, 只需要在子类中 基类::CheckUserValid 即可

写虚幻C++真的可以提升自己的C++水平,在回头看QTC++有种初中生回忆自己小学上学的体验

1.5 检验密码的合法性

1. 首先绑定文本输入完成的信号槽

2. 实现具体的检验逻辑

2.1 检验用户名是否合理

/* 检验用户名是否合法 */
bool ForgetPwdWidget::CheckUserValid()
{
	if (ui.UserName_Edit->text().isEmpty())
	{
		AddTipErr(TipErr::TIP_USER_ERR, QString::fromLocal8Bit("用户名不能为空"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_USER_ERR);
	return true;
}

2.2 检查邮箱是否合理

/* 检验邮箱是否合法 */
bool ForgetPwdWidget::CheckEmailValid()
{
	QString emailText = ui.Email_Edit->text();
	// 邮箱地址的正则表达式
	QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
	bool match = regex.match(emailText).hasMatch();
	if (!match)
	{
		AddTipErr(TipErr::TIP_EMAIL_ERR, QString::fromLocal8Bit("邮箱格式不正确"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_EMAIL_ERR);
	return true;
}

2.3 检查密码的合理性

/* 检验密码是否合法 */
bool ForgetPwdWidget::CheckPwdValid()
{
	QString passText = ui.NewPassword_Edit->text();
	if (passText.length() < 6 || passText.length() > 15)
	{
		AddTipErr(TipErr::TIP_PWD_ERR, QString::fromLocal8Bit("密码长度必须在6-15位之间"));
		return false;
	}

	/* 密码长度至少6位 可以是字母、数字、特定的特殊字符 */
	QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
	bool match = regExp.match(passText).hasMatch();
	if (!match)
	{
		AddTipErr(TipErr::TIP_PWD_ERR, QString::fromLocal8Bit("不能包含非法字符"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_PWD_ERR);
	return true;
}

2.4 检查验证码的合理性

/* 检验验证码是否合法 */
bool ForgetPwdWidget::CheckVerifyCodeValid()
{
	QString verifyText = ui.VerifyCode_Edit->text();
	if (verifyText.isEmpty())
	{
		AddTipErr(TipErr::TIP_VERIFY_ERR, QString::fromLocal8Bit("验证码不能为空"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_VERIFY_ERR);
	return true;
}

2. 创建基类窗口,派生出注册窗口和忘记密码窗口

基类窗口

#ifndef BASEWIGET_H
#define BASEWIGET_H

#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <functional>
#include <QMap>
#include "ClickedLabel.h"
#include "Enum.h"
#include "Global.h"
#include "TimerBtn.h"
#include "ui_BaseWiget.h"

class BaseWidget : public QDialog
{
	Q_OBJECT

public:
	BaseWidget(QWidget *parent = nullptr);
	~BaseWidget();

signals:
	void sigSwitchToLoginPage();																	// 切换到登录界面

public slots:
	/* 检查输入是否合法 */
	virtual bool CheckUserValid();																	// 检查用户名是否合法
	virtual bool CheckPasswordValid();																// 检查密码是否合法
	virtual bool CheckEmailValid();																	// 检查Email是否合法
	virtual bool CheckVerifyValid();																// 检查验证码是否合法
	virtual bool CheckEnsureValid();																// 检查确认密码是否合法

	/* 获取验证码 */
	virtual void OnGetCodeButtonClicked();
	/* 确认按钮点击 */
	virtual void OnConfirmButtonClicked();
	/* 返回按钮点击 */
	virtual void OnReturnButtonClicked();

	/* 注册模块完成时的回调 */
	virtual void slot_reg_mod_finish(ReqID reqId, QString res, ErrorCodes errCode);

public:
	void ShowTipLabel(const QString& tip, const QString& State);									// 刷新文本提示标签的样式
	void AddTipErr(TipErr err, const QString& tips);												// 添加错误提示
	void DelTipErr(TipErr err);																		// 清除错误提示
	void AnalyzingJsonData(ReqID reqId, QByteArray data);											// 解析Json

	virtual void BindSlots();
	virtual void BindSlotsFromEdit();																// 绑定当文本输入完成的槽函数
	virtual void BindSlotsFromButton();																// 绑定按钮的槽函数
	virtual void BindSlotsFromClickedLabel();														// 绑定信号槽从ClickedLabel控件
	virtual void InitHttpHandlers();																// 设置网络回包的回调函数

private:
	Ui::BaseWigetClass ui;

public:
	QLabel* Tip_Label;
	ClickedLabel* PassWord_Visible;																	// 密码可见
	ClickedLabel* Ensure_Visible;																	// 确认密码可见
	QLineEdit* User_LineEdit;																		// 用户名
	QLineEdit* Password_LineEdit;																	// 密码
	QLineEdit* Email_LineEdit;																		// Email
	QLineEdit* Verify_LineEdit;																		// 验证码
	QLineEdit* Ensure_LineEdit;																		// 确认密码

	TimerBtn* GetCode_Button;																	// 获取验证码按钮
	QPushButton* Confirm_Button;																	// 确认按钮
	QPushButton* Return_Button;																		// 返回按钮

	QMap<TipErr, QString> _TipErrs;																	// 错误提示记录
	QMap<ReqID, std::function<void(const QJsonObject&)>> _handlers;									// 不同类型的回调函数
};

#endif // BASEWIGET_H

注册窗口

#ifndef REGISTERWIDGET_H
#define REGISTERWIDGET_H

#include "BaseWidget.h"
#include "ui_RegisterWidget.h"

class RegisterWidget : public BaseWidget
{
	Q_OBJECT

public:
	RegisterWidget(QWidget *parent = nullptr);
	~RegisterWidget();

public:
	/* 确认按钮点击 */
	virtual void OnConfirmButtonClicked() override;

	/* 返回按钮点击 */
	void OnReturnButtonClicked();
	/* 取消按钮点击 */
	void OnCancelButtonClicked();

	/* 注册成功跳转到注册提示界面 */
	void ChangeToTipPage();

	virtual void BindSlotsFromEdit() override;														// 绑定当文本输入完成的槽函数
	virtual void BindSlotsFromButton() override;													// 绑定按钮的槽函数
	virtual void BindSlotsFromClickedLabel() override;												// 绑定信号槽从ClickedLabel控件

	virtual void InitHttpHandlers() override;														// 设置网络回包的回调函数
	void InitCountDownTimer();																		// 初始化倒计时器
	void InitParentControl();																		// 初始化父控件

private:
	Ui::RegisterWidgetClass ui;
	
	QTimer* _CountDownTimer;																		// 注册成功倒计时器
	int _CountDownTime;																				// 倒计时时间
};
#endif // REGISTERWIDGET_H

忘记密码窗口

#ifndef FORGETPWDWIDGET_H
#define FORGETPWDWIDGET_H

#include "BaseWidget.h"
#include "ui_ForgetPwdWidget.h"

class ForgetPwdWidget : public BaseWidget
{
	Q_OBJECT

public:
	ForgetPwdWidget(QWidget *parent = nullptr);
	~ForgetPwdWidget();

	/* 初始化父类控件 */
	void InitParentControl();
	
public:
	virtual void InitHttpHandlers() override;											// 设置网络回包的回调函数
	
	virtual void BindSlots() override;													// 绑定信号槽

	virtual void OnConfirmButtonClicked() override;										// 确认按钮点击事件
private:
	Ui::ForgetPwdWidgetClass ui;
};

#endif // FORGETPWDWIDGET_H

2.1 创建忘记密码窗口的Post请求

重点区别在注册窗口的确认按钮发送的QUrl注册用户的Post请求,忘记密码窗口的确认按钮发送的QUrl是重置用户密码的Post请求

忘记密码窗口

注册用户窗口

注册用户窗口的处理服务器响应的回调函数

2.2 服务器处理重置密码请求

重温一遍逻辑

客户端发送请求

这个客户端为了跨平台没有用正常的TCP请求,而是采用Http请求,有一个HttpMannager管理类

而该请求的发送通常是由按钮点击事件触发的,重要的是当服务器处理完成客户端发来的请求回发给客户端的http响应

处理服务器发来的响应

sig_http_finished 绑定的是基类窗口的槽函数,该槽函数是可以重载的,重要的是这个this指针,子类创建对象时绑定的就是子类的,基类的构造函数是空白的

服务器处理请求

在CServer中监听是否有客户端发来Http请求,创建处理连接的对象,该对象从事件循环池中获取一个事件循环,然后向该事件循环中注册要坚挺的HTTP请求的读写事件

/* 实时的监听该端口是否有客户端发来新的HTTP请求 */
void CServer::Start()
{
	cout << "CServer Thread ID: " << this_thread::get_id() << "\n";
	auto& IOService = AsioIOServicePool::GetInstance()->GetIOService();
	HttpConnection* pConnection = new HttpConnection(IOService);								// 创建新的连接对象
	_acceptor.async_accept(pConnection->GetSocket(), [this, pConnection](boost::system::error_code ec)
		{
			try 
			{
				if (ec)
				{
					Start();																	// 重新监听
					return;
				}
				pConnection->Start();															// 启动连接
				Start();																		// 继续监听
			}
			catch (const std::exception& e) {
				std::cerr << "CServer::Exception: " << e.what() << "\n";
			}
		});
}

注册Http的读事件监听

HttpConnection::HttpConnection(net::io_context& ioContext)
	: _socket(ioContext)
{
	CheckDeadline();														// 绑定超时的回调
}

http::async_read(_socket, _buffer, _request, [this](beast::error_code ec, std::size_t bytes_transferred)

监听到读事件,就处理客户端发来的数据,对Get请求和Post请求区分处理

先看post请求,将数据转发给专门处理登录请求的类去处理,这里不应该用单例了,因为多线程情况下可能会触发使用同一个单例的情况,并且这个类也没有什么特殊的,就是调用function,更应该用静态成员函数吧,或者使用登录对象池以及成员变量的方式

登录逻辑类中处理客户端发来的Post请求,根据url来执行相应的处理函数,其实和直接用if,if的效果一样

bool LogicSystem::HandlePost(std::string url, HttpConnection* connection)
{
	if (_postHandlers.find(url) == _postHandlers.end())
		return false;
	_postHandlers[url](connection);
	return true;
}

重要的是这个url,和客户端发来的url都对应上了

只需要在添加一个服务器处理客户端重置用户密码的请求即可

增加Mysql的API 分别为检查邮箱以及更新密码

bool MySqlDAO::CheckEmail(const string& username, const string& email)
{
	auto conn = _pool->getConnection();
	if (!conn)
	{
		cout << "Get connection failed" << endl;
		return false;
	}

	// 查询用户邮箱
	sql::PreparedStatement* stmt = conn->_con->prepareStatement("SELECT email FROM user WHERE name = ?");
	if (!stmt)
	{
		cout << "Prepare statement failed" << endl;
		_pool->retunConnection(conn);
		return false;
	}
	
	// 传入参数
	stmt->setString(1, username);

	// 执行查询
	sql::ResultSet* res = stmt->executeQuery();
	// 循环查询结果
	while (res->next())
	{
		cout << "Email: " << res->getString("email") << endl;
		_pool->retunConnection(conn);
		if (email != res->getString("email"))
		{
			cout << "Email not match" << endl;
			return false;
		}
		cout << "Email match" << endl;
		return true;
	}
	
	cout << "res->next() is Null" << endl;
	_pool->retunConnection(conn);
	return false;
}
bool MySqlDAO::UpdataPassword(const string& username, const string& email, const string& password)
{
	auto conn = _pool->getConnection();
	if (!conn)
	{
		cout << "Get connection failed" << endl;
		return false;
	}

	// 更新用户密码
	sql::PreparedStatement* stmt = conn->_con->prepareStatement("UPDATE user SET pwd = ? WHERE name = ? AND email = ?");
	if (!stmt)
	{
		_pool->retunConnection(conn);
		return false;
	}

	// 传入参数
	stmt->setString(1, password);
	stmt->setString(2, username);
	stmt->setString(3, email);

	// 执行更新
	int result = stmt->executeUpdate();
	if (result > 0)
	{
		cout << "Update password success" << endl;
		_pool->retunConnection(conn);
		return true;
	}

	cout << "Update password failed" << endl;
	_pool->retunConnection(conn);
	return false;
}

再一次封装

添加验证码不匹配的枚举

enum ErrorCodes
{
	SUCCESS = 0,
	ERROR_JSON = 1001,																		// Json错误
	RPC_FAILED = 1002,																		// RPC通信失败

	ERROR_JSON_KEY_EMAIL_LACK = 1003,														// Json中缺少email字段
	
	Verify_Expired = 1004,																	// 验证码过期
	Verify_Not_Match = 1005,																// 验证码不匹配
	
	User_Exists = 1006,																		// 用户已存在

	Email_Not_Match = 1007,																	// 邮箱不匹配
	Update_Password_Failed = 1008,															// 更新密码失败
};

完整的重置密码请求代码

/* 用户重置密码 */
RegisterPost("/ResetUserPassword", [](HttpConnection* connection)
	{
		if (connection)
		{
			auto bodyStr = boost::beast::buffers_to_string(connection->_request.body().data());	// 获取 Http请求体中的内容
			cout << "receive body is \n" << bodyStr << endl;
			connection->_response.set(http::field::content_type, "text/json");					// 设置 Http响应头中的 content-type
			Json::Value jsonResonse;															// 响应用的Json
			Json::Value jsonResult;																// 请求体解析出来的Json
			Json::Reader reader;																// Json解析器

			bool parseSuccess = reader.parse(bodyStr, jsonResult);								// 将请求体解析为Json
			if (!parseSuccess)
			{
				cout << "parse json failed" << endl;
				jsonResonse["error"] = ErrorCodes::ERROR_JSON;									// 设置响应的错误码
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;						// 向 Http响应体中写入错误码内容
				return;
			}

			/* 查找redis中存储的email对应的验证码是否合理 */
			string verifyCode = "";
			bool bGetVerifyCode = RedisManage::GetInstance()->Get(CODE_PREFIX + jsonResult["email"].asString(), verifyCode);
			if (!bGetVerifyCode)
			{
				cout << "get verify code Expired " << endl;
				jsonResonse["error"] = ErrorCodes::Verify_Expired;
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}
			 
			/* 判断验证码是否正确 */
			if (verifyCode != jsonResult["verifyCode"].asString())
			{
				cout << "verify code not match" << endl;
				jsonResonse["error"] = ErrorCodes::Verify_Not_Match;
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}

			/* 访问Mysql检查用户名和邮箱是否匹配 */
			bool bEmailMatch = MySqlManage::GetInstance()->CheckEmail(jsonResult["email"].asString(), jsonResult["user"].asString());
			if (!bEmailMatch)
			{
				cout << "user or email not match\n";
				jsonResonse["error"] = ErrorCodes::Email_Not_Match;
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}

			/* 访问Mysql更新用户密码 */
			bool bUpdatePassword = MySqlManage::GetInstance()->UpdataPassword(jsonResult["email"].asString(), jsonResult["password"].asString());
			if (!bUpdatePassword)
			{
				cout << "update password failed\n";
				jsonResonse["error"] = ErrorCodes::Update_Password_Failed;
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}

			/* 返回响应报文给客户端 */
			jsonResonse["error"] = 0;
			jsonResonse["email"] = jsonResult["email"];
			jsonResonse["user"] = jsonResult["user"];
			jsonResonse["password"] = jsonResult["password"];
			jsonResonse["verifyCode"] = jsonResult["verifyCode"];
			string jsonStr = jsonResonse.toStyledString();
			beast::ostream(connection->_response.body()) << jsonStr;							// 向 Http响应体中写入Json内容
			return;
		}
		else
		{
			std::cout << "connection is null" << std::endl;
		}
	}
	);

3. 配置新电脑环境

由于在家里学习效率低下的缘故,现在改成在公司学习,重新配置该程序的环境

Boost库没有问题

Json库没有问题

GRPC库没有问题

Mysql连接库没有问题

GRPC-JS库没有问题

使用npm 安装东西出问题了

更换镜像源,已经能够正常安装

// 1. 清空缓存
npm cache clean --force
// 2. 切换新源
npm config set registry https://registry.npmmirror.com
// 3. 查看源是否设置成功
npm config get registry
// 4. 可以正常安装需要的工具了
npm insatll xxx
)

使用npm安装各种库

npm install @grpc/grpc-js
npm install @grpc/proto-loader
npm install nodemailer
npm install redis
npm install ioredis

启动redis服务测试一下

.\redis-server.exe .\redis.windows.conf

连接到redis服务,查看配置文件

监听端口号

监听IP地址,添加一个本机IP地址即可

密码

连接成功

安装MySql

安装地址

在服务中右键Mysql为手动

配置Mysql环境

配置成功

安装Mysql图形化界面

新建数据库

新建user表

新建user_id表

配置完毕,测试注册功能

注册成功

测试忘记密码

返回结果是空的,应该是哪里出问题了,传参出问题了

再次测试

原密码

邮箱匹配但是更新密码失败

修改重置密码逻辑,重置成功


网站公告

今日签到

点亮在社区的每一天
去签到