Skip to content

Commit da0f68c

Browse files
committed
[HttpClient重构与LifecycleCallback组件新增]: 增强网络请求安全性与生命周期管理
- **GitHub Actions优化**: 修复条件判断语法,将`startsWith`改为直接比较操作系统名称,提高脚本执行准确性 - **HttpClient模块重构**: 使用LifecycleCallback包装器替换原始回调函数,自动处理对象销毁时的回调失效,防止悬空指针和内存安全问题 - **新增生命周期感知组件**: 添加LifecycleCallback模块,支持QObject、std::shared_ptr管理的对象及自由函数,提供安全的回调机制 - **测试框架完善**: 为HttpClient添加完整的单元测试,覆盖GET/POST/PUT/DELETE请求、文件上传下载、超时处理、错误处理等场景 - **代码质量提升**: 修复文件操作错误处理,改进网络超时机制,优化回调接口命名(CallBack→JsonCallback/ProgressCallback) - **文档更新**: 同步README组件列表,修正图片路径,新增LifecycleCallback组件说明 - **构建系统调整**: 更新CMakeLists.txt和.pro文件,集成新组件到构建流程中
1 parent bb176e1 commit da0f68c

15 files changed

Lines changed: 1062 additions & 213 deletions

File tree

.github/actions/install-dependencies/action.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,23 @@ runs:
1717

1818
steps:
1919
- name: Install dependencies on windows
20-
if: startsWith(runner.os, 'Windows')
20+
if: runner.os == 'Windows'
2121
shell: bash
2222
run: |
2323
choco install ninja
2424
ninja --version
2525
cmake --version
2626
2727
- name: Install dependencies on macos
28-
if: startsWith(runner.os, 'macOS')
28+
if: runner.os == 'macOS'
2929
shell: bash
3030
run: |
3131
ninja --version
3232
cmake --version
3333
clang --version
3434
3535
- name: Install dependencies on linux
36-
if: startsWith(runner.os, 'Linux')
36+
if: runner.os == 'Linux'
3737
shell: bash
3838
run: |
3939
sudo apt-get update

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757

5858
- 自定义样式气泡对话框
5959
- 可用作增强型工具提示
60-
- <img src="BubbleWindow/picture/Bubble.png" width="300" alt="气泡对话框">
60+
- <img src="src/BubbleWindow/picture/Bubble.png" width="300" alt="气泡对话框">
6161

6262
### [Chart](src/Chart/) - 数据可视化图表(QtCharts 模块 Desprecated)
6363

@@ -114,6 +114,10 @@
114114
- 自动图片轮播
115115
- <img src="src/ImageCarousel/picture/ImageCarousel.jpg" width="90%" alt="图片轮播">
116116

117+
### [LifecycleCallback](src/LifecycleCallback/) - 生命周期感知的回调包装器
118+
119+
- 支持 QObject、std::shared_ptr 管理的对象以及自由函数,在对象被销毁时自动使回调无效,防止悬空指针和内存安全问题。
120+
117121
### [LoadingIndicator](src/LoadingIndicator/) - 加载动画
118122

119123
- 动画加载指示器

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_subdirectory(GridViewModel)
1212
add_subdirectory(HttpClient)
1313
add_subdirectory(IconButton)
1414
add_subdirectory(ImageCarousel)
15+
add_subdirectory(LifecycleCallback)
1516
add_subdirectory(LoadingIndicator)
1617
add_subdirectory(LogAsynchronous)
1718
add_subdirectory(MulClient)

src/HttpClient/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ set(PROJECT_SOURCES httpclient_unittest.cc httpclient.cc httpclient.hpp)
33
qt_add_executable(httpclient_unittest MANUAL_FINALIZATION ${PROJECT_SOURCES})
44
target_link_libraries(httpclient_unittest PRIVATE Qt::Network Qt::Concurrent
55
Qt::Test)
6+
target_include_directories(httpclient_unittest PRIVATE ${CMAKE_SOURCE_DIR}/src)
67
qt_finalize_executable(httpclient_unittest)

src/HttpClient/HttpClient.pro

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
include(../../qmake/ProjectSettings.pri)
22

3-
QT += core gui network testlib
4-
5-
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
3+
QT += core network testlib
64

75
SOURCES += \
86
httpclient.cc \

src/HttpClient/httpclient.cc

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ class HttpClient::HttpClientPrivate
7878
QString filePath;
7979
qint64 fileBaseSize;
8080
QPointer<QFile> filePtr;
81-
ProgressCallBack progressCallBack = nullptr;
81+
ProgressCallback progressCallback = {};
8282
};
8383

8484
HttpClient *q_ptr;
8585

8686
QSslConfiguration sslConfiguration = QSslConfiguration::defaultConfiguration();
87-
QMap<QNetworkReply *, CallBack> tasks;
87+
QMap<QNetworkReply *, JsonCallback> tasks;
8888
QMap<QNetworkReply *, Download *> downloads;
8989
QMap<QNetworkReply *, QFile *> uploads;
9090
};
@@ -102,7 +102,7 @@ QNetworkReply *HttpClient::sendRequest(Method method,
102102
const QJsonObject &body,
103103
int timeout,
104104
bool verifyCertificate,
105-
CallBack callBack)
105+
JsonCallback callback)
106106
{
107107
auto request = d_ptr->networkRequest(verifyCertificate);
108108
request.setUrl(url);
@@ -120,7 +120,7 @@ QNetworkReply *HttpClient::sendRequest(Method method,
120120
connect(reply, &QNetworkReply::finished, this, &HttpClient::onReplyFinish);
121121
connect(reply, &QNetworkReply::errorOccurred, this, &HttpClient::onErrorOccurred);
122122
connect(reply, &QNetworkReply::sslErrors, this, &HttpClient::onSslErrors);
123-
d_ptr->tasks.insert(reply, callBack);
123+
d_ptr->tasks.insert(reply, callback);
124124
if (timeout > 0) {
125125
auto *timer = new QTimer(reply);
126126
connect(timer, &QTimer::timeout, this, &HttpClient::onNetworkTimeout);
@@ -158,13 +158,14 @@ QNetworkReply *HttpClient::downLoad(const QUrl &url,
158158
const QString &filePath,
159159
int timeout,
160160
bool verifyCertificate,
161-
ProgressCallBack progressCallBack,
162-
CallBack callBack)
161+
ProgressCallback progressCallback,
162+
JsonCallback callback)
163163
{
164164
Q_ASSERT(!filePath.isEmpty());
165165
auto *file = new QFile(filePath + ".temp", this);
166166
if (!file->open(QIODevice::WriteOnly | QIODevice::Append)) {
167167
qWarning() << QString("Cannot open the file: %1!").arg(filePath) << file->errorString();
168+
file->deleteLater();
168169
return nullptr;
169170
}
170171

@@ -179,8 +180,8 @@ QNetworkReply *HttpClient::downLoad(const QUrl &url,
179180

180181
auto *reply = QNetworkAccessManager::get(request);
181182
d_ptr->downloads
182-
.insert(reply, new HttpClientPrivate::Download{filePath, bytes, file, progressCallBack});
183-
d_ptr->tasks.insert(reply, callBack);
183+
.insert(reply, new HttpClientPrivate::Download{filePath, bytes, file, progressCallback});
184+
d_ptr->tasks.insert(reply, callback);
184185
connect(reply, &QNetworkReply::errorOccurred, this, &HttpClient::onErrorOccurred);
185186
connect(reply, &QNetworkReply::sslErrors, this, &HttpClient::onSslErrors);
186187
connect(reply, &QNetworkReply::downloadProgress, this, &HttpClient::onDownloadProgress);
@@ -198,7 +199,7 @@ QNetworkReply *HttpClient::upload_put(const QUrl &url,
198199
const QString &filePath,
199200
int timeout,
200201
bool verifyCertificate,
201-
CallBack callBack)
202+
JsonCallback callback)
202203
{
203204
Q_ASSERT(!filePath.isEmpty());
204205
auto *file = new QFile(filePath, this);
@@ -214,27 +215,30 @@ QNetworkReply *HttpClient::upload_put(const QUrl &url,
214215
auto *reply = QNetworkAccessManager::put(request, file);
215216
file->setParent(reply);
216217
d_ptr->uploads.insert(reply, file);
217-
connectUploadSlots(reply, timeout, callBack);
218+
connectUploadSlots(reply, timeout, callback);
218219
return reply;
219220
}
220221

221-
QNetworkReply *HttpClient::upload_put(
222-
const QUrl &url, const QByteArray &data, int timeout, bool verifyCertificate, CallBack callBack)
222+
QNetworkReply *HttpClient::upload_put(const QUrl &url,
223+
const QByteArray &data,
224+
int timeout,
225+
bool verifyCertificate,
226+
JsonCallback callback)
223227
{
224228
qDebug() << QString("Upload To %1").arg(url.toString(QUrl::RemoveUserInfo));
225229

226230
auto request = d_ptr->networkRequest(verifyCertificate);
227231
request.setUrl(url);
228232
auto *reply = QNetworkAccessManager::put(request, data);
229-
connectUploadSlots(reply, timeout, callBack);
233+
connectUploadSlots(reply, timeout, callback);
230234
return reply;
231235
}
232236

233237
QNetworkReply *HttpClient::upload_post(const QUrl &url,
234238
const QString &filePath,
235239
int timeout,
236240
bool verifyCertificate,
237-
CallBack callBack)
241+
JsonCallback callback)
238242
{
239243
Q_ASSERT(!filePath.isEmpty());
240244
auto *file = new QFile(filePath, this);
@@ -261,7 +265,7 @@ QNetworkReply *HttpClient::upload_post(const QUrl &url,
261265
file->setParent(reply);
262266
multiPart->setParent(reply);
263267
d_ptr->uploads.insert(reply, file);
264-
connectUploadSlots(reply, timeout, callBack);
268+
connectUploadSlots(reply, timeout, callback);
265269
return reply;
266270
}
267271

@@ -270,7 +274,7 @@ QNetworkReply *HttpClient::upload_post(const QUrl &url,
270274
const QByteArray &data,
271275
int timeout,
272276
bool verifyCertificate,
273-
CallBack callBack)
277+
JsonCallback callback)
274278
{
275279
qDebug() << QString("Upload To %1").arg(url.toString(QUrl::RemoveUserInfo) + "/" + filename);
276280
auto disposition = QString("form-data; name=\"%1\"; filename=\"%2\"").arg("file", filename);
@@ -285,7 +289,7 @@ QNetworkReply *HttpClient::upload_post(const QUrl &url,
285289

286290
auto *reply = QNetworkAccessManager::post(request, multiPart);
287291
multiPart->setParent(reply);
288-
connectUploadSlots(reply, timeout, callBack);
292+
connectUploadSlots(reply, timeout, callback);
289293
return reply;
290294
}
291295

@@ -360,11 +364,11 @@ void HttpClient::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
360364
if (!download) {
361365
return;
362366
}
363-
auto progressCallBack = download->progressCallBack;
364-
if (!progressCallBack) {
367+
auto progressCallback = download->progressCallback;
368+
if (!progressCallback) {
365369
return;
366370
}
367-
progressCallBack(bytesReceived + download->fileBaseSize, bytesTotal + download->fileBaseSize);
371+
progressCallback(bytesReceived + download->fileBaseSize, bytesTotal + download->fileBaseSize);
368372
}
369373

370374
void HttpClient::onDownloadReadyRead()
@@ -391,25 +395,18 @@ void HttpClient::onDownloadFinish()
391395
}
392396

393397
auto *download = d_ptr->downloads.value(reply);
394-
if (download) {
395-
if (!download->filePtr.isNull()) {
396-
download->filePtr->write(reply->readAll());
397-
download->filePtr->flush();
398-
download->filePtr->close();
399-
if (QFile::exists(download->filePath)) {
400-
QFile::remove(download->filePath);
398+
if (download && !download->filePtr.isNull()) {
399+
download->filePtr->write(reply->readAll());
400+
download->filePtr->flush();
401+
download->filePtr->close();
402+
if (QFile::exists(download->filePath)) {
403+
if (!QFile::remove(download->filePath)) {
404+
qWarning() << "Failed to remove existing file:" << download->filePath;
401405
}
402-
download->filePtr->rename(download->filePath);
403406
}
404-
}
405-
queryResult(reply, {});
406-
}
407-
408-
void HttpClient::onUploadFinish()
409-
{
410-
auto *reply = qobject_cast<QNetworkReply *>(sender());
411-
if (!reply) {
412-
return;
407+
if (!download->filePtr->rename(download->filePath)) {
408+
qWarning() << "Failed to rename temp file to:" << download->filePath;
409+
}
413410
}
414411
queryResult(reply, {});
415412
}
@@ -419,12 +416,12 @@ QJsonObject HttpClient::hookResult(const QJsonObject &object)
419416
return object;
420417
}
421418

422-
void HttpClient::connectUploadSlots(QNetworkReply *reply, int timeout, CallBack callBack)
419+
void HttpClient::connectUploadSlots(QNetworkReply *reply, int timeout, JsonCallback callback)
423420
{
424-
d_ptr->tasks.insert(reply, callBack);
421+
d_ptr->tasks.insert(reply, callback);
425422
connect(reply, &QNetworkReply::errorOccurred, this, &HttpClient::onErrorOccurred);
426423
connect(reply, &QNetworkReply::sslErrors, this, &HttpClient::onSslErrors);
427-
connect(reply, &QNetworkReply::finished, this, &HttpClient::onUploadFinish);
424+
connect(reply, &QNetworkReply::finished, this, &HttpClient::onReplyFinish);
428425
if (timeout > 0) {
429426
auto *timer = new QTimer(reply);
430427
connect(timer, &QTimer::timeout, this, &HttpClient::onNetworkTimeout);
@@ -435,11 +432,14 @@ void HttpClient::connectUploadSlots(QNetworkReply *reply, int timeout, CallBack
435432
void HttpClient::queryResult(QNetworkReply *reply, const QJsonObject &object)
436433
{
437434
qDebug() << object;
438-
auto callBack = d_ptr->tasks.value(reply);
435+
436+
disconnect(reply, nullptr, this, nullptr);
437+
438+
auto callback = d_ptr->tasks.value(reply);
439439
d_ptr->clearTask(reply);
440440
auto json = hookResult(object);
441-
if (callBack) {
442-
callBack(json);
441+
if (callback) {
442+
callback(json);
443443
}
444444
emit ready(reply, json);
445445

src/HttpClient/httpclient.hpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22

3+
#include <LifecycleCallback/lifecyclecallback.hpp>
4+
35
#include <QNetworkAccessManager>
46
#include <QNetworkReply>
57

@@ -9,8 +11,8 @@ class HttpClient : public QNetworkAccessManager
911
{
1012
Q_OBJECT
1113
public:
12-
using CallBack = std::function<void(const QJsonObject &)>;
13-
using ProgressCallBack = std::function<void(qint64 download, qint64 total)>;
14+
using JsonCallback = LifecycleCallback<const QJsonObject &>;
15+
using ProgressCallback = LifecycleCallback<qint64, qint64>;
1416
using HttpHeaders = QHash<QString, QString>;
1517
enum class Method : int { GET, POST, PUT, DELETE };
1618

@@ -23,7 +25,7 @@ class HttpClient : public QNetworkAccessManager
2325
const QJsonObject &body,
2426
int timeout = -1,
2527
bool verifyCertificate = true,
26-
CallBack callBack = nullptr);
28+
JsonCallback callback = {});
2729

2830
QJsonObject sync(QNetworkReply *reply);
2931
void cancel(QNetworkReply *reply);
@@ -32,30 +34,30 @@ class HttpClient : public QNetworkAccessManager
3234
const QString &filePath,
3335
int timeout = -1,
3436
bool verifyCertificate = true,
35-
ProgressCallBack progressCallBack = nullptr,
36-
CallBack callBack = nullptr);
37+
ProgressCallback progressCallback = {},
38+
JsonCallback callback = {});
3739

3840
QNetworkReply *upload_put(const QUrl &url,
3941
const QString &filePath,
4042
int timeout = -1,
4143
bool verifyCertificate = true,
42-
CallBack callBack = nullptr);
44+
JsonCallback callback = {});
4345
QNetworkReply *upload_put(const QUrl &url,
4446
const QByteArray &data,
4547
int timeout = -1,
4648
bool verifyCertificate = true,
47-
CallBack callBack = nullptr);
49+
JsonCallback callback = {});
4850
QNetworkReply *upload_post(const QUrl &url,
4951
const QString &filePath,
5052
int timeout = -1,
5153
bool verifyCertificate = true,
52-
CallBack callBack = nullptr);
54+
JsonCallback callback = {});
5355
QNetworkReply *upload_post(const QUrl &url,
5456
const QString &filename,
5557
const QByteArray &data,
5658
int timeout = -1,
5759
bool verifyCertificate = true,
58-
CallBack callBack = nullptr);
60+
JsonCallback callback = {});
5961

6062
signals:
6163
void timeOut();
@@ -69,13 +71,12 @@ private slots:
6971
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
7072
void onDownloadReadyRead();
7173
void onDownloadFinish();
72-
void onUploadFinish();
7374

7475
protected:
7576
virtual QJsonObject hookResult(const QJsonObject &object);
7677

7778
private:
78-
void connectUploadSlots(QNetworkReply *reply, int timeout, CallBack callBack);
79+
void connectUploadSlots(QNetworkReply *reply, int timeout, JsonCallback callback);
7980
void queryResult(QNetworkReply *reply, const QJsonObject &object);
8081

8182
class HttpClientPrivate;

0 commit comments

Comments
 (0)