diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d2c289..a4c5fbb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS PrintSupport) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Network) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Concurrent) set(ADS_VERSION 4.3.1) add_subdirectory(QtADS) @@ -37,11 +39,14 @@ set(H_HEADER_FILES include/dashboardFrame.h include/dashboardNamingDialog.h include/dataPanel.h + include/dataLoader.h + include/dataManager.h include/panelSelectionDialog.h include/panelConfigurationWidget.h include/dateTimeWidget.h include/customCalendarWidget.h include/dateTimeSelectionPanel.h + include/httpRequestManager.h ) set(CPP_SOURCE_FILES @@ -63,11 +68,14 @@ set(CPP_SOURCE_FILES source/dashboardFrame.cpp source/dashboardNamingDialog.cpp source/dataPanel.cpp + source/dataLoader.cpp + source/dataManager.cpp source/panelSelectionDialog.cpp source/panelConfigurationWidget.cpp source/dateTimeWidget.cpp source/customCalendarWidget.cpp source/dateTimeSelectionPanel.cpp + source/httpRequestManager.cpp ) set(UI_FILES @@ -153,6 +161,8 @@ endif() target_include_directories(PowerMaster PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(PowerMaster PRIVATE Qt${QT_VERSION_MAJOR}::PrintSupport) +target_link_libraries(PowerMaster PRIVATE Qt${QT_VERSION_MAJOR}::Network) +target_link_libraries(PowerMaster PRIVATE Qt${QT_VERSION_MAJOR}::Concurrent) target_link_libraries(PowerMaster PRIVATE qt${QT_VERSION_MAJOR}advanceddocking) target_link_libraries(PowerMaster PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui diff --git a/include/dataLoader.h b/include/dataLoader.h new file mode 100644 index 0000000..5a028d8 --- /dev/null +++ b/include/dataLoader.h @@ -0,0 +1,58 @@ +#ifndef DATALOADER_H +#define DATALOADER_H + +#include +#include +#include +#include +#include +#include + +class DataLoader : public QObject +{ + Q_OBJECT + +public: + explicit DataLoader(QObject* parent = nullptr); + ~DataLoader(); + + void requestData(const QString& dataKey); + void cancelRequest(const QString& dataKey); + void setMaxThreads(int count); + +signals: + void dataLoaded(const QString& dataKey, const QVariant& data); + void errorOccurred(const QString& dataKey, const QString& error); + +public slots: + void handleShutdown(); + +private: + class LoadTask : public QRunnable + { + public: + LoadTask(DataLoader* loader, const QString& key) + :m_loader(loader), m_key(key) + { + setAutoDelete(false);//禁用自动析构,由DataLoader控制管理 + } + + void run() override + { + QVariant data = fetchRealData(m_key); + emit m_loader->dataLoaded(m_key, data); + } + + private: + QVariant fetchRealData(const QString& dataKey); + + DataLoader* m_loader; + QString m_key; + }; + + QThreadPool m_threadPool; + QHash m_runningTasks; + QMutex m_taskMutex; +}; + +#endif diff --git a/include/dataManager.h b/include/dataManager.h new file mode 100644 index 0000000..d81a2af --- /dev/null +++ b/include/dataManager.h @@ -0,0 +1,75 @@ +#ifndef DATAMANAGER_H +#define DATAMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class HttpRequestManager; +class DataManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(DataManager) //禁止拷贝,等价于:DataManager(const DataManager&) = delete; DataManager& operator=(const DataManager&) = delete; + +public: + struct CacheEntry + { + QVariant data; + QDateTime timestamp; + int accessCount = 0; + bool isUpdating = false; + //QSet pendingRequests; + }; + + static DataManager* instance(); + + void registerDataSource(const QString& dataKey, std::function fetcher); + void registerHttpDataSource(const QString& dataKey, const QUrl& url, const QByteArray& method = "GET", + const QByteArray& body = QByteArray(), const QMap& headers = {}); + QVariant getCacheData(const QString& dataKey); + bool hasData(const QString& dataKey); + void requestData(const QString& dataKey, QObject* requester); + void setCacheTimeout(int seconds); + HttpRequestManager* httpManager(); + +signals: + void dataUpdated(const QString& dataKey, const QVariant& data); + void cacheInvalidated(const QString& dataKey); + +public slots: + void invalidateCache(const QString& dataKey = QString()); + void processRequestQueue(); + + void handleHttpDataReceived(const QString& dataKey, const QVariant& data); + void handleHttpRequsetFailed(const QString& dataKey, const QVariant& data); + +private: + struct RequestInfo + { + QString dataKey; + QObject* requester; + }; + + explicit DataManager(); + ~DataManager(); + + void triggerDataUpdate(const QString& dataKey); + + QHash m_cache; + QHash> m_dataSources; + QSet m_pendingUpdates; + QQueue m_requestQueue; + QTimer m_requestTimer; + QReadWriteLock m_cacheLock; + QMutex m_requestMutex; + HttpRequestManager* m_httpManager; + int m_cacheTimeout = 300; //默认5分钟 + int m_minRefreshInterval = 100; //最小刷新间隔(ms) +}; + +#endif diff --git a/include/httpRequestManager.h b/include/httpRequestManager.h new file mode 100644 index 0000000..348c876 --- /dev/null +++ b/include/httpRequestManager.h @@ -0,0 +1,71 @@ +#ifndef HTTPREQUESTMANAGER_H +#define HTTPREQUESTMANAGER_H + +#include +#include +#include +#include +#include +#include +#include + +class HttpRequestManager : public QObject +{ + Q_OBJECT + +public: + explicit HttpRequestManager(QObject* parent = nullptr); + ~HttpRequestManager(); + + void registerEndpoint(const QString& dataKey, const QUrl& url, const QByteArray& method = "GET", + const QByteArray& body = QByteArray(), const QMap& headers = {}); + + void requestData(const QString& dataKey); + void cancleRequest(const QString& dataKey); + + void setDefaultTimeout(int ms); + void setRetryPolicy(int maxRetries, int retryInterval); + + bool hasVaildEndpoint(const QString& dataKey); + +signals: + void dataReceived(const QString& dataKey, const QVariant& data); + void requestFailed(const QString& dataKey, const QString& error); + //void requestProgress(const QString& dataKey, int progress); + +private slots: + void onReplyFinished(); + void onReplyError(QNetworkReply::NetworkError code); + +private: + struct EndpointConfig + { + QString dataKey; + QUrl url; + QByteArray method; //"GET","POST",使用QByteArray而非QString是因为网络编程中的参数都是QByteArray,直接使用可以避免QString引起的转码开销 + QByteArray body; //JSON格式的请求体内容,如"{\"key\":\"value\"}" + QMap headers; //http请求头,如{{"Content-Type", "application/json"}, {"Authorization", "Bearer token123"}} + QNetworkReply* activeReply = nullptr; + QTimer* timeoutTimer = nullptr; + int retryCount = 0; //当前重试次数 + int maxRetries = 0; //最大重试次数 + int retryInterval = 1000; + }; + + QNetworkAccessManager m_networkManager; + QMap m_endpoints; + int m_defaultTimeout = 5000; + int m_maxRetries = 2; + int m_retryIntrval = 1000; + + void sendRequest(EndpointConfig& config); + void handleResponse(const QString& dataKey, QNetworkReply* reply); + void handleRequestTimeout(const QString& dataKey); + void scheduleRetry(const QString& dataKey); + + void parseJsonResponse(const QString& dataKey, const QByteArray& data); + void parseTextResponse(const QString& dataKey, const QByteArray& data); + void parseBinaryResponse(const QString& dataKey, const QByteArray& data); +}; + +#endif diff --git a/source/dataLoader.cpp b/source/dataLoader.cpp new file mode 100644 index 0000000..f049bd2 --- /dev/null +++ b/source/dataLoader.cpp @@ -0,0 +1,32 @@ +#include "dataLoader.h" + +QVariant DataLoader::LoadTask::fetchRealData(const QString& key) +{ + //后续替换成实际接口 + return QVariant(); +} + +DataLoader::DataLoader(QObject* parent) + : QObject(parent) +{ + m_threadPool.setMaxThreadCount(5); +} + +DataLoader::~DataLoader() +{ + handleShutdown(); +} + +void DataLoader::requestData(const QString& dataKey) +{ + QMutexLocker lockder(&m_taskMutex); + if(m_runningTasks.contains(dataKey)) + return; + + LoadTask* task = new LoadTask(this, dataKey); + m_runningTasks.insert(dataKey, task); + m_threadPool.start(task); +} + +void DataLoader::handleShutdown() +{} diff --git a/source/dataManager.cpp b/source/dataManager.cpp new file mode 100644 index 0000000..e9a6a60 --- /dev/null +++ b/source/dataManager.cpp @@ -0,0 +1,236 @@ +#include "dataManager.h" +#include "httpRequestManager.h" +#include + +DataManager* DataManager::instance() +{ + static DataManager instance; + return &instance; +} + +DataManager::DataManager() + :m_httpManager(new HttpRequestManager(this)) +{ + connect(m_httpManager, &HttpRequestManager::dataReceived, this, &DataManager::handleHttpDataReceived); + connect(m_httpManager, &HttpRequestManager::requestFailed, this, &DataManager::handleHttpRequsetFailed); + + m_requestTimer.setInterval(100); + connect(&m_requestTimer, &QTimer::timeout, this, &DataManager::processRequestQueue); + m_requestTimer.start(); +} +DataManager::~DataManager() +{} + +void DataManager::triggerDataUpdate(const QString& dataKey) +{ + bool isHttpSource = false; + { + QReadLocker locker(&m_cacheLock); + isHttpSource = m_dataSources.contains(dataKey) && m_httpManager->hasVaildEndpoint(dataKey); + } + + if(isHttpSource) + m_httpManager->requestData(dataKey); + else + { + QThreadPool::globalInstance()->start([weakThis = QPointer(this), dataKey]() { + if(weakThis.isNull()) + return; + + QVariant newData; + bool success = false; + //获取数据源 + std::function fetcher; + { + QReadLocker locker(&weakThis->m_cacheLock); + if(!weakThis->m_dataSources.contains(dataKey)) + { + qWarning() << "Data source not found for: " << dataKey; + weakThis->m_pendingUpdates.remove(dataKey); + return; + } + fetcher = weakThis->m_dataSources[dataKey]; + } + + try { + newData = fetcher(); + success = true; + } catch (const std::exception& e) { + qWarning() << "Data fetch error for" << dataKey << ": " << e.what(); + } + + QWriteLocker locker(&weakThis->m_cacheLock); + if(weakThis->m_cache.contains(dataKey)) + { + CacheEntry& entry = weakThis->m_cache[dataKey]; + if(success) + { + entry.data = newData; + entry.timestamp = QDateTime::currentDateTime(); + emit weakThis->dataUpdated(dataKey, newData); + } + entry.isUpdating = false; + } + + weakThis->m_pendingUpdates.remove(dataKey); + }); + } +} + +HttpRequestManager* DataManager::httpManager() +{ + return m_httpManager; +} + +void DataManager::registerDataSource(const QString& dataKey, std::function fetcher) +{ + if(dataKey.isEmpty() || !fetcher) + { + qWarning() << "Invalid data source registration"; + return; + } + + QWriteLocker locker(&m_cacheLock); + m_dataSources[dataKey] = fetcher; + + //初始化缓存条目 + if(!m_cache.contains(dataKey)) + m_cache[dataKey] = CacheEntry(); +} + +void DataManager::registerHttpDataSource(const QString& dataKey, const QUrl& url, const QByteArray& method, + const QByteArray& body, const QMap& headers) +{ + m_httpManager->registerEndpoint(dataKey, url, method, body, headers); + //调用registerDataSource初始化缓存 + registerDataSource(dataKey, [](){return QVariant();}); //http的数据通过httpManager的信号进行接收,所以生成函数给空值即可 +} + +QVariant DataManager::getCacheData(const QString& dataKey) +{ + if(dataKey.isEmpty()) + return QVariant(); + + QReadLocker locker(&m_cacheLock); + if(!m_cache.contains(dataKey)) + { + qWarning() << "Unknown data key:" << dataKey; + return QVariant(); + } + + return m_cache[dataKey].data; +} + +bool DataManager::hasData(const QString& dataKey) +{ + QReadLocker locker(&m_cacheLock); + return m_cache.contains(dataKey) && !m_cache[dataKey].data.isNull(); +} + +void DataManager::requestData(const QString& dataKey, QObject* requester) +{ + QMutexLocker lock(&m_requestMutex); + m_requestQueue.enqueue({dataKey, requester}); +} + +void DataManager::invalidateCache(const QString& dataKey) +{ + QWriteLocker locker(&m_cacheLock); + if(dataKey.isEmpty()) + { + m_cache.clear(); + emit cacheInvalidated(""); + } + else + { + m_cache.remove(dataKey); + emit cacheInvalidated(dataKey); + } +} + +void DataManager::setCacheTimeout(int seconds) +{ + m_cacheTimeout = qMax(60, seconds); //最小1分钟 +} + +void DataManager::processRequestQueue() +{ + //收集到本地,减少互斥锁的持有时间,后续都是对本地数据进行操作,无需加锁 + QList requests; + { + QMutexLocker lock(&m_requestMutex); + while(!m_requestQueue.isEmpty()) + requests.append(m_requestQueue.dequeue()); + } + + //按数据类型进行分组,减少更新频率 + QMap> groupedRequests; + for(const auto& req : requests) + groupedRequests[req.dataKey].insert(req.requester); + + QSet needsUpdate; + QDateTime curTime = QDateTime::currentDateTime(); + + //减少写锁范围 + { + QWriteLocker locker(&m_cacheLock); + for(auto it = groupedRequests.begin(); it != groupedRequests.end(); ++it) + { + const QString& dataKey = it.key(); + const QSet& requesters = it.value(); + + if(!m_cache.contains(dataKey)) + { + qWarning() << "Skipping request for unknown data key:" << dataKey; + continue; + } + + CacheEntry& entry = m_cache[dataKey]; + //entry.pendingRequests.unite(it.value()); + + bool needsRefresh = false; + if(entry.timestamp.isNull() || entry.timestamp.msecsTo(curTime) > m_minRefreshInterval) + needsRefresh = true; + if(needsRefresh) + { + entry.isUpdating = true; + m_pendingUpdates.insert(dataKey); + needsUpdate.insert(dataKey); + } + } + } //写锁在这里自动释放 + + //触发更新 + for(const QString& dataKey : needsUpdate) + triggerDataUpdate(dataKey); +} + +void DataManager::handleHttpDataReceived(const QString& dataKey, const QVariant& data) +{ + QWriteLocker locker(&m_cacheLock); + + if(m_cache.contains(dataKey)) + { + CacheEntry& entry = m_cache[dataKey]; + entry.data = data; + entry.timestamp = QDateTime::currentDateTime(); + entry.isUpdating = false; + + emit dataUpdated(dataKey, data); + } + + m_pendingUpdates.remove(dataKey); +} + +void DataManager::handleHttpRequsetFailed(const QString& dataKey, const QVariant& data) +{ + QWriteLocker locker(&m_cacheLock); + + if(m_cache.contains(dataKey)) + { + CacheEntry& entry = m_cache[dataKey]; + entry.isUpdating = false; + } + + m_pendingUpdates.remove(dataKey); +} diff --git a/source/httpRequestManager.cpp b/source/httpRequestManager.cpp new file mode 100644 index 0000000..8576c4a --- /dev/null +++ b/source/httpRequestManager.cpp @@ -0,0 +1,318 @@ +#include "httpRequestManager.h" +#include +#include +#include + +HttpRequestManager::HttpRequestManager(QObject* parent) + : QObject(parent) +{ +} +HttpRequestManager::~HttpRequestManager() +{ + for(auto& config : m_endpoints) + { + if(config.activeReply) + { + config.activeReply->abort(); + config.activeReply->deleteLater(); + config.activeReply = nullptr; + } + if(config.timeoutTimer) + { + config.timeoutTimer->stop(); + config.timeoutTimer->deleteLater(); + config.timeoutTimer = nullptr; + } + } +} + +void HttpRequestManager::sendRequest(EndpointConfig& config) +{ + QNetworkRequest request(config.url); + + for(auto it = config.headers.constBegin(); it != config.headers.constEnd(); ++it) + request.setRawHeader(it.key(), it.value()); + + QNetworkReply* reply = nullptr; + if(config.method == "GET") + reply = m_networkManager.get(request); + else if(config.method == "POST") + reply = m_networkManager.post(request, config.body); + else if(config.method == "PUT") + reply = m_networkManager.put(request, config.body); + else if(config.method == "DELETE") + reply = m_networkManager.deleteResource(request); + else + { + qWarning() << "Unsupported HTTP method:" << config.method; + emit requestFailed(config.dataKey, "Unsupported HTTP method"); + return; + } + + connect(reply, &QNetworkReply::finished, this, &HttpRequestManager::onReplyFinished); + connect(reply, &QNetworkReply::errorOccurred, this, &HttpRequestManager::onReplyError); + + config.timeoutTimer = new QTimer(this); + config.timeoutTimer->setSingleShot(true); + config.timeoutTimer->setInterval(m_defaultTimeout); + connect(config.timeoutTimer, &QTimer::timeout, this, [this, &config](){ + handleRequestTimeout(config.dataKey); + }); +} + +void HttpRequestManager::handleResponse(const QString& dataKey, QNetworkReply* reply) +{ + if(!m_endpoints.contains(dataKey)) + { + qWarning() << "Recived response for unregistered dataKey:" << dataKey; + reply->deleteLater(); + return; + } + + EndpointConfig& config = m_endpoints[dataKey]; + //关闭超时重连定时器 + if(config.timeoutTimer) + { + config.timeoutTimer->stop(); + config.timeoutTimer->deleteLater(); + config.timeoutTimer = nullptr; + } + + if(reply->error() != QNetworkReply::NoError) + return; //具体错误信息在onReplyError中进行处理 + + QByteArray responseData = reply->readAll(); + QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString(); + if(contentType.contains("application/json")) + parseJsonResponse(dataKey, responseData); + else if(contentType.contains("text/")) + parseTextResponse(dataKey, responseData); + else + parseBinaryResponse(dataKey, responseData); + + //数据重置 + reply->deleteLater(); + config.retryCount = 0; + config.activeReply = nullptr; +} + +void HttpRequestManager::handleRequestTimeout(const QString& dataKey) +{ + if(!m_endpoints.contains(dataKey)) + return; + + EndpointConfig& config = m_endpoints[dataKey]; + qWarning() << "Request timeout for dataKey: " << dataKey; + if(config.activeReply) + config.activeReply->abort(); + //重试 + if(config.retryCount < config.maxRetries) + scheduleRetry(config.dataKey); + else + { + emit requestFailed(config.dataKey, "Request timed out"); + config.retryCount = 0; + } +} + +void HttpRequestManager::scheduleRetry(const QString& dataKey) +{ + if(!m_endpoints.contains(dataKey)) + return; + + EndpointConfig& config = m_endpoints[dataKey]; + config.retryCount++; + + QTimer::singleShot(config.retryInterval, this, [this, dataKey](){ + requestData(dataKey); + }); +} + +void HttpRequestManager::parseJsonResponse(const QString& dataKey, const QByteArray& data) +{ + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(data, &parseError); + if(document.isNull()) + { + emit requestFailed(dataKey, "get JSonDocument failed"); + return; + } + if(parseError.error != QJsonParseError::NoError) + { + emit requestFailed(dataKey, "JSON parse error: " + parseError.errorString()); + return; + } + + if(document.isObject()) + emit dataReceived(dataKey, document.object().toVariantMap()); + else if(document.isArray()) + emit dataReceived(dataKey, document.array().toVariantList()); + else + emit dataReceived(dataKey, QVariant()); +} + +void HttpRequestManager::parseTextResponse(const QString& dataKey, const QByteArray& data) +{ + emit dataReceived(dataKey, QString::fromUtf8(data)); +} + +void HttpRequestManager::parseBinaryResponse(const QString& dataKey, const QByteArray& data) +{ + emit dataReceived(dataKey, data); +} + +void HttpRequestManager::registerEndpoint(const QString& dataKey, const QUrl& url, const QByteArray& method, + const QByteArray& body, const QMap& headers) +{ + EndpointConfig config; + config.dataKey = dataKey; + config.url = url; + config.method = method.toUpper(); + config.body = body; + config.headers = headers; + config.maxRetries = m_maxRetries; + config.retryInterval = m_retryIntrval; + + m_endpoints[dataKey] = config; +} + +void HttpRequestManager::requestData(const QString& dataKey) +{ + if(!m_endpoints.contains(dataKey)) + { + qWarning() << "This dataKey is not present in endpoints: " << dataKey; + emit requestFailed(dataKey, "dataKey is not present in endpoints"); + return; + } + + EndpointConfig& config = m_endpoints[dataKey]; + if(config.activeReply) + { + config.activeReply->abort(); + config.activeReply->deleteLater(); + config.activeReply = nullptr; + } + if(config.timeoutTimer) + { + config.timeoutTimer->stop(); + config.timeoutTimer->deleteLater(); + config.timeoutTimer = nullptr; + } + + sendRequest(config); +} + +void HttpRequestManager::cancleRequest(const QString& dataKey) +{ + if(!m_endpoints.contains(dataKey)) + return; + + EndpointConfig& config = m_endpoints[dataKey]; + if(config.activeReply) + { + config.activeReply->abort(); + config.activeReply->deleteLater(); + config.activeReply = nullptr; + } + if(config.timeoutTimer) + { + config.timeoutTimer->stop(); + config.timeoutTimer->deleteLater(); + config.timeoutTimer = nullptr; + } + config.retryCount = 0; +} + +void HttpRequestManager::setDefaultTimeout(int ms) +{ + m_defaultTimeout = qMax(1000, ms); +} + +void HttpRequestManager::setRetryPolicy(int maxRetries, int retryInterval) +{ + m_maxRetries = qMax(0, maxRetries); + m_retryIntrval = qMax(500, retryInterval); + //更新所以已注册端点的充实策略 + for(auto& config : m_endpoints) + { + config.maxRetries = m_maxRetries; + config.retryInterval = m_retryIntrval; + } +} + +bool HttpRequestManager::hasVaildEndpoint(const QString& dataKey) +{ + return m_endpoints.contains(dataKey) && m_endpoints[dataKey].url.isEmpty(); +} + +void HttpRequestManager::onReplyFinished() +{ + QNetworkReply* reply = qobject_cast(sender()); + if(!reply) + return; + + QString dataKey; + for(auto& config : m_endpoints) + { + if(config.activeReply == reply) + { + dataKey = config.dataKey; + break; + } + } + + if(dataKey.isEmpty()) + { + reply->deleteLater(); + return; + } + + handleResponse(dataKey, reply); +} + +void HttpRequestManager::onReplyError(QNetworkReply::NetworkError code) +{ + Q_UNUSED(code); + + QNetworkReply* reply = qobject_cast(sender()); + if(!reply) + return; + + QString dataKey; + for(auto& config : m_endpoints) + { + if(config.activeReply == reply) + { + dataKey = config.dataKey; + break; + } + } + + if(dataKey.isEmpty()) + return; + + EndpointConfig& config = m_endpoints[dataKey]; + //关闭超时重连定时器 + if(config.timeoutTimer) + { + config.timeoutTimer->stop(); + config.timeoutTimer->deleteLater(); + config.timeoutTimer = nullptr; + } + + //打印错误 + QString error = reply->errorString(); + qWarning() << "HTTP request failed for " << dataKey << ":" << error; + + //重试 + if(config.retryCount < config.maxRetries) + scheduleRetry(dataKey); + else + { + emit requestFailed(dataKey, error); + config.retryCount = 0; + } + + reply->deleteLater(); + config.activeReply = nullptr; +}