#define NOMINMAX #include "pq_app.h" #include "json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * mq.cpp * * 本文件负责“外部数据输入/输出”: * - HTTP 请求台账、ICD 列表和 XML 文件下载; * - 动态加载 RocketMQ DLL,初始化生产者/消费者; * - 发送数据追踪请求; * - 消费 DATATopic/TraceTopic,把 jsondata/origin 落盘到 trace_data; * - RocketMQ 不可用时把待发消息写到 mq_outbox_*.jsonl,便于离线排查。 * * 数据追踪落盘结构: * runtime\trace_data\process_N\device_x\monitor_y\ * trace_context.json 本次追踪上下文,GUI 自动解析依赖它; * sent_trace_request.json 已发送/待发送追踪请求; * jsondata_01.txt DATA_TYPE=01 的 DATATopic 数据; * jsondata_02.txt/03.txt 闪变等其它 DATA_TYPE; * origin_*.txt TraceTopic 原始 MMS/报告数据; * final_sorted.xls 自动解析生成的工作簿。 * * 阅读本文件时可留意的 C++/Windows/RocketMQ 语法: * - WinHTTP 的 Open -> Connect -> OpenRequest -> Send/Receive 是成对句柄流程,错误码需逐层检查; * - LoadLibrary/GetProcAddress 在运行时解析 RocketMQ C DLL,成员中的 (*Fn)(...) 是函数指针; * - RocketMqRuntime 是进程级单例;std::mutex + lock_guard 保护 SDK 回调与 GUI 操作共享的状态; * - TraceSession 保存一次点击“数据追踪”的开始时间、GUID 和去重集合,阻断历史/重复消息; * - DetachedHandles 配合析构函数实现 RAII,先从共享状态摘除句柄,再在锁外关闭 SDK; * - nlohmann::json 同时用于构造双层消息体和递归查找字段;filesystem 统一创建/清理落盘目录; * - std::thread([h]{...}).detach() 是异步清理;lambda 按值捕获 h,避免引用已失效的局部变量。 */ using json = nlohmann::json; namespace fs = std::filesystem; namespace { /** * Function: executableDir * * @brief 实现外部通信层的 “executableDir” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path executableDir() { wchar_t path[MAX_PATH]{}; DWORD n = GetModuleFileNameW(nullptr, path, (DWORD)std::size(path)); if (n == 0 || n >= std::size(path)) return fs::current_path(); return fs::path(path).parent_path(); } /** * Function: appRootDir * * @brief 实现外部通信层的 “appRootDir” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path appRootDir() { fs::path dir = executableDir(); std::wstring name = dir.filename().wstring(); std::transform(name.begin(), name.end(), name.begin(), [](wchar_t ch) { return (wchar_t)towlower(ch); }); // exe 通常在 bin 下运行,runtime/source 与 bin 同级,所以从 bin 回到项目根。 if (name == L"bin" && dir.has_parent_path()) return dir.parent_path(); return dir; } /** * Function: runtimeDir * * @brief 处理 “runtimeDir” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path runtimeDir() { return appRootDir() / "runtime"; } /** * Function: runtimeJsonDir * * @brief 处理 “runtimeJsonDir” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path runtimeJsonDir() { return runtimeDir() / "json"; } /** * Function: runtimeLogsDir * * @brief 处理 “runtimeLogsDir” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path runtimeLogsDir() { return runtimeDir() / "logs"; } /** * Function: runtimeJsonPath * * @brief 处理 “runtimeJsonPath” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param name 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path runtimeJsonPath(const std::string& name) { return runtimeJsonDir() / fs::u8path(name); } /** * Function: runtimeLogPath * * @brief 处理 “runtimeLogPath” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param name 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path runtimeLogPath(const std::string& name) { return runtimeLogsDir() / fs::u8path(name); } /** * Function: utf8ToWide * * @brief 实现外部通信层的 “utf8ToWide” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::wstring utf8ToWide(const std::string& s) { if (s.empty()) return L""; int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0); std::wstring out(n, L'\0'); MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n); return out; } /** * Function: wideToUtf8 * * @brief 实现外部通信层的 “wideToUtf8” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string wideToUtf8(const std::wstring& s) { if (s.empty()) return ""; int n = WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0, nullptr, nullptr); std::string out(n, '\0'); WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n, nullptr, nullptr); return out; } /** * Function: jsonString * * @brief 实现外部通信层的 “jsonString” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param obj 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param key 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string jsonString(const json& obj, const char* key) { auto it = obj.find(key); if (it == obj.end() || it->is_null()) return ""; if (it->is_string()) return it->get(); if (it->is_number_integer()) return std::to_string(it->get()); if (it->is_number_unsigned()) return std::to_string(it->get()); if (it->is_number_float()) return std::to_string(it->get()); if (it->is_boolean()) return it->get() ? "true" : "false"; return it->dump(); } /** * Function: setContains * * @brief 把 “setContains” 对应的变更应用到模型、控件或输出文档。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param allowed 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool setContains(const std::unordered_set& allowed, const std::string& value) { return allowed.empty() || allowed.count(value) > 0; } /** * Function: parseStatusSet * * @brief 解析 “parseStatusSet” 对应的文本或结构,提取经过校验的业务字段。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::unordered_set parseStatusSet(const std::string& text) { std::unordered_set out; try { // 支持 GUI 默认的 JSON 数组写法,例如 "[0]"、"[1,2]"。 json j = json::parse(text); if (j.is_array()) { for (const auto& v : j) out.insert(jsonString(json{{"v", v}}, "v")); } } catch (...) { // 也兼容用户手写 "0,1,2" 或带方括号/引号的宽松格式。 std::stringstream ss(text); std::string part; while (std::getline(ss, part, ',')) { part.erase(std::remove_if(part.begin(), part.end(), [](unsigned char ch) { return std::isspace(ch) || ch == '[' || ch == ']' || ch == '"'; }), part.end()); if (!part.empty()) out.insert(part); } } return out; } /** * Function: sanitizeFilePart * * @brief 规范化 “sanitizeFilePart” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string sanitizeFilePart(std::string s) { for (char& ch : s) { if (!(std::isalnum((unsigned char)ch) || ch == '-' || ch == '_' || ch == '.')) ch = '_'; } if (s.empty()) s = "unnamed"; return s; } /** * Function: urlEncode * * @brief 实现外部通信层的 “urlEncode” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string urlEncode(const std::string& value) { static const char* hex = "0123456789ABCDEF"; std::string out; for (unsigned char ch : value) { if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~' || ch == '/' || ch == ':') { out.push_back((char)ch); } else { out.push_back('%'); out.push_back(hex[ch >> 4]); out.push_back(hex[ch & 15]); } } return out; } /** * Function: httpRequest * * @brief 实现外部通信层的 “httpRequest” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param url 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param query 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param body 待解析、比较、显示或发送的 UTF-8 文本。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string httpRequest(const std::string& url, const std::string& query, const std::string& body, PqLogFn log) { std::wstring full = utf8ToWide(url + (query.empty() ? "" : ("?" + query))); URL_COMPONENTS uc{}; uc.dwStructSize = sizeof(uc); wchar_t host[512]{}; wchar_t path[4096]{}; uc.lpszHostName = host; uc.dwHostNameLength = (DWORD)std::size(host); uc.lpszUrlPath = path; uc.dwUrlPathLength = (DWORD)std::size(path); uc.dwSchemeLength = (DWORD)-1; uc.dwExtraInfoLength = (DWORD)-1; if (!WinHttpCrackUrl(full.c_str(), 0, 0, &uc)) { throw std::runtime_error("URL 解析失败: " + url); } std::wstring object(path, uc.dwUrlPathLength); if (uc.lpszExtraInfo && uc.dwExtraInfoLength > 0) { object.append(uc.lpszExtraInfo, uc.dwExtraInfoLength); } HINTERNET session = WinHttpOpen(L"pq_tool/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!session) throw std::runtime_error("WinHttpOpen 失败"); HINTERNET connect = nullptr; HINTERNET request = nullptr; std::string response; try { connect = WinHttpConnect(session, std::wstring(host, uc.dwHostNameLength).c_str(), uc.nPort, 0); if (!connect) throw std::runtime_error("WinHttpConnect 失败"); DWORD flags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0; // 这个项目约定:body 为空走 GET,不为空走 POST JSON。 const wchar_t* method = body.empty() ? L"GET" : L"POST"; request = WinHttpOpenRequest(connect, method, object.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags); if (!request) throw std::runtime_error("WinHttpOpenRequest 失败"); WinHttpSetTimeouts(request, 10000, 10000, 10000, 10000); std::wstring headers = L"Content-Type: application/json\r\n"; BOOL ok = WinHttpSendRequest( request, headers.c_str(), (DWORD)-1, body.empty() ? WINHTTP_NO_REQUEST_DATA : (LPVOID)body.data(), (DWORD)body.size(), (DWORD)body.size(), 0); if (!ok || !WinHttpReceiveResponse(request, nullptr)) { throw std::runtime_error("HTTP 请求失败,错误码: " + std::to_string(GetLastError())); } DWORD status = 0; DWORD len = sizeof(status); WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &len, WINHTTP_NO_HEADER_INDEX); if (status < 200 || status >= 300) { throw std::runtime_error("HTTP 状态码异常: " + std::to_string(status)); } DWORD available = 0; do { if (!WinHttpQueryDataAvailable(request, &available)) { throw std::runtime_error("读取 HTTP 响应失败"); } if (!available) break; std::string chunk(available, '\0'); DWORD read = 0; if (!WinHttpReadData(request, chunk.data(), available, &read)) { throw std::runtime_error("读取 HTTP 响应内容失败"); } chunk.resize(read); response += chunk; } while (available > 0); } catch (...) { if (request) WinHttpCloseHandle(request); if (connect) WinHttpCloseHandle(connect); WinHttpCloseHandle(session); throw; } if (request) WinHttpCloseHandle(request); if (connect) WinHttpCloseHandle(connect); WinHttpCloseHandle(session); if (log) log("HTTP " + std::string(body.empty() ? "GET " : "POST ") + url + " 返回 " + std::to_string(response.size()) + " 字节"); return response; } /** * Function: writeTextFile * * @brief 把 “writeTextFile” 对应的内存数据序列化并写入目标位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 */ void writeTextFile(const fs::path& path, const std::string& text) { fs::create_directories(path.parent_path().empty() ? fs::path(".") : path.parent_path()); std::ofstream ofs(path, std::ios::binary); if (!ofs) throw std::runtime_error("无法写入文件: " + path.string()); ofs << text; } /** * Function: parseResponseDataArray * * @brief 解析 “parseResponseDataArray” 对应的文本或结构,提取经过校验的业务字段。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param raw 待解析、比较、显示或发送的 UTF-8 文本。 * @param name 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ json parseResponseDataArray(const std::string& raw, const std::string& name) { json root = json::parse(raw); if (!root.contains("data") || !root["data"].is_array()) { throw std::runtime_error(name + " 响应缺少 data 数组"); } return root["data"]; } std::vector> objectFields( const json& obj, const std::unordered_set& skip = {}) { std::vector> out; if (!obj.is_object()) return out; for (auto it = obj.begin(); it != obj.end(); ++it) { if (skip.count(it.key())) continue; json wrapper; wrapper["v"] = it.value(); out.push_back({it.key(), jsonString(wrapper, "v")}); } return out; } /** * Function: looksLikeXml * * @brief 判断 “looksLikeXml” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool looksLikeXml(const std::string& text) { std::string s = text; s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); return s.rfind("(now.time_since_epoch()) % 1000; std::time_t tt = system_clock::to_time_t(now); std::tm tm{}; localtime_s(&tm, &tt); std::ostringstream oss; oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << "." << std::setw(3) << std::setfill('0') << ms.count(); return oss.str(); } /** * Function: nowUnixMillis * * @brief 实现外部通信层的 “nowUnixMillis” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ long long nowUnixMillis() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); } /** * Function: appendTextFile * * @brief 向目标结构追加或保证存在 “appendTextFile” 所描述的数据。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 */ void appendTextFile(const fs::path& path, const std::string& text) { fs::create_directories(path.parent_path().empty() ? fs::path(".") : path.parent_path()); std::ofstream ofs(path, std::ios::binary | std::ios::app); if (!ofs) throw std::runtime_error("无法追加写入文件: " + path.string()); ofs << text; } /** * Function: nonEmptyCStr * * @brief 实现外部通信层的 “nonEmptyCStr” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param fallback 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ const char* nonEmptyCStr(const std::string& value, const char* fallback) { return value.empty() ? fallback : value.c_str(); } /** * Function: trimCopy * * @brief 规范化 “trimCopy” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string trimCopy(std::string value) { value.erase(value.begin(), std::find_if(value.begin(), value.end(), [](unsigned char ch) { return !std::isspace(ch); })); value.erase(std::find_if(value.rbegin(), value.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), value.end()); return value; } /** * Function: isWildcardTag * * @brief 判断 “isWildcardTag” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param tag 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool isWildcardTag(const std::string& tag) { std::string t = trimCopy(tag); return t.empty() || t == "*"; } /** * Function: subscribeTagExpression * * @brief 实现外部通信层的 “subscribeTagExpression” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param tag 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string subscribeTagExpression(const std::string& tag) { return isWildcardTag(tag) ? "*" : trimCopy(tag); } /** * Function: mergeSubscribeTags * * @brief 实现外部通信层的 “mergeSubscribeTags” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param left 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param right 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string mergeSubscribeTags(const std::string& left, const std::string& right) { std::string a = subscribeTagExpression(left); std::string b = subscribeTagExpression(right); if (a == "*" || b == "*") return "*"; if (a == b) return a; return a + " || " + b; } /** * Function: tagMatches * * @brief 实现外部通信层的 “tagMatches” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param expectedExpression 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param actualTag 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool tagMatches(const std::string& expectedExpression, const std::string& actualTag) { if (isWildcardTag(expectedExpression)) return true; std::string actual = trimCopy(actualTag); std::string expr = expectedExpression; size_t start = 0; while (start <= expr.size()) { size_t pos = expr.find("||", start); std::string part = trimCopy(expr.substr(start, pos == std::string::npos ? std::string::npos : pos - start)); // RocketMQ C 接口订阅表达式可能是 "tagA || tagB",消费时也按同样规则过滤。 if (part == "*" || part == actual) return true; if (pos == std::string::npos) break; start = pos + 2; } return false; } /** * Function: parseLongLong * * @brief 解析 “parseLongLong” 对应的文本或结构,提取经过校验的业务字段。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ long long parseLongLong(const json& value) { try { if (value.is_number_integer()) return value.get(); if (value.is_number_unsigned()) return static_cast(value.get()); if (value.is_number_float()) return static_cast(value.get()); if (value.is_string()) return std::stoll(value.get()); } catch (...) { } return 0; } /** * Function: findTimestampValue * * @brief 在现有数据结构中查找 “findTimestampValue” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ long long findTimestampValue(const json& value) { static const std::unordered_set keys = { "Value_TIME", "valueTime", "timestamp", "timeStamp", "storeTimestamp", "time", "Time" }; if (value.is_object()) { // 不同消息体时间字段命名不统一,因此递归查找一组常见 key。 for (const auto& key : keys) { auto it = value.find(key); if (it != value.end()) { long long v = parseLongLong(*it); if (v > 0) return v; } } for (auto it = value.begin(); it != value.end(); ++it) { long long v = findTimestampValue(it.value()); if (v > 0) return v; } } else if (value.is_array()) { for (const auto& item : value) { long long v = findTimestampValue(item); if (v > 0) return v; } } return 0; } /** * Function: timestampFromJsonText * * @brief 实现外部通信层的 “timestampFromJsonText” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ long long timestampFromJsonText(const std::string& text) { try { return findTimestampValue(json::parse(text)); } catch (...) { return 0; } } /** * Function: jsonScalarToString * * @brief 实现外部通信层的 “jsonScalarToString” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string jsonScalarToString(const json& value) { if (value.is_string()) return value.get(); if (value.is_number_integer()) return std::to_string(value.get()); if (value.is_number_unsigned()) return std::to_string(value.get()); if (value.is_number_float()) { std::ostringstream oss; oss << value.get(); return oss.str(); } if (value.is_boolean()) return value.get() ? "true" : "false"; return value.is_null() ? "" : value.dump(); } /** * Function: normalizeDataType * * @brief 规范化 “normalizeDataType” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param dataType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string normalizeDataType(std::string dataType) { dataType.erase(std::remove_if(dataType.begin(), dataType.end(), [](unsigned char ch) { return std::isspace(ch) != 0; }), dataType.end()); if (dataType.size() == 1 && std::isdigit(static_cast(dataType[0]))) { dataType = "0" + dataType; } return dataType.empty() ? "unknown" : dataType; } /** * Function: findDataTypeValue * * @brief 在现有数据结构中查找 “findDataTypeValue” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param out 输出参数;函数在成功或失败时写入结果或诊断信息。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool findDataTypeValue(const json& value, std::string& out) { if (value.is_object()) { auto it = value.find("DATA_TYPE"); if (it != value.end()) { out = normalizeDataType(jsonScalarToString(*it)); return true; } for (auto iter = value.begin(); iter != value.end(); ++iter) { if (findDataTypeValue(iter.value(), out)) return true; } } else if (value.is_array()) { for (const auto& item : value) { if (findDataTypeValue(item, out)) return true; } } return false; } /** * Function: dataTypeFromJsonText * * @brief 实现外部通信层的 “dataTypeFromJsonText” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string dataTypeFromJsonText(const std::string& text) { try { std::string dataType; if (findDataTypeValue(json::parse(text), dataType)) return dataType; } catch (...) { } return "unknown"; } /** * Function: findReportNameValue * * @brief 在现有数据结构中查找 “findReportNameValue” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param out 输出参数;函数在成功或失败时写入结果或诊断信息。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool findReportNameValue(const json& value, std::string& out) { static const char* keys[] = {"rpt_id", "rpt_name", "report_name", "rpt_no"}; if (value.is_object()) { for (const char* key : keys) { auto it = value.find(key); if (it != value.end()) { out = trimCopy(jsonScalarToString(*it)); if (!out.empty()) return true; } } for (auto iter = value.begin(); iter != value.end(); ++iter) { if (findReportNameValue(iter.value(), out)) return true; } } else if (value.is_array()) { for (const auto& item : value) { if (findReportNameValue(item, out)) return true; } } return false; } /** * Function: reportNameFromJsonText * * @brief 实现外部通信层的 “reportNameFromJsonText” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string reportNameFromJsonText(const std::string& text) { try { std::string reportName; if (findReportNameValue(json::parse(text), reportName)) return reportName; } catch (...) { } return ""; } std::string lowerAscii(std::string value); /** * Function: isMonitorIdKey * * @brief 判断 “isMonitorIdKey” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param key 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool isMonitorIdKey(const std::string& key) { std::string k = lowerAscii(trimCopy(key)); return k == "monitor" || k == "monitorid" || k == "monitor_id" || k == "mp_id" || k == "mpid" || k == "measurepointid" || k == "measure_point_id" || k == "measureid" || k == "measure_id"; } /** * Function: looksLikeJsonText * * @brief 判断 “looksLikeJsonText” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool looksLikeJsonText(const std::string& text) { std::string s = trimCopy(text); return !s.empty() && (s.front() == '{' || s.front() == '['); } /** * Function: findMonitorIdValue * * @brief 在现有数据结构中查找 “findMonitorIdValue” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param out 输出参数;函数在成功或失败时写入结果或诊断信息。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool findMonitorIdValue(const json& value, std::string& out) { if (value.is_object()) { for (auto it = value.begin(); it != value.end(); ++it) { if (isMonitorIdKey(it.key())) { out = trimCopy(jsonScalarToString(it.value())); if (!out.empty()) return true; } } for (auto it = value.begin(); it != value.end(); ++it) { if ((it.key() == "messageBody" || it.key() == "body") && it.value().is_string()) { // RocketMQ 外层 JSON 的 messageBody 常是字符串化 JSON,需要再 parse 一次。 std::string embedded = it.value().get(); if (looksLikeJsonText(embedded)) { try { if (findMonitorIdValue(json::parse(embedded), out)) return true; } catch (...) { } } } if (findMonitorIdValue(it.value(), out)) return true; } } else if (value.is_array()) { for (const auto& item : value) { if (findMonitorIdValue(item, out)) return true; } } return false; } /** * Function: monitorIdFromJsonText * * @brief 实现外部通信层的 “monitorIdFromJsonText” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string monitorIdFromJsonText(const std::string& text) { try { std::string monitorId; if (findMonitorIdValue(json::parse(text), monitorId)) return monitorId; } catch (...) { } return ""; } /** * Function: findGuidValue * * @brief 在现有数据结构中查找 “findGuidValue” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param out 输出参数;函数在成功或失败时写入结果或诊断信息。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool findGuidValue(const json& value, std::string& out) { if (value.is_object()) { for (auto it = value.begin(); it != value.end(); ++it) { std::string key = lowerAscii(trimCopy(it.key())); if (key == "guid" || key == "traceguid" || key == "trace_guid") { out = trimCopy(jsonScalarToString(it.value())); if (!out.empty()) return true; } } for (auto it = value.begin(); it != value.end(); ++it) { if ((it.key() == "messageBody" || it.key() == "body") && it.value().is_string()) { std::string embedded = it.value().get(); if (looksLikeJsonText(embedded)) { try { if (findGuidValue(json::parse(embedded), out)) return true; } catch (...) { } } } if (findGuidValue(it.value(), out)) return true; } } else if (value.is_array()) { for (const auto& item : value) { if (findGuidValue(item, out)) return true; } } return false; } /** * Function: guidFromJsonText * * @brief 实现外部通信层的 “guidFromJsonText” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string guidFromJsonText(const std::string& text) { try { std::string guid; if (findGuidValue(json::parse(text), guid)) return guid; json root = json::parse(text); if (root.is_object()) { auto it = root.find("key"); if (it != root.end()) return trimCopy(jsonScalarToString(*it)); } } catch (...) { } return ""; } /** * Function: normalizeMonitorId * * @brief 规范化 “normalizeMonitorId” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string normalizeMonitorId(std::string value) { value = trimCopy(value); value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch) != 0 || ch == '"' || ch == '\'' || ch == '{' || ch == '}'; }), value.end()); return lowerAscii(value); } /** * Function: safePathPart * * @brief 规范化 “safePathPart” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @param fallback 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string safePathPart(std::string value, const std::string& fallback) { if (value.empty()) value = fallback; for (char& ch : value) { unsigned char c = static_cast(ch); if (c < 32 || ch == '<' || ch == '>' || ch == ':' || ch == '"' || ch == '/' || ch == '\\' || ch == '|' || ch == '?' || ch == '*') { ch = '_'; } } while (!value.empty() && (value.back() == '.' || value.back() == ' ')) value.pop_back(); if (value.empty()) value = fallback.empty() ? "unnamed" : fallback; if (value.size() > 96) value.resize(96); return value; } /** * Function: firstNonEmpty * * @brief 实现外部通信层的 “firstNonEmpty” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param a 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param b 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string firstNonEmpty(const std::string& a, const std::string& b) { return !a.empty() ? a : b; } /** * Function: prettyJsonOrRaw * * @brief 实现外部通信层的 “prettyJsonOrRaw” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string prettyJsonOrRaw(const std::string& text) { try { return json::parse(text).dump(2); } catch (...) { return text; } } /** * Function: eventTitle * * @brief 实现外部通信层的 “eventTitle” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param prefix 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string eventTitle(const std::string& prefix, const std::string& path) { fs::path p = fs::u8path(path); std::string name = p.filename().u8string(); return prefix + " " + nowIsoMillis() + (name.empty() ? "" : (" " + name)); } /** * Function: existingOriginCount * * @brief 实现外部通信层的 “existingOriginCount” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param dir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 整数状态、索引或计数;入口函数以 0 表示成功。 */ int existingOriginCount(const fs::path& dir) { int count = 0; if (!fs::exists(dir)) return 0; for (const auto& entry : fs::directory_iterator(dir)) { if (!entry.is_regular_file()) continue; std::string name = entry.path().filename().u8string(); if (name.rfind("origin", 0) == 0 && entry.path().extension() == ".txt") ++count; } return count; } /** * Function: lowerAscii * * @brief 规范化 “lowerAscii” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param value 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string lowerAscii(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return value; } /** * Function: isTraceTextRecord * * @brief 判断 “isTraceTextRecord” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool isTraceTextRecord(const fs::path& path) { std::string ext = lowerAscii(path.extension().u8string()); if (ext == ".txt" || ext == ".json" || ext == ".jsonl" || ext == ".log") return true; return false; } /** * Function: clearTraceTextRecords * * @brief 实现外部通信层的 “clearTraceTextRecords” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param dir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 */ void clearTraceTextRecords(const fs::path& dir) { if (!fs::exists(dir)) return; for (const auto& entry : fs::directory_iterator(dir)) { if (!entry.is_regular_file()) continue; if (isTraceTextRecord(entry.path())) { std::error_code ec; fs::remove(entry.path(), ec); } } } struct CProducer; struct CPushConsumer; struct CMessage; struct CMessageExt; struct CSendResult { int sendStatus; char msgId[256]; long long offset; }; /* * RocketMqRuntime 是进程内唯一的 RocketMQ 适配器。 * * 设计原因: * 1. RocketMQ C DLL 通过 LoadLibrary 动态加载,运行时可能不存在; * 2. GUI 会多次点击“初始化 MQ”,需要避免重复启动/热重启导致 DLL 崩溃; * 3. MQ 回调在 SDK 线程触发,必须用互斥锁保护会话状态,再通过 event_ 通知 GUI。 */ class RocketMqRuntime { struct DetachedHandles { CProducer* producer = nullptr; CPushConsumer* consumer = nullptr; HMODULE module = nullptr; int (*ShutdownProducer)(CProducer*) = nullptr; int (*DestroyProducer)(CProducer*) = nullptr; int (*ShutdownPushConsumer)(CPushConsumer*) = nullptr; int (*DestroyPushConsumer)(CPushConsumer*) = nullptr; }; struct TraceSession { std::string monitorId; std::string monitorName; std::string traceDir; std::string xmlPath; std::string frontType; std::string guid; long long startedAtMs = 0; // 点击“数据追踪”的时间;更早的 MQ 存量消息不得进入本次会话。 std::unordered_set receivedDataTypes; // 同一次追踪中每个 DATA_TYPE 只保留一次。 std::unordered_set receivedTraceReports; // 同一次追踪中每个 rpt_id 只保留第一条。 long long dataTimestamp = 0; int originCounter = 0; }; public: /** * @brief 释放 RocketMQ 生产者、消费者和动态库句柄。 * @details 先在互斥锁保护下用 detachLocked() 把共享句柄移出运行时对象,再在锁外 * 调用 shutdownDetached();这样既避免回调线程看到半释放状态,也避免 SDK * 的阻塞式 Shutdown/Destroy 调用长时间占用 mu_。该析构函数由单例在进程 * 退出时自动执行。 */ ~RocketMqRuntime() { DetachedHandles old; { std::lock_guard lock(mu_); old = detachLocked(); } shutdownDetached(old); } /** * Function: initialize * * @brief 实现外部通信层的 “initialize” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @param event 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool initialize(const PqToolConfig& cfg, PqLogFn log, PqMqEventFn event) { DetachedHandles old; { std::lock_guard lock(mu_); // 如果生产/消费连接参数未变,只刷新配置和回调;避免重复 StartProducer/StartPushConsumer。 bool sameRuntimeConfig = cfg.producer == cfg_.producer && cfg.ipport == cfg_.ipport && cfg.producerAccessKey == cfg_.producerAccessKey && cfg.producerSecretKey == cfg_.producerSecretKey && cfg.consumer == cfg_.consumer && cfg.consumerIpport == cfg_.consumerIpport && cfg.consumerAccessKey == cfg_.consumerAccessKey && cfg.consumerSecretKey == cfg_.consumerSecretKey && cfg.dataStatTopic == cfg_.dataStatTopic && cfg.dataRealtimeTopic == cfg_.dataRealtimeTopic && cfg.dataStatTag == cfg_.dataStatTag && cfg.dataRealtimeTag == cfg_.dataRealtimeTag && cfg.traceTopic == cfg_.traceTopic && cfg.traceTag == cfg_.traceTag; if (active_ && producer_ && consumer_ && sameRuntimeConfig) { cfg_ = cfg; event_ = std::move(event); if (log) log("RocketMQ 已初始化,已刷新界面回调和当前配置。"); return true; } if (active_ && producer_ && consumer_) { // 连接参数变了也不在进程内重启底层 SDK,提示用户重启工具后生效。 cfg_ = cfg; event_ = std::move(event); if (log) { log("MQ 配置已保存;当前 RocketMQ 连接已运行。"); log("为避免 RocketMQ SDK 热关闭/重建导致程序崩溃,本次不在进程内重启底层连接。"); log("如果修改了 nameserver、生产者/消费者组、Topic 或订阅 Tag,请关闭工具后重新打开并初始化 MQ 后生效。"); } return true; } old = detachLocked(); } if (old.producer || old.consumer || old.module) { if (log) log("MQ 配置变更,正在重新初始化 RocketMQ..."); shutdownDetachedAsync(old); } std::lock_guard lock(mu_); cfg_ = cfg; event_ = std::move(event); dataTimestamp_ = 0; originCounter_ = 0; currentTraceDir_.clear(); currentMonitorId_.clear(); currentMonitorName_.clear(); sessions_.clear(); sessionsByGuid_.clear(); lastError_.clear(); if (!loadLibraryLocked()) { if (log) log("RocketMQ DLL 未加载: " + lastError_); return false; } producer_ = CreateProducer(nonEmptyCStr(cfg_.producer, "TRACE_PRODUCER")); if (!producer_) return failLocked("CreateProducer 返回空指针", log); if (!okLocked(SetProducerNameServerAddress(producer_, nonEmptyCStr(cfg_.ipport, "127.0.0.1:9876")), "SetProducerNameServerAddress", log)) { return false; } SetProducerSessionCredentials(producer_, nonEmptyCStr(cfg_.producerAccessKey, ""), nonEmptyCStr(cfg_.producerSecretKey, ""), ""); fs::create_directories(runtimeLogsDir()); std::string mqLogDir = runtimeLogsDir().u8string(); SetProducerLogPath(producer_, mqLogDir.c_str()); if (!okLocked(StartProducer(producer_), "StartProducer", log)) return false; consumer_ = CreatePushConsumer(nonEmptyCStr(cfg_.consumer, "TRACE_CONSUMER")); if (!consumer_) return failLocked("CreatePushConsumer 返回空指针", log); if (!okLocked(SetPushConsumerNameServerAddress(consumer_, nonEmptyCStr(cfg_.consumerIpport, "127.0.0.1:9876")), "SetPushConsumerNameServerAddress", log)) { return false; } SetPushConsumerSessionCredentials(consumer_, nonEmptyCStr(cfg_.consumerAccessKey, ""), nonEmptyCStr(cfg_.consumerSecretKey, ""), ""); SetPushConsumerLogPath(consumer_, mqLogDir.c_str()); SetPushConsumerThreadCount(consumer_, 2); // 同一个 Topic 可能同时配置统计/实时 Tag,这里合并成 RocketMQ 支持的 "A || B" 表达式。 std::map subscriptions; auto addSubscription = [&](const std::string& topic, const std::string& tag) { if (topic.empty()) return; auto it = subscriptions.find(topic); if (it == subscriptions.end()) { subscriptions[topic] = subscribeTagExpression(tag); } else { it->second = mergeSubscribeTags(it->second, tag); } }; addSubscription(cfg_.dataStatTopic, cfg_.dataStatTag); addSubscription(cfg_.dataRealtimeTopic, cfg_.dataRealtimeTag); addSubscription(cfg_.traceTopic, cfg_.traceTag); bool subscribed = false; for (const auto& sub : subscriptions) { if (!okLocked(Subscribe(consumer_, sub.first.c_str(), sub.second.c_str()), "Subscribe Topic", log)) { return false; } subscribed = true; if (log) log("Subscribed Topic by Tag only: " + sub.first + " [" + sub.second + "]"); } if (!subscribed) return failLocked("DATATopic/TraceTopic 为空,未订阅任何 Topic", log); if (!okLocked(RegisterMessageCallback(consumer_, &RocketMqRuntime::messageCallback), "RegisterMessageCallback", log)) { return false; } if (!okLocked(StartPushConsumer(consumer_), "StartPushConsumer", log)) return false; active_ = true; if (log) { log("RocketMQ 已连接,生产者和消费者初始化完成。"); log("DATATopic -> " + (cfg_.jsonDataPath.empty() ? std::string("jsondata.txt") : cfg_.jsonDataPath) + ",TraceTopic -> " + originOutputPath().u8string()); } return true; } /** * Function: setTraceTarget * * @brief 把 “setTraceTarget” 对应的变更应用到模型、控件或输出文档。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param frontType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param guid 索引、数量、范围或偏移参数;具体边界由函数体校验。 */ void setTraceTarget(const PqToolConfig& cfg, const PqLedgerDevice& device, const PqLedgerMonitor& monitor, const std::string& frontType, const std::string& guid) { const long long startedAtMs = nowUnixMillis(); std::lock_guard lock(mu_); cfg_ = cfg; // 记录当前追踪目标。后续 MQ 回调会用 monitorId 或 guid 把消息路由到对应 TraceSession。 currentTraceDir_ = pqTraceDirectoryFor(cfg, device, monitor); currentXmlPath_ = pqFindDeviceXmlPath(cfg, device); currentMonitorId_ = monitor.id; currentMonitorName_ = monitor.name; fs::path dir = fs::u8path(currentTraceDir_); fs::create_directories(dir); clearTraceTextRecords(dir); dataTimestamp_ = 0; originCounter_ = 0; std::string monitorKey = normalizeMonitorId(monitor.id); // 一次按钮点击只允许一个活动会话。否则旧测点后续到达的消息仍会写入旧会话目录。 sessions_.clear(); sessionsByGuid_.clear(); TraceSession& session = sessions_[monitorKey]; session.monitorId = monitor.id; session.monitorName = monitor.name; session.traceDir = currentTraceDir_; session.xmlPath = currentXmlPath_; session.frontType = frontType; session.guid = guid; session.startedAtMs = startedAtMs; session.receivedDataTypes.clear(); session.receivedTraceReports.clear(); session.dataTimestamp = 0; session.originCounter = 0; if (!guid.empty()) sessionsByGuid_[normalizeMonitorId(guid)] = monitorKey; // GUI 自动解析优先读取这个上下文,避免用户手动维护 json/origin/output 路径。 json context = { {"deviceId", device.id}, {"deviceName", device.name}, {"devType", device.devType}, {"processNo", device.processNo}, {"monitorId", monitor.id}, {"monitorName", monitor.name}, {"guid", guid}, {"frontType", frontType}, {"startedAt", nowIsoMillis()}, {"startedAtMs", startedAtMs}, {"xmlPath", currentXmlPath_}, {"jsonDataPath", (dir / "jsondata_01.txt").u8string()}, {"jsonDataPaths", json::array({ (dir / "jsondata_01.txt").u8string(), (dir / "jsondata_02.txt").u8string(), (dir / "jsondata_03.txt").u8string() })}, {"outputPath", (dir / "final_sorted.xls").u8string()} }; writeTextFile(dir / "trace_context.json", context.dump(2)); } /** * Function: sendPayload * * @brief 执行 “sendPayload” 对应的外部通信,并把结果交给日志、事件或落盘流程。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param topic 待解析、比较、显示或发送的 UTF-8 文本。 * @param tag 待解析、比较、显示或发送的 UTF-8 文本。 * @param messageKey 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param payload 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param label 待解析、比较、显示或发送的 UTF-8 文本。 * @param err 输出参数;函数在成功或失败时写入结果或诊断信息。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool sendPayload(const std::string& topic, const std::string& tag, const std::string& messageKey, const std::string& payload, const std::string& label, std::string& err, PqLogFn log) { std::lock_guard lock(mu_); err.clear(); if (!active_ || !producer_) { err = lastError_.empty() ? "RocketMQ 未初始化" : lastError_; return false; } CMessage* msg = CreateMessage(nonEmptyCStr(topic, "")); if (!msg) { err = "CreateMessage 返回空指针"; return false; } SetMessageTags(msg, nonEmptyCStr(tag, "")); SetMessageKeys(msg, nonEmptyCStr(messageKey, "")); SetMessageBody(msg, payload.c_str()); CSendResult result{}; int rc = SendMessageSync(producer_, msg, &result); DestroyMessage(msg); if (rc != 0) { err = "SendMessageSync 返回错误码 " + std::to_string(rc); return false; } if (log) { log("RocketMQ " + label + "消息已发送,Topic=" + topic + ",MsgId=" + std::string(result.msgId)); } return true; } private: using MessageCallback = int (*)(CPushConsumer*, CMessageExt*); /** * Function: messageCallback * * @brief 实现外部通信层的 “messageCallback” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param CPushConsumer 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param msg 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 整数状态、索引或计数;入口函数以 0 表示成功。 */ static int messageCallback(CPushConsumer*, CMessageExt* msg) { return instance().onMessage(msg); } /** * Function: findSessionForMessageLocked * * @brief 在现有数据结构中查找 “findSessionForMessageLocked” 对应的对象或位置。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param body 待解析、比较、显示或发送的 UTF-8 文本。 * @param key 待解析、比较、显示或发送的 UTF-8 文本。 * @param actualMonitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param reason 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ TraceSession* findSessionForMessageLocked(const std::string& body, const std::string& key, std::string& actualMonitor, std::string& reason) { actualMonitor.clear(); reason.clear(); // 优先用消息体中的测点 ID 路由;没有测点 ID 时再用 guid/key 路由。 actualMonitor = monitorIdFromJsonText(body); std::string normalized = normalizeMonitorId(actualMonitor); if (!normalized.empty()) { auto it = sessions_.find(normalized); if (it != sessions_.end()) return &it->second; reason = "monitor not being traced, actual=" + actualMonitor; } std::string guid = guidFromJsonText(body); if (guid.empty()) guid = trimCopy(key); std::string normalizedGuid = normalizeMonitorId(guid); if (!normalizedGuid.empty()) { auto git = sessionsByGuid_.find(normalizedGuid); if (git != sessionsByGuid_.end()) { auto sit = sessions_.find(git->second); if (sit != sessions_.end()) return &sit->second; } if (reason.empty()) reason = "guid not being traced, actual=" + guid; } if (reason.empty()) reason = "missing monitor id/guid"; return nullptr; } /** * Function: onMessage * * @brief 处理 “onMessage” 对应的业务入口或事件,并调度下游函数完成实际工作。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param msg 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 整数状态、索引或计数;入口函数以 0 表示成功。 */ int onMessage(CMessageExt* msg) { std::string topic = GetMessageTopic && GetMessageTopic(msg) ? GetMessageTopic(msg) : ""; std::string tag = GetMessageTags && GetMessageTags(msg) ? GetMessageTags(msg) : ""; std::string key = GetMessageKeys && GetMessageKeys(msg) ? GetMessageKeys(msg) : ""; std::string body = GetMessageBody && GetMessageBody(msg) ? GetMessageBody(msg) : ""; long long storeTs = GetMessageStoreTimestamp ? GetMessageStoreTimestamp(msg) : 0; std::lock_guard lock(mu_); try { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " received topic=" + topic + " tag=" + tag + " key=" + key + " bodyLen=" + std::to_string(body.size()) + "\n"); // 先按 Topic/Tag 判断消息类型,再按 monitorId/guid 过滤到当前追踪会话。 std::string dataFrontType = dataFrontTypeForMessageLocked(topic, tag); bool isData = !dataFrontType.empty(); bool isTrace = topic == cfg_.traceTopic && tagMatches(cfg_.traceTag, tag); TraceSession* session = nullptr; if (isData || isTrace) { std::string actualMonitor; std::string reason; session = findSessionForMessageLocked(body, key, actualMonitor, reason); if (!session) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped " + (isData ? "DATATopic" : "TraceTopic") + " by monitor router: " + reason + " topic=" + topic + " tag=" + tag + "\n"); return 0; } if (isData && !session->frontType.empty() && session->frontType != dataFrontType) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped DATATopic by frontType router: session=" + session->frontType + ", message=" + dataFrontType + " topic=" + topic + " tag=" + tag + "\n"); return 0; } // PushConsumer 可能在点击后继续投递积压消息。必须在任何去重、落盘和 GUI 事件之前 // 使用 Broker 存储时间做会话边界判断,保证 jsondata 与 origin 都来自本次追踪。 if (storeTs <= 0 || storeTs < session->startedAtMs) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped " + (isData ? "DATATopic" : "TraceTopic") + " before trace session: storeTs=" + std::to_string(storeTs) + ", startedAtMs=" + std::to_string(session->startedAtMs) + ", monitor=" + session->monitorId + " topic=" + topic + " tag=" + tag + "\n"); return 0; } } if (isData) { std::string dataType = dataTypeFromJsonText(body); std::string normalizedType = normalizeDataType(dataType); // 同一 DATA_TYPE 重复到达时跳过,避免后来的旧数据覆盖已解析文件。 if (!session->receivedDataTypes.insert(normalizedType).second) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped DATATopic duplicate DATA_TYPE=" + normalizedType + " monitor=" + session->monitorId + " topic=" + topic + " tag=" + tag + "\n"); return 0; } fs::path out = jsonDataOutputPathLocked(*session, dataType); writeTextFile(out, body); long long bodyTs = timestampFromJsonText(body); session->dataTimestamp = bodyTs > 0 ? bodyTs : storeTs; if (normalizeMonitorId(session->monitorId) == normalizeMonitorId(currentMonitorId_)) { dataTimestamp_ = session->dataTimestamp; } appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " DATATopic DATA_TYPE=" + dataType + " tag=" + tag + " -> " + out.u8string() + "\n"); if (event_) event_({"data", eventTitle("DATATopic", out.u8string()), body, out.u8string(), session->monitorId, session->traceDir}); } else if (isTrace) { long long bodyTs = timestampFromJsonText(body); // 如果 TraceTopic 的时间早于已经收到的 DATATopic,认为是旧追踪结果,避免串入本次追踪。 if (session->dataTimestamp > 0 && bodyTs > 0 && bodyTs < session->dataTimestamp) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " TraceTopic tag=" + tag + " skipped by timestamp\n"); } else { std::string reportName = reportNameFromJsonText(body); std::string reportKey = lowerAscii(trimCopy(reportName)); if (reportKey.empty()) reportKey = "__unknown_report__"; // TopicAsk 的 limit=20 可能返回同一 rpt_id 的多份历史采样;本工具每次点击只取第一份。 if (!session->receivedTraceReports.insert(reportKey).second) { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped TraceTopic duplicate report=" + reportKey + " monitor=" + session->monitorId + " topic=" + topic + " tag=" + tag + "\n"); return 0; } fs::path out = nextOriginOutputPathLocked(*session, reportName); writeTextFile(out, body); appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " TraceTopic tag=" + tag + " -> " + out.u8string() + "\n"); if (event_) event_({"trace", eventTitle("TraceTopic", out.u8string()), body, out.u8string(), session->monitorId, session->traceDir}); } } else { appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " skipped topic=" + topic + " tag=" + tag + "\n"); } } catch (...) { return 1; } return 0; } /** * Function: instance * * @brief 实现外部通信层的 “instance” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ static RocketMqRuntime& instance() { static RocketMqRuntime runtime; return runtime; } friend RocketMqRuntime& rocketRuntime(); /** * Function: originOutputPath * * @brief 实现外部通信层的 “originOutputPath” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path originOutputPath() const { if (!cfg_.originPaths.empty() && !cfg_.originPaths.front().empty()) { return fs::u8path(cfg_.originPaths.front()); } return fs::u8path("origin.txt"); } /** * Function: dataFrontTypeForMessageLocked * * @brief 实现外部通信层的 “dataFrontTypeForMessageLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param topic 待解析、比较、显示或发送的 UTF-8 文本。 * @param tag 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string dataFrontTypeForMessageLocked(const std::string& topic, const std::string& tag) const { if (topic == cfg_.dataStatTopic && tagMatches(cfg_.dataStatTag, tag)) return "cfg_stat_data"; if (topic == cfg_.dataRealtimeTopic && tagMatches(cfg_.dataRealtimeTag, tag)) return "cfg_3s_data"; return ""; } /** * Function: jsonDataOutputPathLocked * * @brief 实现外部通信层的 “jsonDataOutputPathLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param session 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param dataType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path jsonDataOutputPathLocked(const TraceSession& session, const std::string& dataType) const { std::string suffix = normalizeDataType(dataType); std::string filename = "jsondata_" + safePathPart(suffix, "unknown") + ".txt"; if (!session.traceDir.empty()) return fs::u8path(session.traceDir) / fs::u8path(filename); if (!currentTraceDir_.empty()) return fs::u8path(currentTraceDir_) / fs::u8path(filename); if (!cfg_.jsonDataPath.empty()) { std::vector paths; std::stringstream ss(cfg_.jsonDataPath); std::string part; while (std::getline(ss, part)) { part = trimCopy(part); if (!part.empty()) paths.push_back(part); } fs::path base = paths.empty() ? fs::u8path(cfg_.jsonDataPath) : fs::u8path(paths.front()); fs::path dir = base.has_parent_path() ? base.parent_path() : fs::path("."); return dir / fs::u8path(filename); } return runtimeJsonPath(filename); } /** * Function: nextOriginOutputPathLocked * * @brief 实现外部通信层的 “nextOriginOutputPathLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param session 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param reportName 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ fs::path nextOriginOutputPathLocked(TraceSession& session, const std::string& reportName) { if (session.traceDir.empty() && currentTraceDir_.empty()) return originOutputPath(); std::string safeReport = reportName.empty() ? "" : safePathPart(reportName, "report"); std::string baseName; if (safeReport.empty()) { ++session.originCounter; baseName = "origin" + std::to_string(session.originCounter); } else { baseName = "origin_" + safeReport; } fs::path dir = session.traceDir.empty() ? fs::u8path(currentTraceDir_) : fs::u8path(session.traceDir); fs::path out = dir / fs::u8path(baseName + ".txt"); int suffix = 2; while (fs::exists(out)) { out = dir / fs::u8path(baseName + "_" + std::to_string(suffix++) + ".txt"); } return out; } /** * Function: failLocked * * @brief 实现外部通信层的 “failLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param message 输出参数;函数在成功或失败时写入结果或诊断信息。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool failLocked(const std::string& message, PqLogFn log) { lastError_ = message; shutdownLocked(); if (log) log("RocketMQ 初始化失败: " + message); return false; } /** * Function: okLocked * * @brief 实现外部通信层的 “okLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param rc 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param op 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool okLocked(int rc, const char* op, PqLogFn log) { if (rc == 0) return true; return failLocked(std::string(op) + " 返回错误码 " + std::to_string(rc), log); } /** * Function: loadLibraryLocked * * @brief 读取或加载 “loadLibraryLocked” 对应的数据,并转换为本模块后续逻辑使用的内存结构。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool loadLibraryLocked() { if (module_) return true; // 兼容不同部署目录和 DLL 命名;LoadLibraryEx 用于带路径 DLL 时顺带搜索其依赖。 std::vector candidates = { "rocketmq-client-cpp.dll", "rocketmq.dll", "librocketmq.dll", "bin\\rocketmq-client-cpp.dll", "bin\\rocketmq.dll", "bin\\librocketmq.dll", "rocketmq-client-cpp-2.2.0-source-release\\bin\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\bin\\rocketmq.dll", "rocketmq-client-cpp-2.2.0-source-release\\bin\\librocketmq.dll", "Win32\\Debug\\rocketmq-client-cpp.dll", "Win32\\Release\\rocketmq-client-cpp.dll", "Win32\\x86\\Debug\\rocketmq-client-cpp.dll", "Win32\\x86\\Release\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\Debug\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\Release\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\x86\\Debug\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\x86\\Release\\rocketmq-client-cpp.dll", "Win32\\x64\\Debug\\rocketmq-client-cpp.dll", "Win32\\x64\\Release\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\x64\\Debug\\rocketmq-client-cpp.dll", "rocketmq-client-cpp-2.2.0-source-release\\Win32\\x64\\Release\\rocketmq-client-cpp.dll" }; DWORD lastErr = 0; std::string tried; for (const auto& path : candidates) { bool exists = fs::exists(fs::u8path(path)); module_ = path.find('\\') == std::string::npos ? LoadLibraryA(path.c_str()) : LoadLibraryExA(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); if (module_) { loadedFrom_ = path; break; } lastErr = GetLastError(); if (!tried.empty()) tried += ", "; tried += path; tried += exists ? "(load error=" + std::to_string(lastErr) + ")" : "(not found)"; } if (!module_) { lastError_ = "No loadable RocketMQ DLL. Tried: " + tried + ". LastError=" + std::to_string(lastErr) + ". Error 126 means the DLL itself or one of its dependent DLLs is missing. " "This pq_tool.exe is 32-bit, so RocketMQ must be built as x86."; return false; lastError_ = "未找到可加载的 RocketMQ DLL,已尝试: " + tried + ",GetLastError=" + std::to_string(lastErr) + "。当前 exe 是 32 位,DLL 也需要是 x86。"; return false; } std::vector missing; #define LOAD_MQ_FN(name) \ do { \ name = reinterpret_cast(GetProcAddress(module_, #name)); \ if (!name) missing.push_back(#name); \ } while (0) LOAD_MQ_FN(CreateProducer); LOAD_MQ_FN(DestroyProducer); LOAD_MQ_FN(StartProducer); LOAD_MQ_FN(ShutdownProducer); LOAD_MQ_FN(SetProducerNameServerAddress); LOAD_MQ_FN(SetProducerSessionCredentials); LOAD_MQ_FN(SetProducerLogPath); LOAD_MQ_FN(CreateMessage); LOAD_MQ_FN(DestroyMessage); LOAD_MQ_FN(SetMessageTags); LOAD_MQ_FN(SetMessageKeys); LOAD_MQ_FN(SetMessageBody); LOAD_MQ_FN(SendMessageSync); LOAD_MQ_FN(CreatePushConsumer); LOAD_MQ_FN(DestroyPushConsumer); LOAD_MQ_FN(StartPushConsumer); LOAD_MQ_FN(ShutdownPushConsumer); LOAD_MQ_FN(SetPushConsumerNameServerAddress); LOAD_MQ_FN(SetPushConsumerSessionCredentials); LOAD_MQ_FN(SetPushConsumerLogPath); LOAD_MQ_FN(SetPushConsumerThreadCount); LOAD_MQ_FN(Subscribe); LOAD_MQ_FN(RegisterMessageCallback); LOAD_MQ_FN(GetMessageTopic); LOAD_MQ_FN(GetMessageTags); LOAD_MQ_FN(GetMessageKeys); LOAD_MQ_FN(GetMessageBody); LOAD_MQ_FN(GetMessageStoreTimestamp); #undef LOAD_MQ_FN if (!missing.empty()) { std::ostringstream oss; for (size_t i = 0; i < missing.size(); ++i) { if (i) oss << ", "; oss << missing[i]; } lastError_ = "RocketMQ DLL 缺少 C 接口导出: " + oss.str(); FreeLibrary(module_); module_ = nullptr; return false; } return true; } /** * Function: detachLocked * * @brief 实现外部通信层的 “detachLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ DetachedHandles detachLocked() { DetachedHandles h; h.producer = producer_; h.consumer = consumer_; h.module = module_; h.ShutdownProducer = ShutdownProducer; h.DestroyProducer = DestroyProducer; h.ShutdownPushConsumer = ShutdownPushConsumer; h.DestroyPushConsumer = DestroyPushConsumer; active_ = false; producer_ = nullptr; consumer_ = nullptr; module_ = nullptr; loadedFrom_.clear(); return h; } /** * Function: shutdownDetached * * @brief 实现外部通信层的 “shutdownDetached” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 */ void shutdownDetached(const DetachedHandles& h) { if (h.consumer) { if (h.ShutdownPushConsumer) h.ShutdownPushConsumer(h.consumer); if (h.DestroyPushConsumer) h.DestroyPushConsumer(h.consumer); } if (h.producer) { if (h.ShutdownProducer) h.ShutdownProducer(h.producer); if (h.DestroyProducer) h.DestroyProducer(h.producer); } if (h.module) FreeLibrary(h.module); } /** * Function: shutdownDetachedAsync * * @brief 实现外部通信层的 “shutdownDetachedAsync” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 */ void shutdownDetachedAsync(DetachedHandles h) { std::thread([h]() { try { if (h.consumer) { if (h.ShutdownPushConsumer) h.ShutdownPushConsumer(h.consumer); if (h.DestroyPushConsumer) h.DestroyPushConsumer(h.consumer); } if (h.producer) { if (h.ShutdownProducer) h.ShutdownProducer(h.producer); if (h.DestroyProducer) h.DestroyProducer(h.producer); } if (h.module) FreeLibrary(h.module); } catch (...) { } }).detach(); } /** * Function: shutdownLocked * * @brief 实现外部通信层的 “shutdownLocked” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 */ void shutdownLocked() { active_ = false; if (consumer_) { ShutdownPushConsumer(consumer_); DestroyPushConsumer(consumer_); consumer_ = nullptr; } if (producer_) { ShutdownProducer(producer_); DestroyProducer(producer_); producer_ = nullptr; } if (module_) { FreeLibrary(module_); module_ = nullptr; } } std::mutex mu_; PqToolConfig cfg_; PqMqEventFn event_; HMODULE module_ = nullptr; std::string loadedFrom_; std::string lastError_; std::string currentTraceDir_; std::string currentXmlPath_; std::string currentMonitorId_; std::string currentMonitorName_; std::unordered_map sessions_; std::unordered_map sessionsByGuid_; bool active_ = false; long long dataTimestamp_ = 0; int originCounter_ = 0; CProducer* producer_ = nullptr; CPushConsumer* consumer_ = nullptr; CProducer* (*CreateProducer)(const char*) = nullptr; int (*DestroyProducer)(CProducer*) = nullptr; int (*StartProducer)(CProducer*) = nullptr; int (*ShutdownProducer)(CProducer*) = nullptr; int (*SetProducerNameServerAddress)(CProducer*, const char*) = nullptr; int (*SetProducerSessionCredentials)(CProducer*, const char*, const char*, const char*) = nullptr; int (*SetProducerLogPath)(CProducer*, const char*) = nullptr; CMessage* (*CreateMessage)(const char*) = nullptr; int (*DestroyMessage)(CMessage*) = nullptr; int (*SetMessageTags)(CMessage*, const char*) = nullptr; int (*SetMessageKeys)(CMessage*, const char*) = nullptr; int (*SetMessageBody)(CMessage*, const char*) = nullptr; int (*SendMessageSync)(CProducer*, CMessage*, CSendResult*) = nullptr; CPushConsumer* (*CreatePushConsumer)(const char*) = nullptr; int (*DestroyPushConsumer)(CPushConsumer*) = nullptr; int (*StartPushConsumer)(CPushConsumer*) = nullptr; int (*ShutdownPushConsumer)(CPushConsumer*) = nullptr; int (*SetPushConsumerNameServerAddress)(CPushConsumer*, const char*) = nullptr; int (*SetPushConsumerSessionCredentials)(CPushConsumer*, const char*, const char*, const char*) = nullptr; int (*SetPushConsumerLogPath)(CPushConsumer*, const char*) = nullptr; int (*SetPushConsumerThreadCount)(CPushConsumer*, int) = nullptr; int (*Subscribe)(CPushConsumer*, const char*, const char*) = nullptr; int (*RegisterMessageCallback)(CPushConsumer*, MessageCallback) = nullptr; const char* (*GetMessageTopic)(CMessageExt*) = nullptr; const char* (*GetMessageTags)(CMessageExt*) = nullptr; const char* (*GetMessageKeys)(CMessageExt*) = nullptr; const char* (*GetMessageBody)(CMessageExt*) = nullptr; long long (*GetMessageStoreTimestamp)(CMessageExt*) = nullptr; }; /** * Function: rocketRuntime * * @brief 实现外部通信层的 “rocketRuntime” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ RocketMqRuntime& rocketRuntime() { return RocketMqRuntime::instance(); } } // namespace /** * Function: pqDefaultConfig * * @brief 实现外部通信层的 “pqDefaultConfig” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ PqToolConfig pqDefaultConfig() { PqToolConfig cfg; // 这里只覆盖需要根据 exe 位置动态计算的路径,以及 MQ 常用默认参数。 cfg.producer = "TRACE_PRODUCER"; cfg.consumer = "TRACE_CONSUMER"; cfg.dataStatTopic = "MQ_INST_1697676490574298_Bm3DLamM%LN_Topic"; cfg.dataRealtimeTopic = "MQ_INST_1697676490574298_Bm3DLamM%Real_Time_Data_Topic"; cfg.dataStatTag = "6c2adb324940e19e208d717f4970f2ee"; cfg.dataStatKey = "Test_Keys"; cfg.dataRealtimeTag = "6c2adb324940e19e208d717f4970f2ee"; cfg.dataRealtimeKey = "Test_Keys"; cfg.dataTopic = cfg.dataStatTopic; cfg.dataTag = cfg.dataStatTag; cfg.dataKey = cfg.dataStatKey; cfg.xmlSaveDir = (runtimeDir() / "downloaded_xml").u8string(); cfg.traceRootDir = (runtimeDir() / "trace_data").u8string(); cfg.jsonDataPath = runtimeJsonPath("jsondata.txt").u8string(); cfg.outputPath = (runtimeDir() / "final_sorted.xls").u8string(); return cfg; } /** * Function: pqTraceDirectoryFor * * @brief 实现外部通信层的 “pqTraceDirectoryFor” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string pqTraceDirectoryFor(const PqToolConfig& cfg, const PqLedgerDevice& device, const PqLedgerMonitor& monitor) { fs::path root = fs::u8path(cfg.traceRootDir.empty() ? "trace_data" : cfg.traceRootDir); // 路径中同时放名称和 id,既方便人读,也保证同名测点不互相覆盖。 std::string proc = safePathPart(firstNonEmpty(device.processNo, "1"), "1"); std::string dev = safePathPart(firstNonEmpty(device.name, device.id), "device"); std::string mon = safePathPart(firstNonEmpty(monitor.name, monitor.id), "monitor"); if (!device.id.empty()) dev += "_" + safePathPart(device.id, "id"); if (!monitor.id.empty()) mon += "_" + safePathPart(monitor.id, "id"); fs::path dir = root / fs::u8path("process_" + proc) / fs::u8path("device_" + dev) / fs::u8path("monitor_" + mon); return dir.u8string(); } /** * Function: pqClearTraceRecords * * @brief 实现外部通信层的 “pqClearTraceRecords” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 */ void pqClearTraceRecords(const PqToolConfig& cfg, const PqLedgerDevice& device, const PqLedgerMonitor& monitor) { fs::path dir = fs::u8path(pqTraceDirectoryFor(cfg, device, monitor)); fs::create_directories(dir); clearTraceTextRecords(dir); } /** * Function: pqFindDeviceXmlPath * * @brief 实现外部通信层的 “pqFindDeviceXmlPath” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string pqFindDeviceXmlPath(const PqToolConfig& cfg, const PqLedgerDevice& device) { if (!device.xmlPath.empty() && fs::exists(fs::u8path(device.xmlPath))) return device.xmlPath; // 下载 ICD 后会把接口响应缓存到 runtime\json\icd_response.json,这里用 devType 反查本地 XML。 fs::path saveDir = fs::u8path(cfg.xmlSaveDir.empty() ? "downloaded_xml" : cfg.xmlSaveDir); auto existing = [&](const std::string& name) -> std::string { if (name.empty()) return ""; fs::path p = saveDir / fs::u8path(name); if (fs::exists(p)) return p.u8string(); return ""; }; fs::path icdCache = runtimeJsonPath("icd_response.json"); if (fs::exists(icdCache)) { try { std::string raw; { std::ifstream ifs(icdCache, std::ios::binary); std::ostringstream ss; ss << ifs.rdbuf(); raw = ss.str(); } json arr = parseResponseDataArray(raw, "ICD"); for (const auto& model : arr) { if (jsonString(model, "devType") != device.devType) continue; std::string byId = existing(jsonString(model, "id") + ".xml"); if (!byId.empty()) return byId; std::string fileName = jsonString(model, "fileName"); std::string byFile = existing(fileName + ".xml"); if (!byFile.empty()) return byFile; } } catch (...) { } } std::vector guesses = { device.devType + ".xml", device.devKey + ".xml", device.id + ".xml" }; for (const auto& g : guesses) { std::string p = existing(g); if (!p.empty()) return p; } if (!cfg.xmlPath.empty() && fs::exists(fs::u8path(cfg.xmlPath))) return cfg.xmlPath; return ""; } /** * Function: pqFetchLedgerAndIcd * * @brief 实现外部通信层的 “pqFetchLedgerAndIcd” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param devices 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param firstXmlPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg, std::vector& devices, std::string& firstXmlPath, PqLogFn log) { devices.clear(); firstXmlPath.clear(); // 1. 请求台账:frontIp + terminalStatus/runFlag。 json ledgerReq; ledgerReq["ip"] = cfg.frontIp; try { ledgerReq["runFlag"] = json::parse(cfg.terminalStatus); } catch (...) { ledgerReq["runFlag"] = cfg.terminalStatus; } if (log) log("获取台账: " + ledgerReq.dump()); std::string ledgerRaw = httpRequest(cfg.webDevice, "", ledgerReq.dump(), log); writeTextFile(runtimeJsonPath("ledger_response.json"), ledgerRaw); std::unordered_set terminalAllowed = parseStatusSet(cfg.terminalStatus); std::set devTypes; // 2. 解析装置和 monitorData 测点,并按 terminalStatus 过滤装置。 json data = parseResponseDataArray(ledgerRaw, "台账"); for (const auto& item : data) { PqLedgerDevice dev; dev.id = jsonString(item, "id"); dev.name = jsonString(item, "name"); dev.ip = jsonString(item, "ip"); dev.status = jsonString(item, "status"); dev.devType = jsonString(item, "devType"); dev.devKey = jsonString(item, "devKey"); dev.orgName = jsonString(item, "org_name"); dev.stationName = jsonString(item, "stationName"); dev.subName = jsonString(item, "subName"); dev.manufacturer = jsonString(item, "manufacturer"); dev.port = jsonString(item, "port"); dev.series = jsonString(item, "series"); dev.updateTime = jsonString(item, "updateTime"); dev.processNo = jsonString(item, "processNo"); dev.maxProcessNum = jsonString(item, "maxProcessNum"); dev.fields = objectFields(item, {"monitorData"}); if (!setContains(terminalAllowed, dev.status)) continue; if (!dev.devType.empty()) devTypes.insert(dev.devType); auto monitors = item.find("monitorData"); if (monitors != item.end() && monitors->is_array()) { for (const auto& m : *monitors) { PqLedgerMonitor mon; mon.id = jsonString(m, "id"); mon.name = jsonString(m, "name"); mon.lineNo = jsonString(m, "lineNo"); mon.voltageLevel = jsonString(m, "voltageLevel"); mon.connectType = jsonString(m, "ptType"); mon.status = jsonString(m, "status"); mon.fields = objectFields(m); dev.monitors.push_back(mon); } } devices.push_back(dev); } if (log) log("台账解析完成: 装置 " + std::to_string(devices.size()) + " 个"); // 3. 请求 ICD 列表。icdFlag=1 时只请求本次台账中出现的 devType。 json icdReq = json::array(); if (cfg.icdFlag == "1") { for (const auto& t : devTypes) icdReq.push_back(t); } if (log) log("获取 ICD 列表: " + icdReq.dump()); std::string icdRaw = httpRequest(cfg.webIcd, "", icdReq.dump(), log); writeTextFile(runtimeJsonPath("icd_response.json"), icdRaw); fs::path saveDir = fs::u8path(cfg.xmlSaveDir.empty() ? "downloaded_xml" : cfg.xmlSaveDir); fs::create_directories(saveDir); json icdData = parseResponseDataArray(icdRaw, "ICD"); int downloaded = 0; std::unordered_map devTypeToXml; // 4. 按 filePath 下载 XML,保存到 xmlSaveDir,并建立 devType -> XML 路径映射。 for (const auto& model : icdData) { std::string modelId = jsonString(model, "id"); std::string devType = jsonString(model, "devType"); std::string fileName = jsonString(model, "fileName"); std::string filePath = jsonString(model, "filePath"); if (filePath.empty()) continue; std::string localName = !modelId.empty() ? (sanitizeFilePart(modelId) + ".xml") : !fileName.empty() ? sanitizeFilePart(fileName) : (sanitizeFilePart(devType) + ".xml"); fs::path localPath = saveDir / fs::u8path(localName); std::string xml = httpRequest(cfg.webFiledownload, "filePath=" + urlEncode(filePath), "", log); if (!looksLikeXml(xml)) { if (log) log("跳过非 XML 下载结果: " + filePath); continue; } writeTextFile(localPath, xml); if (!devType.empty() && !devTypeToXml.count(devType)) devTypeToXml[devType] = localPath.u8string(); if (firstXmlPath.empty()) firstXmlPath = localPath.u8string(); ++downloaded; if (log) log("已下载 XML: " + localPath.u8string()); } if (log) log("ICD/XML 下载完成: " + std::to_string(downloaded) + " 个文件"); for (auto& dev : devices) { auto it = devTypeToXml.find(dev.devType); if (it != devTypeToXml.end()) dev.xmlPath = it->second; } return true; } /** * Function: pqInitializeMq * * @brief 实现外部通信层的 “pqInitializeMq” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @param event 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool pqInitializeMq(const PqToolConfig& cfg, PqLogFn log, PqMqEventFn event) { // 初始化时先把界面配置落盘,便于用户核对,也便于离线排查 MQ 参数。 json saved = { {"producer", cfg.producer}, {"ipport", cfg.ipport}, {"producerAccessKey", cfg.producerAccessKey}, {"producerSecretKey", cfg.producerSecretKey}, {"topicLOG", cfg.topicLOG}, {"tagLOG", cfg.tagLOG}, {"keyLOG", cfg.keyLOG}, {"topicAsk", cfg.topicAsk}, {"tagAsk", cfg.tagAsk}, {"keyAsk", cfg.keyAsk}, {"consumer", cfg.consumer}, {"consumerIpport", cfg.consumerIpport}, {"consumerAccessKey", cfg.consumerAccessKey}, {"consumerSecretKey", cfg.consumerSecretKey}, {"dataStatTopic", cfg.dataStatTopic}, {"dataStatTag", cfg.dataStatTag}, {"dataStatKey", cfg.dataStatKey}, {"dataRealtimeTopic", cfg.dataRealtimeTopic}, {"dataRealtimeTag", cfg.dataRealtimeTag}, {"dataRealtimeKey", cfg.dataRealtimeKey}, {"activeDataTopic", cfg.dataTopic}, {"dataTag", cfg.dataTag}, {"dataKey", cfg.dataKey}, {"traceTopic", cfg.traceTopic}, {"traceTag", cfg.traceTag}, {"traceKey", cfg.traceKey} }; writeTextFile(runtimeJsonPath("mq_runtime_config.json"), saved.dump(2)); if (log) log("MQ config saved to " + runtimeJsonPath("mq_runtime_config.json").u8string()); if (!rocketRuntime().initialize(cfg, log, event) && log) { // 这里仍返回 true,让 GUI 可以继续构造追踪请求;发送失败时会进入 outbox 兜底。 log("RocketMQ runtime is not active; trace messages will be written to mq_outbox_*.jsonl."); log("RocketMQ source is present, but a 32-bit rocketmq-client-cpp.dll or rocketmq.dll is still required."); } return true; if (log) { log("MQ 配置已初始化并写入 mq_runtime_config.json"); log("当前 Windows 编译未链接 RocketMQ SDK,发送消息会先落地到 mq_outbox_*.jsonl。"); log("接入 RocketMQ DLL 后只需要替换 mq.cpp 中 pqInitializeMq/pqSendTraceRequest 的适配实现。"); } return true; } /** * Function: pqBuildTracePayload * * @brief 实现外部通信层的 “pqBuildTracePayload” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param frontType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string pqBuildTracePayload(const PqToolConfig& cfg, const PqLedgerDevice& device, const PqLedgerMonitor& monitor, const std::string& frontType) { std::string guid = makeGuid(); int processNo = 0; if (frontType == "cfg_3s_data") { // 实时数据进程约定为 0;统计数据使用台账中的 processNo。 processNo = 0; } else { try { processNo = device.processNo.empty() ? 0 : std::stoi(device.processNo); } catch (...) { processNo = 0; } } // body 是前置识别的 set_log 指令;outer 是 RocketMQ 业务包装。 json body = { {"code", "set_log"}, {"nodeId", cfg.frontInst}, {"guid", guid}, {"processNo", processNo}, {"id", monitor.id}, {"level", "measurepoint"}, {"grade", "TRACE"}, {"logtype", 1}, {"frontType", frontType} }; json outer = { {"key", guid}, {"source", "WEB"}, {"sendTime", nowIsoMillis()}, {"retryTimes", 0}, {"messageBody", body.dump()} }; return outer.dump(); } /** * Function: buildRealtimeAskPayload * * @brief 构造 “buildRealtimeAskPayload” 对应的对象、消息、窗口控件或输出结构。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string buildRealtimeAskPayload(const PqLedgerDevice& device, const PqLedgerMonitor& monitor) { const std::string guid = makeGuid(); json body = { {"devSeries", device.id}, {"limit", 20}, {"line", monitor.id}, {"realData", true}, {"soeData", true} }; json outer = { {"key", guid}, {"source", "WEB"}, {"sendTime", nowIsoMillis()}, {"retryTimes", 0}, {"messageBody", body.dump()} }; return outer.dump(); } /** * Function: pqSendTraceRequest * * @brief 实现外部通信层的 “pqSendTraceRequest” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。 * @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem;异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。 * @param cfg 运行配置;提供接口地址、MQ 参数、路径和当前数据模式。 * @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param frontType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param log 回调函数;用于向界面或日志系统报告进度和结果。 * @param event 回调函数;用于向界面或日志系统报告进度和结果。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool pqSendTraceRequest(const PqToolConfig& cfg, const PqLedgerDevice& device, const PqLedgerMonitor& monitor, const std::string& frontType, PqLogFn log, PqMqEventFn event) { std::string payload = pqBuildTracePayload(cfg, device, monitor, frontType); std::string guid = guidFromJsonText(payload); // 先设置追踪目标,确保追踪请求发出后立即到达的 MQ 回调也能正确路由。 rocketRuntime().setTraceTarget(cfg, device, monitor, frontType, guid); fs::path traceDir = fs::u8path(pqTraceDirectoryFor(cfg, device, monitor)); fs::create_directories(traceDir); fs::path sentPath = traceDir / "sent_trace_request.json"; writeTextFile(sentPath, prettyJsonOrRaw(payload)); writeTextFile(runtimeJsonPath("last_trace_request.json"), json::parse(payload).dump(2)); if (event) event({"sent", eventTitle("Send", sentPath.u8string()), payload, sentPath.u8string(), monitor.id, traceDir.u8string()}); auto sendOrSave = [&](const std::string& topic, const std::string& tag, const std::string& messageKey, const std::string& body, const std::string& label) { std::string mqError; if (rocketRuntime().sendPayload(topic, tag, messageKey, body, label, mqError, log)) return; // RocketMQ 未初始化或发送失败时不丢请求,按目标 Topic 追加到对应 outbox。 std::string topicPart = sanitizeFilePart(topic.empty() ? label : topic); fs::path outbox = runtimeJsonPath("mq_outbox_" + topicPart + ".jsonl"); std::ofstream ofs(outbox, std::ios::binary | std::ios::app); if (!ofs) throw std::runtime_error("无法写入 MQ outbox: " + outbox.string()); ofs << body << "\n"; if (log) log(label + "消息发送失败(" + mqError + "),已写入 " + outbox.string()); }; // 第一条始终先发到 TopicLOG,请求前置建立本次追踪日志。 sendOrSave(cfg.topicLOG, cfg.tagLOG, cfg.keyLOG, payload, "set_log"); // cfg_3s_data 紧接着向 TopicAsk 请求该装置、测点的实时/SOE 数据。 if (frontType == "cfg_3s_data") { std::string askPayload = buildRealtimeAskPayload(device, monitor); fs::path askPath = traceDir / "sent_realtime_request.json"; writeTextFile(askPath, prettyJsonOrRaw(askPayload)); writeTextFile(runtimeJsonPath("last_realtime_request.json"), json::parse(askPayload).dump(2)); if (event) event({"sent", eventTitle("Send Ask", askPath.u8string()), askPayload, askPath.u8string(), monitor.id, traceDir.u8string()}); sendOrSave(cfg.topicAsk, cfg.tagAsk, cfg.keyAsk, askPayload, "realtime ask"); } if (log) log("Trace target monitor=" + monitor.name + "(" + monitor.id + "), frontType=" + frontType); return true; }