Files
datatrace/source/mq.cpp
2026-07-14 17:02:23 +08:00

2380 lines
122 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define NOMINMAX
#include "pq_app.h"
#include "json.hpp"
#include <windows.h>
#include <winhttp.h>
#include <objbase.h>
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cwctype>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <map>
#include <mutex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <unordered_set>
/*
* 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<std::string>();
if (it->is_number_integer()) return std::to_string(it->get<long long>());
if (it->is_number_unsigned()) return std::to_string(it->get<unsigned long long>());
if (it->is_number_float()) return std::to_string(it->get<double>());
if (it->is_boolean()) return it->get<bool>() ? "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<std::string>& 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<std::string> parseStatusSet(const std::string& text) {
std::unordered_set<std::string> 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<std::pair<std::string, std::string>> objectFields(
const json& obj,
const std::unordered_set<std::string>& skip = {}) {
std::vector<std::pair<std::string, std::string>> 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("<?xml", 0) == 0 || s.rfind("<", 0) == 0;
}
/**
* Function: makeGuid
*
* @brief 构造 “makeGuid” 对应的对象、消息、窗口控件或输出结构。
* @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string makeGuid() {
GUID guid;
if (CoCreateGuid(&guid) != S_OK) {
auto now = std::chrono::high_resolution_clock::now().time_since_epoch().count();
return "guid_" + std::to_string(now);
}
wchar_t buf[64]{};
StringFromGUID2(guid, buf, 64);
std::string s = wideToUtf8(buf);
s.erase(std::remove(s.begin(), s.end(), '{'), s.end());
s.erase(std::remove(s.begin(), s.end(), '}'), s.end());
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) { return (char)std::tolower(ch); });
return s;
}
/**
* Function: nowIsoMillis
*
* @brief 实现外部通信层的 “nowIsoMillis” 功能,服务于 HTTP、RocketMQ 或运行时文件管理。
* @details 本文件组合使用 WinHTTP、Windows 动态库 API、RocketMQ C 接口、nlohmann::json 与 std::filesystem异常会转换为日志、错误字符串或离线 outbox。共享 MQ 会话状态由互斥锁保护。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string nowIsoMillis() {
using namespace std::chrono;
auto now = system_clock::now();
auto ms = duration_cast<milliseconds>(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::milliseconds>(
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<long long>();
if (value.is_number_unsigned()) return static_cast<long long>(value.get<unsigned long long>());
if (value.is_number_float()) return static_cast<long long>(value.get<double>());
if (value.is_string()) return std::stoll(value.get<std::string>());
} 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<std::string> 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<std::string>();
if (value.is_number_integer()) return std::to_string(value.get<long long>());
if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
if (value.is_number_float()) {
std::ostringstream oss;
oss << value.get<double>();
return oss.str();
}
if (value.is_boolean()) return value.get<bool>() ? "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<unsigned char>(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<std::string>();
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<std::string>();
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<unsigned char>(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<char>(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<std::string> receivedDataTypes; // 同一次追踪中每个 DATA_TYPE 只保留一次。
std::unordered_set<std::string> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string, std::string> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string> 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<std::string> 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<std::string> missing;
#define LOAD_MQ_FN(name) \
do { \
name = reinterpret_cast<decltype(name)>(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<std::string, TraceSession> sessions_;
std::unordered_map<std::string, std::string> 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<std::string> 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<PqLedgerDevice>& 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<std::string> terminalAllowed = parseStatusSet(cfg.terminalStatus);
std::set<std::string> 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<std::string, std::string> 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;
}