///////////////////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include //流解析 #include //写去重的设备类型 #include //打开文件 #include #include #include #include #include #include #include #include #include #include #include #include #include #include ///////////////////////////////////////////////////////////////////////////////////////////////// #include "nlohmann/json.hpp" #include "curl/curl.h" #include "log4.h" //关键上送日志 #include "interface.h" //台账结构 #include "tinyxml2.h" #include "rocketmq.h" ///////////////////////////////////////////////////////////////////////////////////////////////// using namespace std; ///////////////////////////////////////////////////////////////////////////////////////////////// //进程标识 extern std::string subdir; extern int g_front_seg_index; extern int g_front_seg_num; extern unsigned int g_node_id; //前置程序类型(100-600) //初始化完成标识 extern int INITFLAG; //线程阻塞计数 extern uint32_t g_ontime_blocked_times; //台账锁 extern std::mutex ledgermtx; //队列 extern std::mutex queue_data_list_mutex; //queue发送数据锁 extern std::list queue_data_list; //queue发送数据链表 extern int three_secs_enabled; extern std::map xmlinfo_list;//保存所有型号对应的icd映射文件解析数据 extern XmlConfig xmlcfg;//星形接线xml节点解析的数据-默认映射文件解析数据 extern std::list topicList; //队列发送主题链表 extern XmlConfig xmlcfg2;//角型接线xml节点解析的数据-默认映射文件解析数据 extern std::list topicList2; //角型接线发送主题链表 extern std::map xmlinfo_list2;//保存所有型号角形接线对应的icd映射文件解析数据 ////////////////////////////////////////////////////////////////////////////////////////////////// extern time_t ConvertToTimestamp(const tagTime& time); //////////////////////////////////////////////////////////////////////////////////////////////////// //补招记录文件 std::mutex g_recall_file_mtx; std::map, std::string> g_recall_file_index; //实时数据时间记录 std::mutex g_last_ts_mtx; std::unordered_map g_last_ts_by_devid; static std::mutex g_filemenu_cache_mtx; std::map> g_filemenu_cache; //补招 std::list g_StatisticLackList; //日志补招结构类链表 std::mutex g_StatisticLackList_list_mutex; //补招队列数据锁 std::string DEFAULT_CONFIG_FN = "Default_Config.xml"; //默认映射文件 std::string LEDGER_UPDATE_FN = "LedgerUpdate.log"; //台账更新本地记录日志 //一个终端最大检测点数 const int MAX_CPUNO = 10; //角型接线标志,0不存在角形接线,1存在角形接线 int isdelta_flag = 0; //多前置flag:1为开启,0为关闭 int MULTIPLE_NODE_FLAG = 1; //终端台账数量配置 int IED_COUNT = 300; //默认300 //前置标志 std::string FRONT_INST = ""; std::string FRONT_IP = ""; //终端和监测点的状态筛选 std::string TERMINAL_STATUS = ""; std::string MONITOR_STATUS = ""; std::string ICD_FLAG = ""; //web接口 std::string WEB_DEVICE = ""; std::string WEB_ICD = ""; std::string WEB_EVENT = ""; std::string WEB_FILEUPLOAD = ""; std::string WEB_FILEDOWNLOAD = ""; ////////////////////////////////////////////////////////////////////////////mq配置 //备用 std::string BROKER_LIST = ""; //通用主题 std::string TOPIC_STAT = ""; std::string TOPIC_PST = ""; std::string TOPIC_PLT = ""; std::string TOPIC_EVENT = ""; std::string TOPIC_ALARM = ""; std::string TOPIC_SNG = ""; std::string TOPIC_RTDATA = ""; //通用tagkey std::string G_ROCKETMQ_TAG = "";//tag std::string G_ROCKETMQ_KEY = "";//key //实时数据tagkey std::string G_RT_TAG = "";//tag std::string G_RT_KEY = "";//key //生产者 std::string G_ROCKETMQ_PRODUCER = ""; //rocketmq producer std::string G_MQPRODUCER_IPPORT = ""; //rocketmq ip+port std::string G_MQPRODUCER_ACCESSKEY = ""; //rocketmq 认证 std::string G_MQPRODUCER_SECRETKEY = ""; //rocketmq 秘钥 //日志 std::string G_LOG_TOPIC = "";//topie std::string G_LOG_TAG = "";//tag std::string G_LOG_KEY = "";//key //终端连接 std::string G_CONNECT_TOPIC = "";//consumer topie std::string G_CONNECT_TAG = "";//consumer tag std::string G_CONNECT_KEY = "";//consumer key //心跳 std::string Heart_Beat_Topic = ""; std::string Heart_Beat_Tag = ""; std::string Heart_Beat_Key = ""; //消息响应 std::string Topic_Reply_Topic = ""; std::string Topic_Reply_Tag = ""; std::string Topic_Reply_Key = ""; //消费者 std::string G_ROCKETMQ_CONSUMER = "";//rocketmq consumer std::string G_MQCONSUMER_IPPORT = "";//consumer ip+port std::string G_MQCONSUMER_ACCESSKEY = ""; std::string G_MQCONSUMER_SECRETKEY = ""; std::string G_MQCONSUMER_CHANNEL = ""; //实时数据请求 std::string G_MQCONSUMER_TOPIC_RT = "";//consumer topie std::string G_MQCONSUMER_TAG_RT = "";//consumer tag std::string G_MQCONSUMER_KEY_RT = "";//consumer key //台账更新请求 std::string G_MQCONSUMER_TOPIC_UD = "";//consumer topie std::string G_MQCONSUMER_TAG_UD = "";//consumer tag std::string G_MQCONSUMER_KEY_UD = "";//consumer key //补招数据请求 std::string G_MQCONSUMER_TOPIC_RC = "";//consumer topie std::string G_MQCONSUMER_TAG_RC = "";//consumer tag std::string G_MQCONSUMER_KEY_RC = "";//consumer key //进程控制请求 std::string G_MQCONSUMER_TOPIC_SET = "";//consumer topie std::string G_MQCONSUMER_TAG_SET = "";//consumer tag std::string G_MQCONSUMER_KEY_SET = "";//consumer key //日志数据请求 std::string G_MQCONSUMER_TOPIC_LOG = "";//consumer topie std::string G_MQCONSUMER_TAG_LOG = "";//consumer tag std::string G_MQCONSUMER_KEY_LOG = "";//consumer key std::string G_MQCONSUMER_TOPIC_CLOUD = "";//consumer topie std::string G_MQCONSUMER_TAG_CLOUD = "";//consumer tag std::string G_MQCONSUMER_KEY_CLOUD = "";//consumer key //测试用的主题 std::string G_ROCKETMQ_TOPIC_TEST = "";//topie std::string G_ROCKETMQ_TAG_TEST = "";//tag std::string G_ROCKETMQ_KEY_TEST = "";//key //测试相关配置 int G_TEST_FLAG = 0; int G_TEST_NUM = 0; int G_TEST_TYPE = 0; int LEDGER_MAX_ITEMS = 5; //台账打印最大项数限制 int TEST_PORT = 11000; //用于当前进程登录测试shell的端口 std::string G_TEST_LIST = ""; //测试用的发送实际数据的终端列表 std::vector TESTARRAY; //解析的列表数组 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////其他文件定义的函数引用声明 bool enqueue_direct_download(const std::string& dev_id, const std::string& monitor_id, const std::string& filename, const std::string& guid); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////当前文件函数声明 void create_ledger_log(trigger_update_xml_t* ledger_update_xml); void print_trigger_update_xml(const trigger_update_xml_t& trigger_update); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////辅助函数 //去除字符串前后空格 static void trim(std::string& s) { auto not_space = [](int ch) { return !std::isspace(ch); }; s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space)); s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end()); } //判断空格 bool is_blank(const std::string& str) { for (std::string::const_iterator it = str.begin(); it != str.end(); ++it) { if (!std::isspace(*it)) { return false; } } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////配置文件读取 //将 G_TEST_LIST 按逗号分割,填充 TESTARRAY static void parseTestList() { TESTARRAY.clear(); std::istringstream iss(G_TEST_LIST); std::string token; while (std::getline(iss, token, ',')) { trim(token); if (!token.empty()) { TESTARRAY.push_back(token); } } } //简易映射方式解析配置文件文件 void loadConfig(const std::string& filename) { // 1. 构造“节.键 → 变量地址”映射表 std::unordered_map strMap; std::unordered_map intMap; // [Flag] strMap["Flag.FrontInst"] = &FRONT_INST; strMap["Flag.FrontIP"] = &FRONT_IP; // [Ledger] intMap["Ledger.IedCount"] = &IED_COUNT; strMap["Ledger.TerminalStatus"]= &TERMINAL_STATUS; strMap["Ledger.MonitorStatus"] = &MONITOR_STATUS; strMap["Ledger.IcdFlag"] = &ICD_FLAG; // [Http] strMap["Http.WebDevice"] = &WEB_DEVICE; strMap["Http.WebIcd"] = &WEB_ICD; strMap["Http.WebEvent"] = &WEB_EVENT; strMap["Http.WebFileupload"] = &WEB_FILEUPLOAD; strMap["Http.WebFiledownload"]= &WEB_FILEDOWNLOAD; // [Queue] strMap["Queue.BrokerList"] = &BROKER_LIST; strMap["Queue.RTDataTopic"] = &TOPIC_RTDATA; strMap["Queue.HisTopic"] = &TOPIC_STAT; strMap["Queue.PSTTopic"] = &TOPIC_PST; strMap["Queue.PLTTopic"] = &TOPIC_PLT; strMap["Queue.EventTopic"] = &TOPIC_EVENT; strMap["Queue.AlmTopic"] = &TOPIC_ALARM; strMap["Queue.SngTopic"] = &TOPIC_SNG; strMap["Queue.QUEUE_TAG"] = &G_ROCKETMQ_TAG; strMap["Queue.QUEUE_KEY"] = &G_ROCKETMQ_KEY; //添加rt的tagkey strMap["Queue.RT_TAG"] = &G_RT_TAG; strMap["Queue.RT_KEY"] = &G_RT_KEY; // [RocketMq] —— 生产者 strMap["RocketMq.producer"] = &G_ROCKETMQ_PRODUCER; strMap["RocketMq.Ipport"] = &G_MQPRODUCER_IPPORT; strMap["RocketMq.AccessKey"] = &G_MQPRODUCER_ACCESSKEY; strMap["RocketMq.SecretKey"] = &G_MQPRODUCER_SECRETKEY; strMap["RocketMq.LOGTopic"] = &G_LOG_TOPIC; strMap["RocketMq.LOGTag"] = &G_LOG_TAG; strMap["RocketMq.LOGKey"] = &G_LOG_KEY; strMap["RocketMq.CONNECTTopic"] = &G_CONNECT_TOPIC; strMap["RocketMq.CONNECTTag"] = &G_CONNECT_TAG; strMap["RocketMq.CONNECTKey"] = &G_CONNECT_KEY; strMap["RocketMq.Heart_Beat_Topic"] = &Heart_Beat_Topic; strMap["RocketMq.Heart_Beat_Tag"] = &Heart_Beat_Tag; strMap["RocketMq.Heart_Beat_Key"] = &Heart_Beat_Key; strMap["RocketMq.Topic_Reply_Topic"] = &Topic_Reply_Topic; strMap["RocketMq.Topic_Reply_Tag"] = &Topic_Reply_Tag; strMap["RocketMq.Topic_Reply_Key"] = &Topic_Reply_Key; // [RocketMq] —— 消费者 strMap["RocketMq.consumer"] = &G_ROCKETMQ_CONSUMER; strMap["RocketMq.ConsumerIpport"] = &G_MQCONSUMER_IPPORT; strMap["RocketMq.ConsumerAccessKey"] = &G_MQCONSUMER_ACCESSKEY; strMap["RocketMq.ConsumerSecretKey"] = &G_MQCONSUMER_SECRETKEY; strMap["RocketMq.ConsumerChannel"] = &G_MQCONSUMER_CHANNEL; strMap["RocketMq.ConsumerTopicRT"] = &G_MQCONSUMER_TOPIC_RT; strMap["RocketMq.ConsumerTagRT"] = &G_MQCONSUMER_TAG_RT; strMap["RocketMq.ConsumerKeyRT"] = &G_MQCONSUMER_KEY_RT; strMap["RocketMq.ConsumerTopicUD"] = &G_MQCONSUMER_TOPIC_UD; strMap["RocketMq.ConsumerTagUD"] = &G_MQCONSUMER_TAG_UD; strMap["RocketMq.ConsumerKeyUD"] = &G_MQCONSUMER_KEY_UD; strMap["RocketMq.ConsumerTopicRC"] = &G_MQCONSUMER_TOPIC_RC; strMap["RocketMq.ConsumerTagRC"] = &G_MQCONSUMER_TAG_RC; strMap["RocketMq.ConsumerKeyRC"] = &G_MQCONSUMER_KEY_RC; strMap["RocketMq.ConsumerTopicSET"] = &G_MQCONSUMER_TOPIC_SET; strMap["RocketMq.ConsumerTagSET"] = &G_MQCONSUMER_TAG_SET; strMap["RocketMq.ConsumerKeySET"] = &G_MQCONSUMER_KEY_SET; strMap["RocketMq.ConsumerTopicLOG"] = &G_MQCONSUMER_TOPIC_LOG; strMap["RocketMq.ConsumerTagLOG"] = &G_MQCONSUMER_TAG_LOG; strMap["RocketMq.ConsumerKeyLOG"] = &G_MQCONSUMER_KEY_LOG; strMap["RocketMq.ConsumerTopicCLOUD"] = &G_MQCONSUMER_TOPIC_CLOUD; strMap["RocketMq.ConsumerTagCLOUD"] = &G_MQCONSUMER_TAG_CLOUD; strMap["RocketMq.ConsumerKeyCLOUD"] = &G_MQCONSUMER_KEY_CLOUD; strMap["RocketMq.Topic_Test"] = &G_ROCKETMQ_TOPIC_TEST; strMap["RocketMq.Tag_Test"] = &G_ROCKETMQ_TAG_TEST; strMap["RocketMq.Key_Test"] = &G_ROCKETMQ_KEY_TEST; intMap["RocketMq.Testflag"] = &G_TEST_FLAG; intMap["RocketMq.Testnum"] = &G_TEST_NUM; intMap["RocketMq.Testtype"] = &G_TEST_TYPE; intMap["RocketMq.TestPort"] = &TEST_PORT; strMap["RocketMq.TestList"] = &G_TEST_LIST; // 2. 打开并逐行解析 INI 文件 std::ifstream fin(filename); if (!fin.is_open()) { std::cerr << "无法打开配置文件: " << filename << "\n"; return; } std::string line; std::string currentSection; while (std::getline(fin, line)) { trim(line); if (line.empty() || line[0] == ';' || line[0] == '#') { continue; // 跳过空白或注释 } if (line.front() == '[' && line.back() == ']') { currentSection = line.substr(1, line.size() - 2); trim(currentSection); continue; } auto pos = line.find('='); if (pos == std::string::npos) { continue; } std::string key = line.substr(0, pos); std::string value = line.substr(pos + 1); trim(key); trim(value); // 去掉值两端双引号 if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { value = value.substr(1, value.size() - 2); } // 拼出 "节.键" std::string fullKey = currentSection + "." + key; // 如果在字符串映射表里,就写入对应的全局 std::string auto sit = strMap.find(fullKey); if (sit != strMap.end()) { *(sit->second) = value; continue; } // 如果在整型映射表里,就 stoi 后写入对应的全局 int auto iit = intMap.find(fullKey); if (iit != intMap.end()) { try { *(iit->second) = std::stoi(value); } catch (...) { *(iit->second) = 0; } } } fin.close(); // 3. 将 G_TEST_LIST 拆分到 TESTARRAY parseTestList(); } //打印所有全局变量,名称对齐 void printConfig() { const int nameWidth = 30; // 变量名区域宽度 std::cout << "------- Loaded Configuration -------\n"; // 辅助 lambda 方便打印 auto printStr = [&](const std::string& name, const std::string& val) { std::cout << std::left << std::setw(nameWidth) << name << " = " << val << "\n"; }; auto printInt = [&](const std::string& name, int val) { std::cout << std::left << std::setw(nameWidth) << name << " = " << val << "\n"; }; std::cout << "\n// 前置区分 —— 通用\n"; printInt("IED_COUNT", IED_COUNT); printStr("FRONT_INST", FRONT_INST); printStr("FRONT_IP", FRONT_IP); std::cout << "\n// 消息队列 —— 通用\n"; printStr("BROKER_LIST", BROKER_LIST); printStr("TOPIC_STAT", TOPIC_STAT); printStr("TOPIC_PST", TOPIC_PST); printStr("TOPIC_PLT", TOPIC_PLT); printStr("TOPIC_EVENT", TOPIC_EVENT); printStr("TOPIC_ALARM", TOPIC_ALARM); printStr("TOPIC_SNG", TOPIC_SNG); printStr("TOPIC_RTDATA", TOPIC_RTDATA); std::cout << "\n// MQ —— 生产者\n"; printStr("G_ROCKETMQ_PRODUCER", G_ROCKETMQ_PRODUCER); printStr("G_MQPRODUCER_IPPORT", G_MQPRODUCER_IPPORT); printStr("G_MQPRODUCER_ACCESSKEY", G_MQPRODUCER_ACCESSKEY); printStr("G_MQPRODUCER_SECRETKEY", G_MQPRODUCER_SECRETKEY); printStr("G_LOG_TOPIC", G_LOG_TOPIC); printStr("G_LOG_TAG", G_LOG_TAG); printStr("G_LOG_KEY", G_LOG_KEY); printStr("G_CONNECT_TOPIC", G_CONNECT_TOPIC); printStr("G_CONNECT_TAG", G_CONNECT_TAG); printStr("G_CONNECT_KEY", G_CONNECT_KEY); std::cout << "\n// MQ —— 心跳 & 响应\n"; printStr("Heart_Beat_Topic", Heart_Beat_Topic); printStr("Heart_Beat_Tag", Heart_Beat_Tag); printStr("Heart_Beat_Key", Heart_Beat_Key); printStr("Topic_Reply_Topic", Topic_Reply_Topic); printStr("Topic_Reply_Tag", Topic_Reply_Tag); printStr("Topic_Reply_Key", Topic_Reply_Key); std::cout << "\n// MQ —— 消费者\n"; printStr("G_ROCKETMQ_CONSUMER", G_ROCKETMQ_CONSUMER); printStr("G_MQCONSUMER_IPPORT", G_MQCONSUMER_IPPORT); printStr("G_MQCONSUMER_ACCESSKEY", G_MQCONSUMER_ACCESSKEY); printStr("G_MQCONSUMER_SECRETKEY", G_MQCONSUMER_SECRETKEY); printStr("G_MQCONSUMER_CHANNEL", G_MQCONSUMER_CHANNEL); std::cout << "\n// MQ —— 主题细分类\n"; printStr("G_MQCONSUMER_TOPIC_RT", G_MQCONSUMER_TOPIC_RT); printStr("G_MQCONSUMER_TAG_RT", G_MQCONSUMER_TAG_RT); printStr("G_MQCONSUMER_KEY_RT", G_MQCONSUMER_KEY_RT); printStr("G_MQCONSUMER_TOPIC_UD", G_MQCONSUMER_TOPIC_UD); printStr("G_MQCONSUMER_TAG_UD", G_MQCONSUMER_TAG_UD); printStr("G_MQCONSUMER_KEY_UD", G_MQCONSUMER_KEY_UD); printStr("G_MQCONSUMER_TOPIC_RC", G_MQCONSUMER_TOPIC_RC); printStr("G_MQCONSUMER_TAG_RC", G_MQCONSUMER_TAG_RC); printStr("G_MQCONSUMER_KEY_RC", G_MQCONSUMER_KEY_RC); printStr("G_MQCONSUMER_TOPIC_SET", G_MQCONSUMER_TOPIC_SET); printStr("G_MQCONSUMER_TAG_SET", G_MQCONSUMER_TAG_SET); printStr("G_MQCONSUMER_KEY_SET", G_MQCONSUMER_KEY_SET); printStr("G_MQCONSUMER_TOPIC_LOG", G_MQCONSUMER_TOPIC_LOG); printStr("G_MQCONSUMER_TAG_LOG", G_MQCONSUMER_TAG_LOG); printStr("G_MQCONSUMER_KEY_LOG", G_MQCONSUMER_KEY_LOG); std::cout << "\n// MQ —— 测试用主题 & 参数\n"; printStr("G_ROCKETMQ_TOPIC_TEST", G_ROCKETMQ_TOPIC_TEST); printStr("G_ROCKETMQ_TAG_TEST", G_ROCKETMQ_TAG_TEST); printStr("G_ROCKETMQ_KEY_TEST", G_ROCKETMQ_KEY_TEST); printInt("G_TEST_FLAG", G_TEST_FLAG); printInt("G_TEST_NUM", G_TEST_NUM); printInt("G_TEST_TYPE", G_TEST_TYPE); printInt("TEST_PORT", TEST_PORT); printStr("G_TEST_LIST", G_TEST_LIST); // 打印解析后的 TESTARRAY std::cout << std::left << std::setw(nameWidth) << "TESTARRAY" << " = ["; for (size_t i = 0; i < TESTARRAY.size(); ++i) { std::cout << TESTARRAY[i]; if (i + 1 < TESTARRAY.size()) { std::cout << ", "; } } std::cout << "]\n"; std::cout << "\n// 终端 & 监测点状态筛选\n"; printStr("TERMINAL_STATUS", TERMINAL_STATUS); printStr("MONITOR_STATUS", MONITOR_STATUS); printStr("ICD_FLAG", ICD_FLAG); std::cout << "\n// Web 接口\n"; printStr("WEB_DEVICE", WEB_DEVICE); printStr("WEB_ICD", WEB_ICD); printStr("WEB_EVENT", WEB_EVENT); printStr("WEB_FILEUPLOAD", WEB_FILEUPLOAD); printStr("WEB_FILEDOWNLOAD", WEB_FILEDOWNLOAD); std::cout << "-------------------------------------\n"; } //初始化配置 void init_config() { loadConfig(FRONT_PATH + "/config/front.cfg"); printConfig(); //多前置处理 if (g_front_seg_index > 0 && g_front_seg_num > 0) { MULTIPLE_NODE_FLAG = 1; std::cout << "this is multiple process of index:" << g_front_seg_index << std::endl; if(g_front_seg_index > g_front_seg_num){ DIY_ERRORLOG("process","【ERROR】前置当前进程的进程号为:%d,前置的多进程最大进程号为:%d,当前进程的进程号应该为1到最大进程号范围内的整数,退出该进程",g_front_seg_index,g_front_seg_num); exit(-1039); } } else if(g_front_seg_num == 0 && g_front_seg_index == 0){ MULTIPLE_NODE_FLAG = 0; std::cout << "this is single process" << std::endl; } else{ DIY_ERRORLOG("process","【ERROR】前置当前进程的进程号为:%d,前置的多进程最大进程号为:%d,应该为大于0的整数,退出该进程",g_front_seg_index,g_front_seg_num); exit(-1039); } //测试进程端口 /*if (g_node_id == STAT_DATA_BASE_NODE_ID)//统计采集 TEST_PORT = TEST_PORT + STAT_DATA_BASE_NODE_ID + g_front_seg_index; else if (g_node_id == RECALL_HIS_DATA_BASE_NODE_ID) {//补召 TEST_PORT = TEST_PORT + RECALL_HIS_DATA_BASE_NODE_ID + g_front_seg_index; } else if (g_node_id == THREE_SECS_DATA_BASE_NODE_ID) {//3秒采集 TEST_PORT = TEST_PORT + THREE_SECS_DATA_BASE_NODE_ID + g_front_seg_index; } else if (g_node_id == SOE_COMTRADE_BASE_NODE_ID) {//暂态录波 TEST_PORT = TEST_PORT + SOE_COMTRADE_BASE_NODE_ID + g_front_seg_index; }*/ TEST_PORT = TEST_PORT + g_front_seg_index; } ////////////////////////////////////////////////////////////////////////////////////////////获取当前时间 // 用于获取当前时间,单位毫秒 double sGetMsTime() { auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast( now.time_since_epoch() ).count(); return static_cast(ms); } //秒时间转为标准时间字符串 // ★新增:将 epoch 秒级时间转成 "YYYY-MM-DD HH:MM:SS" static std::string epoch_to_datetime_str(long long epoch_sec) { char buf[20]; std::tm tm{}; time_t t = static_cast(epoch_sec); localtime_r(&t, &tm); std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); return std::string(buf); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////实时触发部分 //获取实时触发文件 std::string get_3s_trig_fn() { const std::string dirPath = FRONT_PATH + "/etc/trigger3s"; DIR* dp = opendir(dirPath.c_str()); if (!dp) return ""; struct dirent* entry; std::vector> xmlFiles; while ((entry = readdir(dp)) != nullptr) { if (strstr(entry->d_name, ".xml")) { std::string fullPath = dirPath + "/" + entry->d_name; struct stat st; if (stat(fullPath.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { xmlFiles.emplace_back(entry->d_name, st.st_mtime); } } } closedir(dp); if (xmlFiles.empty()) return ""; std::sort(xmlFiles.begin(), xmlFiles.end(), [](const std::pair& a, const std::pair& b) { return a.second > b.second; // 最近时间在前 }); return xmlFiles.front().first; } // 打印 trigger_t 结构体的函数 void print_trigger(const trigger_t& trigger) { std::cout << " dev_idx: " << trigger.dev_idx << ", line_id: " << trigger.line_id << ", real_data: " << trigger.real_data << ", soe_data: " << trigger.soe_data << ", limit: " << trigger.limit << ", count: " << trigger.count << std::endl; } // 打印 trigger_3s_xml_t 结构体的函数 void print_trigger_3s_xml(const trigger_3s_xml_t& trigger_3s_xml) { std::cout << "Work Trigger Count: " << trigger_3s_xml.work_trigger_num << std::endl; for (int i = 0; i < trigger_3s_xml.work_trigger_num; ++i) { std::cout << " Work Trigger [" << (i + 1) << "]:" << std::endl; print_trigger(trigger_3s_xml.work_triggers[i]); } std::cout << "New Trigger Count: " << trigger_3s_xml.new_trigger_num << std::endl; for (int i = 0; i < trigger_3s_xml.new_trigger_num; ++i) { std::cout << " New Trigger [" << (i + 1) << "]:" << std::endl; print_trigger(trigger_3s_xml.new_triggers[i]); } std::cout << "Delete Trigger Count: " << trigger_3s_xml.delete_trigger_num << std::endl; for (int i = 0; i < trigger_3s_xml.delete_trigger_num; ++i) { std::cout << " Delete Trigger [" << (i + 1) << "]:" << std::endl; print_trigger(trigger_3s_xml.delete_triggers[i]); } std::cout << "Modify Trigger Count: " << trigger_3s_xml.modify_trigger_num << std::endl; for (int i = 0; i < trigger_3s_xml.modify_trigger_num; ++i) { std::cout << " Modify Trigger [" << (i + 1) << "]:" << std::endl; print_trigger(trigger_3s_xml.modify_triggers[i]); } } //处理触发标志 int getValueFromElemAttrStr(std::string str) { if (str == "true") return 1; else if (str == "false") return 0; else return -1; } //将文件内容读取到结构中 void parse_3s_trigger(trigger_3s_xml_t* trigger_3s_xml, const std::string& parentTag, tinyxml2::XMLElement* trigger_e) { if (!trigger_3s_xml || !trigger_e) return; trigger_t trigger; const char* attr = nullptr; // DevSeries attr = trigger_e->Attribute("DevSeries"); trigger.dev_idx = attr ? std::atoi(attr) : 0; // Line attr = trigger_e->Attribute("Line"); trigger.line_id = attr ? std::atoi(attr) : 0; // RealData attr = trigger_e->Attribute("RealData"); std::string realDataStr = attr ? attr : ""; std::transform(realDataStr.begin(), realDataStr.end(), realDataStr.begin(), ::tolower); trigger.real_data = getValueFromElemAttrStr(realDataStr); // SOEData attr = trigger_e->Attribute("SOEData"); std::string soeDataStr = attr ? attr : ""; std::transform(soeDataStr.begin(), soeDataStr.end(), soeDataStr.begin(), ::tolower); trigger.soe_data = getValueFromElemAttrStr(soeDataStr); // Limit attr = trigger_e->Attribute("Limit"); trigger.limit = attr ? std::atoi(attr) : 0; // Count attr = trigger_e->Attribute("Count"); trigger.count = attr ? std::atoi(attr) : 0; // 分类插入 if (parentTag == "Work") { trigger_3s_xml->work_triggers[trigger_3s_xml->work_trigger_num++] = trigger; } else if (parentTag == "New") { trigger_3s_xml->new_triggers[trigger_3s_xml->new_trigger_num++] = trigger; } else if (parentTag == "Delete") { trigger_3s_xml->delete_triggers[trigger_3s_xml->delete_trigger_num++] = trigger; } else if (parentTag == "Modify") { trigger_3s_xml->modify_triggers[trigger_3s_xml->modify_trigger_num++] = trigger; } // 调试用 print_trigger_3s_xml(*trigger_3s_xml); } //实时触发文件处理 int load_3s_data_from_xml(trigger_3s_xml_t* trigger_3s_xml, const std::string& xml_fn) { if (!trigger_3s_xml) return 1; tinyxml2::XMLDocument doc; tinyxml2::XMLError result = doc.LoadFile(xml_fn.c_str()); if (result == tinyxml2::XML_ERROR_FILE_NOT_FOUND) { return 1; } else if (result != tinyxml2::XML_SUCCESS) { return 1; } tinyxml2::XMLElement* root = doc.RootElement(); if (!root) return 1; for (tinyxml2::XMLElement* groupElem = root->FirstChildElement(); groupElem != nullptr; groupElem = groupElem->NextSiblingElement()) { std::string strTag = groupElem->Name(); if (strTag == "Work" || strTag == "New" || strTag == "Delete" || strTag == "Modify") { for (tinyxml2::XMLElement* triggerElem = groupElem->FirstChildElement("Trigger"); triggerElem != nullptr; triggerElem = triggerElem->NextSiblingElement("Trigger")) { parse_3s_trigger(trigger_3s_xml, strTag, triggerElem); } } } return 0; } //将内容写入协议 void process_3s_config(trigger_3s_xml_t *trigger_3s_xml) { //根据协议补充内容 //根据协议补充内容 } //实时触发处理 int parse_3s_xml(trigger_3s_xml_t* trigger_3s_xml) { std::cout << "begin 3s xml..." << std::endl; std::memset(trigger_3s_xml, 0, sizeof(trigger_3s_xml_t)); std::string THREE_SECS_WEBSERVICE_DIR = FRONT_PATH + "/etc/trigger3s/"; //实时数据读取目录 std::string BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN = THREE_SECS_WEBSERVICE_DIR + "bak_3s_trig_command.txt"; //实时触发文件备份文件名 std::string the_webservice_xml_fn = get_3s_trig_fn(); // 获取目录下最新 XML 文件名 if (the_webservice_xml_fn.length() > 4) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待 0.1 秒 std::string full_path = std::string(THREE_SECS_WEBSERVICE_DIR) + the_webservice_xml_fn; if (load_3s_data_from_xml(trigger_3s_xml, full_path) != 0) { std::cerr << "Failed to load 3s data from XML: " << full_path << std::endl; return 1; } std::remove(BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN.c_str()); // 删除旧备份 if (std::rename(full_path.c_str(), BAK_WEBSERVICE_3S_TRIG_COMMAND_XML_FN.c_str()) != 0) { std::perror("Rename failed"); } std::cout << "/etc/trigger3s/*.xml success..." << std::endl; DIY_WARNLOG("process", "【WARN】前置读取实时数据触发文件成功,即将注册实时数据报告"); return 0; } std::cout << "3s xml fail..." << std::endl; return 1; } void check_3s_config() { double now; static double last_check_3s_config_time = 0.0; // 初始化时间 trigger_3s_xml_t trigger_3s_xml; // 3s触发文件 if (!three_secs_enabled) // 只有cfg_3s_data进程才会开启 return; now = sGetMsTime(); // 当前时间 if (std::fabs(now - last_check_3s_config_time) < 3 * 1000) // wait 3secs return; // 当前进程任务执行时查看当前时间和上次执行时间间隔,小于3秒不执行,大于等于3秒往下执行 last_check_3s_config_time = now; // 记录本次运行时间 while (0 == parse_3s_xml(&trigger_3s_xml)) { // 处理3秒文件,一次处理一个 process_3s_config(&trigger_3s_xml); // 根据文件处理数据 } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件部分 //将文件内容读取到结构中 void parse_recall(recall_xml_t* recall_xml, const std::string& parentTag, const std::map& attributes, const std::string& id) { recall_t recall; std::memset(&recall, 0, sizeof(recall_t)); // 设置监测点 ID recall.line_id = id; // 解析时间字符串 auto parse_time = [](const std::string& time_str) -> std::time_t { std::tm tm = {}; std::istringstream ss(time_str); ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); if (ss.fail()) return 0; return std::mktime(&tm); }; recall.start_time = parse_time(attributes.at("StartTime")); recall.end_time = parse_time(attributes.at("EndTime")); recall.need_steady = std::stoi(attributes.at("STEADY")); recall.need_voltage = std::stoi(attributes.at("VOLTAGE")); std::cout << parentTag << " -> " << recall.line_id << " " << recall.need_steady << " " << recall.need_voltage << " " << recall.start_time << " " << recall.end_time << std::endl; if (parentTag == "Work" && recall_xml->work_recall_num < MAX_RECALL_NUM) { recall_xml->work_recalls[recall_xml->work_recall_num++] = recall; } else if (parentTag == "New" && recall_xml->new_recall_num < MAX_RECALL_NUM) { recall_xml->new_recalls[recall_xml->new_recall_num++] = recall; } } //读取补招文件 int parse_recall_xml(recall_xml_t* recall_xml, const std::string& id) { std::string cfg_dir = FRONT_PATH + "/etc/recall"; std::string pattern = subdir + "_" + std::to_string(g_front_seg_index) + "_" + id + "_*_Recall.xml"; DIR* dir = opendir(cfg_dir.c_str()); if (!dir) { DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法解析补招文件,补招文件路径FRONT_PATH + /etc/recall/不存在", g_front_seg_index); return false; } struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string filename = entry->d_name; if (fnmatch(pattern.c_str(), filename.c_str(), 0) != 0) continue; std::string filepath = cfg_dir + "/" + filename; tinyxml2::XMLDocument doc; if (doc.LoadFile(filepath.c_str()) != tinyxml2::XML_SUCCESS) { DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法解析补招文件%s,补招内容无效", g_front_seg_index, filepath.c_str()); continue; } tinyxml2::XMLElement* root = doc.RootElement(); if (!root) continue; for (tinyxml2::XMLElement* elem = root->FirstChildElement(); elem != nullptr; elem = elem->NextSiblingElement()) { std::string tag = elem->Name(); if (tag == "Work" || tag == "New") { for (tinyxml2::XMLElement* recallElem = elem->FirstChildElement("Recall"); recallElem != nullptr; recallElem = recallElem->NextSiblingElement("Recall")) { std::map attrs; const char* start = recallElem->Attribute("StartTime"); const char* end = recallElem->Attribute("EndTime"); const char* steady = recallElem->Attribute("STEADY"); const char* voltage = recallElem->Attribute("VOLTAGE"); if (start && end && steady && voltage) { attrs["StartTime"] = start; attrs["EndTime"] = end; attrs["STEADY"] = steady; attrs["VOLTAGE"] = voltage; parse_recall(recall_xml, tag, attrs, id); } } } } } closedir(dir); return 0; } //将读取到的补招文件写入到监测点的运行结构中,后续根据实际补充 void process_recall_config(recall_xml_t* recall_xml) { } //根据监测点id来获取补招数据,补招时调用这个 void Check_Recall_Config(const std::string& id) { /*if (g_node_id == HIS_DATA_BASE_NODE_ID || g_node_id == NEW_HIS_DATA_BASE_NODE_ID || g_node_id == RECALL_HIS_DATA_BASE_NODE_ID || g_node_id == RECALL_ALL_DATA_BASE_NODE_ID) {*/ recall_xml_t recall_xml; std::memset(&recall_xml, 0, sizeof(recall_xml_t)); // 解析补招文件 parse_recall_xml(&recall_xml, id); // 将补招数据赋值到全局变量 process_recall_config(&recall_xml); //} } //补招成功后删除补招文件,补招后调用这个 int delete_recall_xml(const std::string& id) { std::string cfg_dir = FRONT_PATH + "/etc/recall"; std::string pattern = std::string(subdir) + "_" + std::to_string(g_front_seg_index) + "_" + id + "_*_Recall.xml"; DIR* dir = opendir(cfg_dir.c_str()); if (!dir) { std::cerr << "Folder does not exist or cannot be opened: " << cfg_dir << std::endl; return false; } struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string filename = entry->d_name; if (fnmatch(pattern.c_str(), filename.c_str(), 0) == 0) { std::string fullpath = cfg_dir + "/" + filename; if (remove(fullpath.c_str()) == 0) { std::cout << "Deleted: " << fullpath << std::endl; } else { std::cerr << "Failed to delete: " << fullpath << std::endl; } } } closedir(dir); return 0; } //删除过期的xmllnk:多个进程并发删除导致的失败不会影响进程 void DeletcRecallXml() { std::string cfg_dir = FRONT_PATH + "/etc/recall"; std::string pattern = std::string(subdir) + "_*_Recall.xml"; DIR* dir = opendir(cfg_dir.c_str()); if (!dir) { std::cerr << "folder does not exist!" << std::endl; DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 删除旧的补招文件失败,补招文件路径FRONT_PATH + /etc/recall/不存在", g_front_seg_index); return; } // 获取当前时间,计算 2 天前的时间戳 std::time_t now = std::time(nullptr); std::time_t cutoff = now - 2 * 24 * 60 * 60; // 两天前 struct dirent* entry; while ((entry = readdir(dir)) != NULL) { std::string filename = entry->d_name; if (fnmatch(pattern.c_str(), filename.c_str(), 0) == 0) { std::string fullpath = cfg_dir + "/" + filename; struct stat file_stat; if (stat(fullpath.c_str(), &file_stat) == 0) { if (file_stat.st_mtime < cutoff) { if (remove(fullpath.c_str()) == 0) { DIY_INFOLOG("process", "【NORMAL】前置的%d号进程 删除超过两天的补招文件", g_front_seg_index); } else { std::cerr << "Failed to remove file: " << fullpath << std::endl; } } } } } closedir(dir); } //根据补招列表创建补招文件 void CreateRecallXml() { std::time_t now = std::time(nullptr); std::tm* tm_now = std::localtime(&now); char timestamp[32] = {0}; std::strftime(timestamp, sizeof(timestamp), "%Y%m%d%H%M%S", tm_now); g_StatisticLackList_list_mutex.lock(); if (!g_StatisticLackList.empty()) { DIY_INFOLOG("process", "【NORMAL】前置的%d号进程 开始写入补招文件", g_front_seg_index); std::map> id_map; for (const auto& jr : g_StatisticLackList) { id_map[jr.MonitorID].push_back(jr); } for (const auto& pair : id_map) { const std::string& monitor_id = pair.first; const std::list& recalls = pair.second; std::ostringstream path; path << FRONT_PATH << "/etc/recall/" << subdir << "_" << g_front_seg_index << "_" << monitor_id << "_" << timestamp << "_Recall.xml"; tinyxml2::XMLDocument doc; tinyxml2::XMLDeclaration* decl = doc.NewDeclaration(); doc.InsertFirstChild(decl); tinyxml2::XMLElement* root = doc.NewElement("RecallList"); doc.InsertEndChild(root); // 空 Work 段 tinyxml2::XMLElement* work = doc.NewElement("Work"); root->InsertEndChild(work); // New 段 tinyxml2::XMLElement* new_elem = doc.NewElement("New"); for (const auto& jr : recalls) { tinyxml2::XMLElement* recall = doc.NewElement("Recall"); recall->SetAttribute("MonitorID", jr.MonitorID.c_str()); recall->SetAttribute("StartTime", jr.StartTime.c_str()); recall->SetAttribute("EndTime", jr.EndTime.c_str()); recall->SetAttribute("STEADY", jr.STEADY.c_str()); recall->SetAttribute("VOLTAGE", jr.VOLTAGE.c_str()); new_elem->InsertEndChild(recall); } root->InsertEndChild(new_elem); tinyxml2::XMLError save_result = doc.SaveFile(path.str().c_str()); if (save_result != tinyxml2::XML_SUCCESS) { DIY_ERRORLOG("process", "【ERROR】前置的%d号进程 无法将补招文件写入路径: %s", g_front_seg_index, path.str().c_str()); continue; } } } g_StatisticLackList.clear(); g_StatisticLackList_list_mutex.unlock(); } //生成待补招xml文件 void create_recall_xml() { //if (g_node_id == HIS_DATA_BASE_NODE_ID || g_node_id == NEW_HIS_DATA_BASE_NODE_ID || g_node_id == RECALL_HIS_DATA_BASE_NODE_ID || (g_node_id == RECALL_ALL_DATA_BASE_NODE_ID)) { DeletcRecallXml(); CreateRecallXml(); //} } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////补招部分 // 工具函数:将时间字符串转为 time_t(秒级) // ▲新增:从 monitorId 提取结尾数字(不含前导非数字部分),失败返回空串 static std::string get_monitor_digits_from_terminal_list(const std::string& dev_id, const std::string& monitor_id) { std::lock_guard lk(ledgermtx); // 找终端 const terminal_dev* dev = NULL; for (std::vector::const_iterator it = terminal_devlist.begin(); it != terminal_devlist.end(); ++it) { if (it->terminal_id == dev_id) { dev = &(*it); break; } } if (!dev) { std::cout << "[digits] dev not found: " << dev_id << std::endl; return std::string(); } // 找监测点 for (std::vector::const_iterator itLm = dev->line.begin(); itLm != dev->line.end(); ++itLm) { if (!itLm->monitor_id.empty() && itLm->monitor_id == monitor_id) { // 常见就是 logical_device_seq,比如 "1"、"02" 等 std::string seq = itLm->logical_device_seq; // 可选:去掉前导 0(与您生成“数字_时间.xxx”的命名规则保持一致) // 若不需要去零,注释以下 5 行即可。 size_t p = 0; while (p < seq.size() && seq[p] == '0') ++p; seq = (p >= seq.size()) ? "0" : seq.substr(p); return seq; } } std::cout << "[digits] monitor not found in dev: mon=" << monitor_id << " dev=" << dev_id << std::endl; return std::string(); } // ▲新增:把 "YYYY-MM-DD HH:MM:SS[.ffffff]" -> "YYYYMMDD_HHMMSS_mmm" static std::string compact_ts_for_filename(const std::string& ts) { // 允许输入 "2025-09-09 07:46:57.246000" // 输出 "20250909_074657_246" int year, mon, day, hour, min, sec, ms = 0; char dotpart[16] = {0}; if (sscanf(ts.c_str(), "%d-%d-%d %d:%d:%d.%15s", &year, &mon, &day, &hour, &min, &sec, dotpart) >= 6) { // 提取前三位毫秒 if (dotpart[0]) { std::string frac(dotpart); while (frac.size() < 3) frac.push_back('0'); // 不足3补0 ms = std::atoi(frac.substr(0, 3).c_str()); } char buf[32]; /*snprintf(buf, sizeof(buf), "%04d%02d%02d_%02d%02d%02d_%03d", year, mon, day, hour, min, sec, ms);*/ snprintf(buf, sizeof(buf), "%04d%02d%02d_%02d%02d%02d", year, mon, day, hour, min, sec); return std::string(buf); } return ""; } // ▲新增:按“数字_时间后缀.后缀”拼直下文件名(返回{*.cfg, *.dat}两种) static std::vector build_direct_filenames(const std::string& monitorDigits, const std::string& ts_compact) { std::vector out; if (monitorDigits.empty() || ts_compact.empty()) return out; out.push_back(monitorDigits + "_" + ts_compact); return out; } static long long parse_time_to_epoch(const std::string& time_str) { std::tm tm = {}; std::istringstream ss(time_str); ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); if (ss.fail()) { return 0; } return static_cast(std::mktime(&tm)); } // 数据完整性补招判断(划分为1小时) void Get_Recall_Time_Char(const std::string& start_time_str, const std::string& end_time_str, std::vector& recallinfo_list_hour) { long long starttime = parse_time_to_epoch(start_time_str); long long endtime = parse_time_to_epoch(end_time_str); if (starttime == 0 || endtime == 0 || starttime >= endtime) { return; } // 初始区间加入 RecallInfo initial; initial.starttime = starttime; initial.endtime = endtime; std::vector recallinfo_list; recallinfo_list.push_back(initial); const long long max_interval = 3600; // 1小时 for (const auto& interval : recallinfo_list) { long long duration = interval.endtime - interval.starttime; for (long long j = 0; j <= duration; j += max_interval) { RecallInfo info; info.starttime = interval.starttime + j; if (j + max_interval > duration) { info.endtime = interval.endtime; } else { info.endtime = interval.starttime + j + max_interval - 1; } recallinfo_list_hour.push_back(info); } } } //mq调用将补招信息写入补招列表 int recall_json_handle_from_mq(const std::string& body) { try { // ====== 解析外层 JSON ====== nlohmann::json root; try { root = nlohmann::json::parse(body); } catch (const std::exception& e) { std::cerr << "Error parsing JSON: " << e.what() << std::endl; // ★与原逻辑等价:无法解析,不再进入 recall_json_handle DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,消息的json结构不正确", g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str()); return 10004; } // 提取 "messageBody"(字符串) if (!root.contains("messageBody") || !root["messageBody"].is_string()) { std::cerr << "'messageBody' is missing or is not a string" << std::endl; DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,没有messageBody字段", g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str()); return 10004; } std::string messageBodyStr = root["messageBody"].get(); if (messageBodyStr.empty()) { std::cerr << "'messageBody' is empty" << std::endl; DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,messageBody为空", g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str()); return 10004; } // 解析 messageBody 内层 JSON nlohmann::json mb; try { mb = nlohmann::json::parse(messageBodyStr); } catch (const std::exception& e) { std::cerr << "Failed to parse 'messageBody' JSON: " << e.what() << std::endl; DIY_ERRORLOG("process","【ERROR】前置的%d号进程处理topic:%s_%s的补招触发消息失败,messageBody的json结构不正确", g_front_seg_index, G_MQCONSUMER_TOPIC_RC.c_str(), FRONT_INST.c_str()); return 10004; } if (mb.is_array()) { // ====== 新格式(数组):支持 dataType=0/1 的区间补招 & dataType=2 的直下文件 ====== for (const auto& rec : mb) { if (!rec.is_object()) continue; // 必要字段 std::string guid = rec.value("guid", ""); std::string terminalId = rec.value("terminalId", ""); if (terminalId.empty()) continue; // ▲dataType:string "0/1/2" 或 number 0/1/2,先判断 contains int dt = -1; if (rec.contains("dataType")) { if (rec["dataType"].is_number_integer()) { dt = rec["dataType"].get(); } else if (rec["dataType"].is_string()) { std::string s = rec["dataType"].get(); if (s == "0") dt = 0; else if (s == "1") dt = 1; else if (s == "2") dt = 2; } } if (dt == -1) { std::cout << "[recall] skip: invalid dataType, guid=" << guid << "\n"; continue; } // === 统一收集监测点:支持 monitorIdList 或 monitorId === std::vector monitors; if (rec.contains("monitorIdList") && rec["monitorIdList"].is_array()) { for (const auto& m : rec["monitorIdList"]) { if (m.is_string()) monitors.push_back(m.get()); } } if (monitors.empty() && rec.contains("monitorId")) { if (rec["monitorId"].is_array()) { for (const auto& m : rec["monitorId"]) { if (m.is_string()) monitors.push_back(m.get()); } } else if (rec["monitorId"].is_string()) { monitors.push_back(rec["monitorId"].get()); } } if (monitors.empty()) { std::cout << "[recall] skip: monitors empty (no monitorIdList/monitorId), guid=" << guid << "\n"; continue; } // ▲沿用:校验终端归属 + 在线性 { std::lock_guard lock(ledgermtx); const terminal_dev* targetDev = NULL; for (std::vector::const_iterator it = terminal_devlist.begin(); it != terminal_devlist.end(); ++it) { if (it->terminal_id == terminalId) { targetDev = &(*it); break; } } if (!targetDev) { std::cout << "terminalId未匹配当前进程内装置: " << terminalId << std::endl; continue; } if (ClientManager::instance().get_dev_status(targetDev->terminal_id) != 1) { std::cout << "terminalId对应装置不在线: " << targetDev->terminal_id << std::endl; std::string msg = std::string("装置:") + targetDev->terminal_name + " 不在线,无法补招"; send_reply_to_kafka_recall(guid, dt, static_cast(ResponseCode::NOT_FOUND), msg, targetDev->terminal_id, "", "", ""); continue; } } if (dt == 2) { //一个测点一个guid对应多个文件 // ▲直下文件:timeList -> fun1/fun2 -> enqueue_direct_download if (!rec.contains("timeList") || !rec["timeList"].is_array()) continue; for (const auto& monId : monitors) { // fun1:提取 monitor 数字 std::string digits = get_monitor_digits_from_terminal_list(terminalId, monId);//有锁 if (digits.empty()) { std::cout << "monitorId数字提取失败: " << monId << std::endl; continue; } for (const auto& t : rec["timeList"]) { if (!t.is_string()) continue; std::string ts_compact = compact_ts_for_filename(t.get()); if (ts_compact.empty()) { std::cout << "时间解析失败: " << t << std::endl; continue; } // fun2:生成 *.cfg/*.dat 两个文件名 std::vector fns = build_direct_filenames(digits, ts_compact); for (const auto& fn : fns) { bool ok = enqueue_direct_download(terminalId, monId, fn, guid);//有锁 std::cout << "[direct] enqueue " << (ok ? "ok " : "fail ") << "dev=" << terminalId << " mon=" << monId << " file=" << fn << std::endl; } } init_recall_record_file(guid, terminalId, monId, "", ""); } } else if (dt == 0 || dt == 1) { //一个装置对应一个guid对应多个监测点的多个时间段 // ▲保持老逻辑(与“对象+data”一致):timeInterval 数组 if (!rec.contains("timeInterval") || !rec["timeInterval"].is_array()) continue; // 解析 dataType-> stat/voltage int stat = (dt == 0) ? 1 : 0; int voltage = (dt == 1) ? 1 : 0; // 把每个 monitor 的区间写入 recall_list / recall_list_static std::lock_guard lock(ledgermtx); // 找终端 terminal_dev* dev_nc = NULL; for (auto& d : terminal_devlist) if (d.terminal_id == terminalId) { dev_nc = &d; break; } if (!dev_nc) continue; for (const auto& monId : monitors) { // 找监测点 ledger_monitor* lm = NULL; for (auto itLm = dev_nc->line.begin(); itLm != dev_nc->line.end(); ++itLm) { if (!itLm->monitor_id.empty() && itLm->monitor_id == monId) { lm = &(*itLm); break; } } if (!lm) { std::cout << "monitorId未在terminal内找到: " << monId << " @ " << terminalId << std::endl; continue; } for (const auto& ti : rec["timeInterval"]) { if (!ti.is_string()) continue; std::string s = ti.get(); std::string::size_type pos = s.find('~'); if (pos == std::string::npos) { std::cout << "timeInterval格式错误: " << s << std::endl; continue; } std::string start = s.substr(0, pos); std::string end = s.substr(pos + 1); RecallFile rm_all; rm_all.recall_guid = guid; rm_all.recall_status = 0; rm_all.StartTime = start; rm_all.EndTime = end; rm_all.STEADY = std::to_string(stat); rm_all.VOLTAGE = std::to_string(voltage); //lnk 20251027xuyang request:生成文件记录单个测点单个时间段的补招记录文件,补招结束后使用这个文件信息来响应 init_recall_record_file(guid, terminalId, monId, start, end); if (voltage == 1) { std::vector recallinfo_list_hour; Get_Recall_Time_Char(start, end, recallinfo_list_hour); for (size_t i = 0; i < recallinfo_list_hour.size(); ++i) { const RecallInfo& info = recallinfo_list_hour[i]; RecallMonitor rm; rm.recall_guid = guid; rm.recall_status = 0; rm.StartTime = epoch_to_datetime_str(info.starttime); rm.EndTime = epoch_to_datetime_str(info.endtime); rm.STEADY = std::to_string(stat); rm.VOLTAGE = std::to_string(voltage); lm->recall_list.push_back(rm); } } if (stat == 1) { lm->recall_list_static.push_back(rm_all); } if (stat == 0 && voltage == 0) return 10003; } } } else { // 未知 dataType,忽略 std::cout << "unknown dataType: " << dt << std::endl; continue; } } } else { // 不支持的 messageBody 形态 std::cout << "unknown messageBody form" << std::endl; return 10004; } } catch (const std::exception& e) { std::cout << "处理客户端发送的消息错误,原因:" << e.what() << std::endl; return 10004; } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////台账接口打印 // 打印 terminal_dev_map 中所有内容的函数 void printTerminalDevMap(const std::map& terminal_dev_map) { for (const auto& kv : terminal_dev_map) { const std::string& key = kv.first; const terminal_dev& dev = kv.second; // 引用对象 std::cout << "Key: " << key << ", Terminal ID: " << dev.terminal_id << ", Terminal Code: " << dev.terminal_name << ", Organization Name: "<< dev.org_name << ", Maintenance Name: " << dev.maint_name << ", Station Name: " << dev.station_name << ", Factory: " << dev.tmnl_factory << ", Status: " << dev.tmnl_status << ", Device Type: " << dev.dev_type << ", Device Key: " << dev.dev_key << ", Device Series: " << dev.dev_series << ", ProcessNo: " << dev.processNo << ", MaxProcessNum: " << dev.maxProcessNum << ", Address: " << dev.addr_str << ", Port: " << dev.port << ", Timestamp: " << dev.timestamp << ", mac: " << dev.mac << std::endl; // 打印监测点信息 for (size_t i = 0; i < dev.line.size(); ++i) { const auto& m = dev.line[i]; std::cout << " Monitor [" << i << "] " << "ID: " << m.monitor_id << ", Code: " << m.terminal_id << ", Name: " << m.monitor_name << ", Seq: " << m.logical_device_seq << ", Voltage: "<< m.voltage_level << ", Connect: "<< m.terminal_connect << ", Timestamp:"<< m.timestamp << ", Status: " << m.status << ", CT1: " << m.CT1 << ", CT2: " << m.CT2 << ", PT1: " << m.PT1 << ", PT2: " << m.PT2 << std::endl; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////更新台账 //在台账更新目录查找自己进程要处理的文件 std::list find_xml_belong_to_this_process() { char prefix[20]; // 假设最多需要20个字符(根据实际需要调整) sprintf(prefix, "%d_%d", g_node_id, g_front_seg_index); // 将g_node_id和g_front_seg_index格式化为字符串 std::string LEDGER_UPDATE_DIR = FRONT_PATH + "/etc/ledgerupdate/"; //台账更新读取目录 DIR *dir = opendir(LEDGER_UPDATE_DIR.c_str()); // 打开目录 struct dirent *entry; std::list found_files; // 用于存储找到的所有匹配文件名 if (dir == NULL) { std::cout << "Failed to open directory: " << LEDGER_UPDATE_DIR << std::endl; return found_files; // 返回空的list } // 遍历目录中的所有文件 while ((entry = readdir(dir)) != NULL) { std::string filename = entry->d_name; // 排除 "." 和 ".." 目录 if (filename == "." || filename == "..") { continue; } std::cout << "find" << filename << "in" << LEDGER_UPDATE_DIR << std::endl; // 判断文件名是否以 prefix 开头且扩展名是 .xml if (filename.find(prefix) == 0 && filename.substr(filename.find_last_of('.') + 1) == "xml") { std::string full_path = LEDGER_UPDATE_DIR + filename; found_files.push_back(full_path); // 将完整路径加入容器 } } closedir(dir); // 关闭目录 return found_files; // 返回所有找到的文件名 } // 根据 str_tag 将 terminal 添加到对应的数组 void add_terminal_to_trigger_update(trigger_update_xml_t& trigger_update_xml, const std::string& str_tag, const update_dev& work_terminal) { if (str_tag == "add") { std::cout << "new ledger!!!!" << std::endl; trigger_update_xml.new_updates.push_back(work_terminal); } else if (str_tag == "modify") { std::cout << "modify ledger!!!" << std::endl; trigger_update_xml.modify_updates.push_back(work_terminal); } else { std::cerr << "Unknown tag: " << str_tag << std::endl; } } // 将添加和修改的文件内容写入结构 void parse_terminal_from_data(trigger_update_xml_t& trigger_update_xml, const std::string& str_tag, const std::string& data, const std::string& guid_value) { update_dev work_terminal; work_terminal.guid = guid_value; tinyxml2::XMLDocument doc; if (doc.Parse(data.c_str()) != tinyxml2::XML_SUCCESS) { return; } auto root = doc.FirstChildElement("terminal"); if (!root) return; auto get_value = [&](const char* tag) -> std::string { auto elem = root->FirstChildElement(tag); return elem && elem->GetText() ? elem->GetText() : ""; }; work_terminal.terminal_id = get_value("id"); work_terminal.terminal_name = get_value("terminalCode"); //work_terminal.org_name = get_value("orgName"); //work_terminal.maint_name = get_value("maintName"); //work_terminal.station_name = get_value("stationName"); //work_terminal.tmnl_factory = get_value("manufacturer"); work_terminal.tmnl_status = get_value("status"); work_terminal.dev_type = get_value("devType"); //work_terminal.dev_key = get_value("devKey"); //work_terminal.dev_series = get_value("series"); work_terminal.processNo = get_value("processNo"); //work_terminal.addr_str = get_value("ip"); //work_terminal.port = get_value("port"); //work_terminal.timestamp = get_value("updateTime"); work_terminal.Righttime = get_value("Righttime"); work_terminal.mac = get_value("mac"); for (tinyxml2::XMLElement* monitor = root->FirstChildElement("monitorData"); monitor; monitor = monitor->NextSiblingElement("monitorData")) { update_monitor mon; mon.monitor_id = monitor->FirstChildElement("id") ? monitor->FirstChildElement("id")->GetText() : "N/A"; mon.monitor_name = monitor->FirstChildElement("name") ? monitor->FirstChildElement("name")->GetText() : "N/A"; mon.voltage_level = monitor->FirstChildElement("voltageLevel") ? monitor->FirstChildElement("voltageLevel")->GetText() : "N/A"; mon.terminal_connect = monitor->FirstChildElement("ptType") ? monitor->FirstChildElement("ptType")->GetText() : "N/A"; mon.logical_device_seq = monitor->FirstChildElement("lineNo") ? monitor->FirstChildElement("lineNo")->GetText() : "N/A"; //mon.timestamp = monitor->FirstChildElement("timestamp") ? monitor->FirstChildElement("timestamp")->GetText() : "N/A"; mon.terminal_id = monitor->FirstChildElement("terminal_id") ? monitor->FirstChildElement("terminal_name")->GetText() : "N/A"; mon.status = monitor->FirstChildElement("status") ? monitor->FirstChildElement("status")->GetText() : "N/A"; mon.CT1 = monitor->FirstChildElement("CT1") && monitor->FirstChildElement("CT1")->GetText() ? atof(monitor->FirstChildElement("CT1")->GetText()) : 0.0; mon.CT2 = monitor->FirstChildElement("CT2") && monitor->FirstChildElement("CT2")->GetText() ? atof(monitor->FirstChildElement("CT2")->GetText()) : 0.0; mon.PT1 = monitor->FirstChildElement("PT1") && monitor->FirstChildElement("PT1")->GetText() ? atof(monitor->FirstChildElement("PT1")->GetText()) : 0.0; mon.PT2 = monitor->FirstChildElement("PT2") && monitor->FirstChildElement("PT2")->GetText() ? atof(monitor->FirstChildElement("PT2")->GetText()) : 0.0; work_terminal.line.push_back(mon); } add_terminal_to_trigger_update(trigger_update_xml, str_tag, work_terminal); } // 统一处理文件内容和结构 void parse_ledger_update(trigger_update_xml_t& trigger_update_xml, const std::string& strTag, const std::string& data, const std::string& guid_value) { std::cout << "record one xml.." << std::endl; if (strTag == "add" || strTag == "modify") { parse_terminal_from_data(trigger_update_xml, strTag, data, guid_value); } else if (strTag == "delete") { update_dev delete_terminal; tinyxml2::XMLDocument doc; if (doc.Parse(data.c_str()) != tinyxml2::XML_SUCCESS) { std::cerr << "Failed to parse XML for delete tag." << std::endl; return; } tinyxml2::XMLElement* root = doc.FirstChildElement("terminalData"); if (!root) { std::cerr << "Missing terminalData element in delete tag." << std::endl; return; } tinyxml2::XMLElement* idElem = root->FirstChildElement("id"); if (idElem && idElem->GetText()) { delete_terminal.terminal_id = idElem->GetText(); } else { std::cerr << "Missing id element in delete tag." << std::endl; return; } delete_terminal.guid = guid_value; trigger_update_xml.delete_updates.push_back(delete_terminal); } else { std::cerr << "Unsupported strTag: " << strTag << std::endl; } } //读取台账更新文件 int load_ledger_update_from_xml(trigger_update_xml_t& trigger_update_xml, const std::string& xml_fn) { std::cout << "start to load one xml.." << std::endl; std::ifstream file(xml_fn); if (!file.is_open()) { std::cerr << "Failed to open file: " << xml_fn << std::endl; return -1; } std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); file.close(); tinyxml2::XMLDocument doc; if (doc.Parse(content.c_str()) != tinyxml2::XML_SUCCESS) { std::cerr << "Failed to parse XML content." << std::endl; return -1; } auto* root = doc.FirstChildElement("ledger_update"); if (!root) { std::cerr << "Missing root tag." << std::endl; return -1; } std::string guid_value; auto* guidElem = root->FirstChildElement("guid"); if (guidElem && guidElem->GetText()) { guid_value = guidElem->GetText(); std::cout << "Found guid: " << guid_value << std::endl; } const char* tag_names[] = {"add", "modify", "delete"}; tinyxml2::XMLElement* tagElem = nullptr; std::string target_tag; for (const auto& tag : tag_names) { tagElem = root->FirstChildElement(tag); if (tagElem) { target_tag = tag; break; } } if (!tagElem) { std::cerr << "No , , or tag found!" << std::endl; return -1; } for (auto* termElem = tagElem->FirstChildElement("terminalData"); termElem; termElem = termElem->NextSiblingElement("terminalData")) { tinyxml2::XMLPrinter printer; termElem->Accept(&printer); std::string data_content = printer.CStr(); std::cout << "ledger data_content is " << data_content << std::endl; parse_ledger_update(trigger_update_xml, target_tag, data_content, guid_value); } std::cout << "load one xml finish" << std::endl; return 0; } //台账更新处理文件 int parse_ledger_update_xml(trigger_update_xml_t& trigger_update_xml) { std::list result = find_xml_belong_to_this_process(); if (result.empty()) return 1; for (const auto& filename : result) { std::cout << "Found XML: " << filename << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (!load_ledger_update_from_xml(trigger_update_xml, filename)) { DIY_WARNLOG("process", "【WARN】成功读取台账更新文件: %s", filename.c_str()); } if (std::remove(filename.c_str()) != 0) { DIY_ERRORLOG("process", "【ERROR】删除台账更新文件失败: %s", filename.c_str()); return 1; } else { DIY_INFOLOG("process", "【NORMAL】成功删除台账更新文件: %s", filename.c_str()); } } if (!trigger_update_xml.new_updates.empty() || !trigger_update_xml.modify_updates.empty() || !trigger_update_xml.delete_updates.empty()) { std::cout << "ledger update xml have data..." << std::endl; return 0; } std::cout << "ledger update xml no data..." << std::endl; return 1; } //更新单个台账 int update_one_terminal_ledger(const update_dev& update,terminal_dev& target_dev) { // 更新基本信息 if (!update.terminal_id.empty()) { target_dev.terminal_id = update.terminal_id; std::cout << "terminal_id: " << target_dev.terminal_id << std::endl; } if (!update.terminal_name.empty()) { target_dev.terminal_name = update.terminal_name; std::cout << "terminal_name: " << target_dev.terminal_name << std::endl; } /*if (!update.tmnl_factory.empty()) { target_dev.tmnl_factory = update.tmnl_factory; std::cout << "tmnl_factory: " << target_dev.tmnl_factory << std::endl; } if (!update.tmnl_status.empty()) { target_dev.tmnl_status = update.tmnl_status; std::cout << "tmnl_status: " << target_dev.tmnl_status << std::endl; }*/ if (!update.dev_type.empty()) { target_dev.dev_type = update.dev_type; std::cout << "dev_type: " << target_dev.dev_type << std::endl; } if (!update.processNo.empty()) { target_dev.processNo = update.processNo; std::cout << "processNo: " << target_dev.processNo << std::endl; } /*if (!update.dev_series.empty()) { target_dev.dev_series = update.dev_series; std::cout << "dev_series: " << target_dev.dev_series << std::endl; } if (!update.dev_key.empty()) { target_dev.dev_key = update.dev_key; std::cout << "dev_key: " << target_dev.dev_key << std::endl; }*/ if (!update.addr_str.empty()) { target_dev.addr_str = update.addr_str; std::cout << "addr_str: " << target_dev.addr_str << std::endl; } /*if (!update.port.empty()) { target_dev.port = update.port; std::cout << "port: " << target_dev.port << std::endl; } if (!update.mac.empty()) { target_dev.mac = update.mac; std::cout << "mac: " << target_dev.mac << std::endl; } if (!update.timestamp.empty()) { struct tm timeinfo = {}; if (sscanf(update.timestamp.c_str(), "%4d-%2d-%2d %2d:%2d:%2d", &timeinfo.tm_year, &timeinfo.tm_mon, &timeinfo.tm_mday, &timeinfo.tm_hour, &timeinfo.tm_min, &timeinfo.tm_sec) == 6) { timeinfo.tm_year -= 1900; timeinfo.tm_mon -= 1; timeinfo.tm_isdst = -1; time_t raw_time = mktime(&timeinfo); if (raw_time != -1) { target_dev.timestamp = static_cast(raw_time); std::cout << "timestamp (unix): " << target_dev.timestamp << std::endl; } else { std::cerr << "Error: mktime failed." << std::endl; return -1; } } else { std::cerr << "Error: invalid timestamp format." << std::endl; return -1; } }*/ // 清空旧监测点并重新填充 target_dev.line.clear(); for (const auto& mon : update.line) { if (mon.monitor_id.empty()) break; ledger_monitor m; m.monitor_id = mon.monitor_id; m.monitor_name = mon.monitor_name; m.logical_device_seq = mon.logical_device_seq; m.voltage_level = mon.voltage_level; m.terminal_connect = mon.terminal_connect; m.status = mon.status; m.terminal_id = mon.terminal_id; //m.timestamp = mon.timestamp; m.CT1 = mon.CT1; m.CT2 = mon.CT2; m.PT1 = mon.PT1; m.PT2 = mon.PT2; if (m.terminal_connect != "0") { isdelta_flag = 1; std::cout << "monitor_id " << m.monitor_id << " uses delta wiring." << std::endl; } /*if (!m.timestamp.empty()) { struct tm timeinfo = {}; if (sscanf(m.timestamp.c_str(), "%4d-%2d-%2d %2d:%2d:%2d", &timeinfo.tm_year, &timeinfo.tm_mon, &timeinfo.tm_mday, &timeinfo.tm_hour, &timeinfo.tm_min, &timeinfo.tm_sec) == 6) { timeinfo.tm_year -= 1900; timeinfo.tm_mon -= 1; timeinfo.tm_isdst = -1; time_t raw_time = mktime(&timeinfo); if (raw_time != -1) { m.timestamp = static_cast(raw_time); std::cout << "monitor time (unix): " << m.timestamp << std::endl; } } }*/ target_dev.line.push_back(m); } return 0; } //台账更新到台账列表 void process_ledger_update(trigger_update_xml_t& ledger_update_xml) { // --- 1. 新增处理 --- std::cout << "add ledger num: " << ledger_update_xml.new_updates.size() << std::endl; for (auto it = ledger_update_xml.new_updates.begin(); it != ledger_update_xml.new_updates.end(); ) { update_dev& new_dev = *it; auto found = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d) { return d.terminal_id == new_dev.terminal_id; }); if (found != terminal_devlist.end()) { if (ledger_update_xml.modify_updates.size() < MAX_UPDATEA_NUM) { ledger_update_xml.modify_updates.push_back(new_dev); } else { std::cerr << "Exceeded MAX_UPDATEA_NUM limit for modify_updates!" << std::endl; } it = ledger_update_xml.new_updates.erase(it); // 删除已处理项 continue; } if (terminal_devlist.size() >= static_cast(IED_COUNT)) { send_reply_to_queue(new_dev.guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + new_dev.terminal_id + " 台账更新失败,配置台账数量已满"); ++it; continue; } terminal_dev target_dev; if (update_one_terminal_ledger(new_dev, target_dev) != 0) { send_reply_to_queue(new_dev.guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + new_dev.terminal_id + " 台账更新失败,无法写入台账"); ++it; continue; } init_loggers_bydevid(target_dev.terminal_id); terminal_devlist.push_back(target_dev); DeviceInfo device = make_device_from_terminal(target_dev); ClientManager::instance().add_device(device); send_reply_to_queue(new_dev.guid, static_cast(ResponseCode::OK), "终端 id: " + new_dev.terminal_id + " 台账添加成功"); it = ledger_update_xml.new_updates.erase(it); } // --- 2. 修改处理 --- std::cout << "modify ledger num: " << ledger_update_xml.modify_updates.size() << std::endl; for (auto& mod_dev : ledger_update_xml.modify_updates) { auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d) { return d.terminal_id == mod_dev.terminal_id; }); if (it != terminal_devlist.end()) { erase_one_terminals_by_id(mod_dev.terminal_id); if (update_one_terminal_ledger(mod_dev, *it) != 0) { send_reply_to_queue(mod_dev.guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + mod_dev.terminal_id + " 台账更新失败,写入失败"); continue; } init_loggers_bydevid(mod_dev.terminal_id); DeviceInfo device = make_device_from_terminal(*it); ClientManager::instance().add_device(device); send_reply_to_queue(mod_dev.guid, static_cast(ResponseCode::OK), "终端 id: " + mod_dev.terminal_id + " 台账修改成功"); } else { send_reply_to_queue(mod_dev.guid, static_cast(ResponseCode::NOT_FOUND), "终端 id: " + mod_dev.terminal_id + " 台账修改失败,未找到终端"); } } // --- 3. 删除处理 --- std::cout << "delete ledger num: " << ledger_update_xml.delete_updates.size() << std::endl; for (auto& del_dev : ledger_update_xml.delete_updates) { auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d) { return d.terminal_id == del_dev.terminal_id; }); if (it != terminal_devlist.end()) { erase_one_terminals_by_id(del_dev.terminal_id); ClientManager::instance().remove_device(del_dev.terminal_id); send_reply_to_queue(del_dev.guid, static_cast(ResponseCode::OK), "终端 id: " + del_dev.terminal_id + " 台账删除成功"); } else { send_reply_to_queue(del_dev.guid, static_cast(ResponseCode::NOT_FOUND), "终端 id: " + del_dev.terminal_id + " 台账删除失败,未找到终端"); } } // --- 4. 日志记录 --- if (!ledger_update_xml.modify_updates.empty() || !ledger_update_xml.new_updates.empty() || !ledger_update_xml.delete_updates.empty()) { create_ledger_log(&ledger_update_xml); } } //台账更新处理函数 void check_ledger_update() { static double last_check_time = 0.0; double now = sGetMsTime(); if (now - last_check_time < 3000.0) return; last_check_time = now; std::unique_ptr trigger_ledger_update_xml(new trigger_update_xml_t()); if (0 == parse_ledger_update_xml(*trigger_ledger_update_xml)) { print_trigger_update_xml(*trigger_ledger_update_xml); std::lock_guard lock(ledgermtx); process_ledger_update(*trigger_ledger_update_xml); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////台账更新记录日志 // 获取当前时间并格式化为 "YYYY-MM-DD HH:MM:SS" std::string get_current_time() { std::time_t t = std::time(NULL); struct std::tm tm = *std::localtime(&t); char buffer[80]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); return std::string(buffer); } // 写入日志条目 void write_log_entry(std::ofstream &log_file, const std::string &action, const std::string &terminal_id, const std::string ¤t_time) { log_file << terminal_id << "\t" << action << "time:" << current_time << "\n"; } // 创建台账更新日志 void create_ledger_log(trigger_update_xml_t* ledger_update_xml) { std::cout << "create_ledger_log." << std::endl; std::string log_filename = FRONT_PATH + "/etc/" + LEDGER_UPDATE_FN; std::ofstream log_file(log_filename.c_str(), std::ios::app); // 以追加模式打开文件 if (!log_file.is_open()) { std::cerr << "Failed to open log file: " << log_filename << std::endl; return; } std::vector> new_entries; std::vector> modify_entries; std::vector> delete_entries; std::string current_time = get_current_time(); // 获取当前时间 // new_updates for (const auto& dev : ledger_update_xml->new_updates) { new_entries.emplace_back(dev.terminal_id, current_time); } // modify_updates for (const auto& dev : ledger_update_xml->modify_updates) { modify_entries.emplace_back(dev.terminal_id, current_time); } // delete_updates for (const auto& dev : ledger_update_xml->delete_updates) { delete_entries.emplace_back(dev.terminal_id, current_time); } // 写入日志 if (!new_entries.empty()) { log_file << "\n"; for (const auto& entry : new_entries) { write_log_entry(log_file, "add", entry.first, entry.second); } log_file << "\n"; } if (!modify_entries.empty()) { log_file << "\n"; for (const auto& entry : modify_entries) { write_log_entry(log_file, "modify", entry.first, entry.second); } log_file << "\n"; } if (!delete_entries.empty()) { log_file << "\n"; for (const auto& entry : delete_entries) { write_log_entry(log_file, "delete", entry.first, entry.second); } log_file << "\n"; } log_file.close(); std::cout << "Ledger log has been updated." << std::endl; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////shell打印日志 // ------------------ 全局日志列表和锁 ------------------ std::list errorList; std::list warnList; std::list normalList; pthread_mutex_t errorListMutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t warnListMutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t normalListMutex = PTHREAD_MUTEX_INITIALIZER; // ------------------ 输出开关 ------------------ bool errorOutputEnabled = false; // 是否将 error 级别写入 errorList bool warnOutputEnabled = false; // 是否将 warn 级别写入 warnList bool normalOutputEnabled = false; // 是否将 normal 级别写入 normalList // ------------------ 用于恢复原始缓冲区 ------------------ static std::streambuf* g_originalCoutBuf = NULL; static std::streambuf* g_originalClogBuf = NULL; static std::streambuf* g_originalCerrBuf = NULL; // ------------------ 日志级别枚举(C++98) ------------------ enum LogLevel { LOGERROR, LOGWARN, LOGNORMAL }; // ------------------------------------------------------------------ // TeeStreamBuf: 先写到原始buf(保持终端输出),再拷贝到list里 // ------------------------------------------------------------------ class TeeStreamBuf : public std::streambuf { public: // 默认构造:先把指针设为NULL TeeStreamBuf() : m_originalBuf(NULL), m_level(LOGNORMAL) { pthread_mutex_init(&m_mutex, NULL); } // 带参构造:直接初始化 TeeStreamBuf(std::streambuf* originalBuf, LogLevel level) : m_originalBuf(originalBuf), m_level(level) { pthread_mutex_init(&m_mutex, NULL); } // 析构函数:销毁互斥锁 virtual ~TeeStreamBuf() { pthread_mutex_destroy(&m_mutex); } // 自定义 init(...) 函数:在同一个对象上重新设置 void init(std::streambuf* originalBuf, LogLevel level) { m_originalBuf = originalBuf; m_level = level; pthread_mutex_lock(&m_mutex); m_buffer.clear(); pthread_mutex_unlock(&m_mutex); } protected: // 当 flush 或 std::endl 时会调用 sync() virtual int sync() { // 先让原始缓冲区执行同步 if (m_originalBuf) { m_originalBuf->pubsync(); } // 再将自身的缓存 flush flushBuffer(); return 0; // 成功 } // 当写入一个新字符时,overflow() 被调用 virtual int_type overflow(int_type ch) { if (ch == traits_type::eof()) { return ch; } // 1) 写到原始缓冲区 → 保留终端输出 if (m_originalBuf) { if (m_originalBuf->sputc(static_cast(ch)) == traits_type::eof()) { return traits_type::eof(); } } // 2) 存到我们的临时缓存,注意加锁保护 pthread_mutex_lock(&m_mutex); //防止多线程推入崩溃lnk20250305 m_buffer.push_back(static_cast(ch)); // 3) 遇到换行就 flushBuffer() if (ch == '\n') { flushBuffer_locked(); } pthread_mutex_unlock(&m_mutex); return ch; } private: // 内部版本:假定互斥锁已经被加锁 void flushBuffer_locked() { if (m_buffer.empty()) { return; } // 根据等级和对应开关,将 m_buffer 写入相应的 list switch (m_level) { case LOGERROR: if (normalOutputEnabled) { pthread_mutex_lock(&normalListMutex); normalList.push_back(m_buffer); pthread_mutex_unlock(&normalListMutex); } else if (warnOutputEnabled) { pthread_mutex_lock(&warnListMutex); warnList.push_back(m_buffer); pthread_mutex_unlock(&warnListMutex); } else if (errorOutputEnabled) { pthread_mutex_lock(&errorListMutex); errorList.push_back(m_buffer); pthread_mutex_unlock(&errorListMutex); } break; case LOGWARN: if (normalOutputEnabled) { pthread_mutex_lock(&normalListMutex); normalList.push_back(m_buffer); pthread_mutex_unlock(&normalListMutex); } else if (warnOutputEnabled) { pthread_mutex_lock(&warnListMutex); warnList.push_back(m_buffer); pthread_mutex_unlock(&warnListMutex); } break; case LOGNORMAL: if (normalOutputEnabled) { pthread_mutex_lock(&normalListMutex); normalList.push_back(m_buffer); pthread_mutex_unlock(&normalListMutex); } break; } m_buffer.clear(); } // 对外接口,内部对 m_buffer 加锁 void flushBuffer() { pthread_mutex_lock(&m_mutex); flushBuffer_locked(); pthread_mutex_unlock(&m_mutex); } private: // 禁止自动生成的赋值函数 TeeStreamBuf& operator=(const TeeStreamBuf&); private: std::streambuf* m_originalBuf; LogLevel m_level; std::string m_buffer; pthread_mutex_t m_mutex; }; // ------------------ 全局Tee对象(避免重复赋值) ------------------ static TeeStreamBuf g_errorTeeBuf; static TeeStreamBuf g_warnTeeBuf; static TeeStreamBuf g_normalTeeBuf; // ------------------ 重定向函数 ------------------ // 只在第一次启用时,用 init(...) 初始化 TeeStreamBuf; // 之后再启用时,不再 new 或 赋值,而是直接用之前构造好的对象。 void redirectErrorOutput(bool enabled) { errorOutputEnabled = enabled; if (enabled) { if (g_originalCerrBuf == NULL) { g_originalCerrBuf = std::cerr.rdbuf(); g_errorTeeBuf.init(g_originalCerrBuf, LOGERROR); } std::cerr.rdbuf(&g_errorTeeBuf); } else { if (g_originalCerrBuf) { std::cerr.rdbuf(g_originalCerrBuf); } } } void redirectWarnOutput(bool enabled) { warnOutputEnabled = enabled; if (enabled) { if (g_originalClogBuf == NULL) { g_originalClogBuf = std::clog.rdbuf(); g_warnTeeBuf.init(g_originalClogBuf, LOGWARN); } std::clog.rdbuf(&g_warnTeeBuf); } else { if (g_originalClogBuf) { std::clog.rdbuf(g_originalClogBuf); } } } void redirectNormalOutput(bool enabled) { normalOutputEnabled = enabled; if (enabled) { if (g_originalCoutBuf == NULL) { g_originalCoutBuf = std::cout.rdbuf(); g_normalTeeBuf.init(g_originalCoutBuf, LOGNORMAL); } std::cout.rdbuf(&g_normalTeeBuf); } else { if (g_originalCoutBuf) { std::cout.rdbuf(g_originalCoutBuf); } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////脚本控制 void execute_bash(string fun,int process_num,string type) { // 为 char 数组分配足够的空间 char p_num_str[20]; // 使用 sprintf 转换 std::sprintf(p_num_str, "%d", process_num); std::string script = FRONT_PATH + "/bin/set_process.sh";//使用setsid防止端口占用 const char* param1 = fun.c_str(); const char* param2 = p_num_str; const char* param3 = type.c_str(); // 构造完整的命令 char command[256]; snprintf(command, sizeof(command), "%s %s %s %s &", script.c_str(), param1, param2, param3); std::cout << "command:" << command < *ctopiclist, const std::string& path) { // 注释同原代码 const std::string strPhasic[4] = { "A", "B", "C", "T" }; const std::string strLine[4] = { "AB", "BC", "CA", "T" }; int nStart = 1, nEnd = 1; std::string strValueTemp; tinyxml2::XMLDocument doc; std::string xmlFile; if (path == "not define") { xmlFile = FRONT_PATH + "/etc/" + DEFAULT_CONFIG_FN; //默认映射文件 std::cout << "[调试] 加载XML路径: " << xmlFile << std::endl; } else { xmlFile = FRONT_PATH + "/dat/" + path + ".xml"; std::cout << "[调试] 加载XML路径: " << xmlFile << std::endl; } auto result = doc.LoadFile(xmlFile.c_str()); if (result != tinyxml2::XML_SUCCESS) { std::cout << "[错误] 无法打开XML文件: " << xmlFile << std::endl; return false; } tinyxml2::XMLElement* root = doc.RootElement(); if (!root) { std::cout << "[错误] XML无根节点!" << std::endl; return false; } for (tinyxml2::XMLElement* topicElem = root->FirstChildElement(); topicElem; topicElem = topicElem->NextSiblingElement()) { std::string strTag = topicElem->Name(); if (strTag == "Topic") { CTopic* topic = new CTopic(); topic->strTopic = topicElem->Attribute("name") ? topicElem->Attribute("name") : ""; ctopiclist->push_back(topic); std::cout << "[调试] 解析Topic节点: " << topic->strTopic << std::endl; // HISDATA、RTDATA if (topic->strTopic == "HISDATA" || topic->strTopic == "RTDATA") { for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) { if (std::string(dataTypeElem->Name()) == "DataType") { CDataType* dt = new CDataType(); dt->iDataType = dataTypeElem->IntAttribute("value", 0); dt->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : ""; dt->BaseFlag0 = 0; dt->BaseFlag1 = 0; topic->DataTypeList.push_back(dt); // 监测点 for (tinyxml2::XMLElement* monitorElem = dataTypeElem->FirstChildElement(); monitorElem; monitorElem = monitorElem->NextSiblingElement()) { if (std::string(monitorElem->Name()) == "Monitor") { CMonitor* mt = new CMonitor(); mt->strMonitor = monitorElem->Attribute("name") ? monitorElem->Attribute("name") : ""; mt->type = monitorElem->Attribute("type") ? monitorElem->Attribute("type") : ""; dt->MonitorList.push_back(mt); // 数据项 for (tinyxml2::XMLElement* itemElem = monitorElem->FirstChildElement(); itemElem; itemElem = itemElem->NextSiblingElement()) { if (std::string(itemElem->Name()) == "Item") { CItem* it = new CItem(); it->strItemName = itemElem->Attribute("name") ? itemElem->Attribute("name") : ""; it->type = itemElem->Attribute("type") ? itemElem->Attribute("type") : ""; if (it->strItemName == "FLAG") it->strItemValue = itemElem->Attribute("value") ? itemElem->Attribute("value") : ""; mt->ItemList.push_back(it); // Sequence for (tinyxml2::XMLElement* seqElem = itemElem->FirstChildElement(); seqElem; seqElem = seqElem->NextSiblingElement()) { if (std::string(seqElem->Name()) == "Sequence") { std::string strPhase = seqElem->Attribute("value") ? seqElem->Attribute("value") : ""; // 读取ABC三相 if ((!xml_flag || it->strItemName != "V") && strPhase == "7") { for (int n = 0; n < 3; ++n) { CSequence* sq = new CSequence(); sq->strSValue = strPhase; sq->type = seqElem->Attribute("type") ? seqElem->Attribute("type") : ""; sq->strSeq = strPhasic[n]; it->SequenceList.push_back(sq); for (tinyxml2::XMLElement* valueElem = seqElem->FirstChildElement(); valueElem; valueElem = valueElem->NextSiblingElement()) { if (std::string(valueElem->Name()) == "Value") { std::string strDVName = valueElem->Attribute("name") ? valueElem->Attribute("name") : ""; std::string strDAName = valueElem->Attribute("DA") ? valueElem->Attribute("DA") : ""; // l_phs* → phsAB if (strDAName.find("l_phs*") != std::string::npos) { strDAName.replace(strDAName.find("l_phs"), 5, "phs"); size_t starPos = strDAName.find("*"); if (starPos != std::string::npos) strDAName.replace(starPos, 1, strLine[n]); } else if (strDAName.find("phs*") != std::string::npos) { size_t starPos = strDAName.find("*"); if (starPos != std::string::npos) strDAName.replace(starPos, 1, sq->strSeq); } // 谐波数据 if (strDVName.find("%") != std::string::npos && strDAName.find("%-") != std::string::npos) { size_t firstP = strDVName.find('%'), lastP = strDVName.rfind('%'); if (firstP != std::string::npos && lastP != std::string::npos && firstP != lastP) { std::string harmRange = strDVName.substr(firstP + 1, lastP - firstP - 1); // 0,49 size_t comma = harmRange.find(','); if (comma != std::string::npos) { nStart = std::stoi(harmRange.substr(0, comma)); nEnd = std::stoi(harmRange.substr(comma + 1)); strValueTemp = "%" + harmRange + "%"; } } // 取DA的范围 size_t lb = strDAName.find('['), rb = strDAName.find(']'); int strDAoffset = 0; if (lb != std::string::npos && rb != std::string::npos && lb < rb) { std::string sub = strDAName.substr(lb + 1, rb - lb - 1); size_t dash = sub.find('-'); if (dash != std::string::npos) strDAoffset = std::stoi(sub.substr(dash + 1)); } int offset = valueElem->IntAttribute("Offset", 0); for (int i = nStart; i <= nEnd; ++i) { std::string strDVNameTemp = strDVName; std::string strDANameTemp = strDAName; CDataValue* dv1 = new CDataValue(); dv1->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : ""; if (valueElem->Attribute("Coefficient")) { dv1->strCoefficient = valueElem->Attribute("Coefficient"); dv1->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f); } dv1->strOffset = valueElem->Attribute("Offset") ? valueElem->Attribute("Offset") : ""; dv1->iOffset = offset; dv1->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : ""; size_t tmpPos = strDVNameTemp.find(strValueTemp); if (tmpPos != std::string::npos) strDVNameTemp.replace(tmpPos, strValueTemp.size(), std::to_string(i + offset)); size_t subPos = strDANameTemp.find('['); size_t subPos2 = strDANameTemp.find(']'); if (subPos != std::string::npos && subPos2 != std::string::npos) strDANameTemp.replace(subPos + 1, subPos2 - subPos - 1, std::to_string(i - strDAoffset)); dv1->strName = strDVNameTemp; dv1->DA = strDANameTemp; // BaseFlag, LimitUp, LimitDown if (valueElem->Attribute("BaseFlag")) { dv1->BaseFlag = valueElem->Attribute("BaseFlag"); if (dv1->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++; } else { dt->BaseFlag0++; dv1->BaseFlag = "0"; } dv1->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define"; dv1->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define"; if (!dv1->DO.empty() && !dv1->DA.empty()) dv1->strFullName = dv1->DO + "$" + dv1->DA; else dv1->strFullName = "not define"; sq->DataValueList.push_back(dv1); // 调试打印谐波展开 std::cout << "[调试] 谐波展开DataValue: " << dv1->strName << " DA=" << dv1->DA << std::endl; } } else { // 非谐波数据 CDataValue* dv2 = new CDataValue(); dv2->strName = strDVName; dv2->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : ""; if (valueElem->Attribute("Coefficient")) { dv2->strCoefficient = valueElem->Attribute("Coefficient"); dv2->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f); } dv2->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : ""; dv2->DA = strDAName; if (valueElem->Attribute("BaseFlag")) { dv2->BaseFlag = valueElem->Attribute("BaseFlag"); if (dv2->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++; } else { dt->BaseFlag0++; dv2->BaseFlag = "0"; } dv2->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define"; dv2->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define"; if (valueElem->Attribute("PltFlag") && std::string(valueElem->Attribute("PltFlag")) == "True") dv2->bPlt = true; else dv2->bPlt = false; if (!dv2->DO.empty() && !dv2->DA.empty()) dv2->strFullName = dv2->DO + "$" + dv2->DA; else dv2->strFullName = "not define"; sq->DataValueList.push_back(dv2); std::cout << "[调试] 普通DataValue: " << dv2->strName << " DA=" << dv2->DA << std::endl; } } } } } // T相 if (strPhase == "8") { CSequence* sq = new CSequence(); sq->strSValue = strPhase; sq->type = seqElem->Attribute("type") ? seqElem->Attribute("type") : ""; sq->strSeq = strPhasic[3]; it->SequenceList.push_back(sq); for (tinyxml2::XMLElement* valueElem = seqElem->FirstChildElement(); valueElem; valueElem = valueElem->NextSiblingElement()) { if (std::string(valueElem->Name()) == "Value") { CDataValue* dv2 = new CDataValue(); dv2->strName = valueElem->Attribute("name") ? valueElem->Attribute("name") : ""; dv2->type = valueElem->Attribute("type") ? valueElem->Attribute("type") : ""; if (valueElem->Attribute("Coefficient")) { dv2->strCoefficient = valueElem->Attribute("Coefficient"); dv2->fCoefficient = valueElem->FloatAttribute("Coefficient", 1.0f); } dv2->DO = valueElem->Attribute("DO") ? valueElem->Attribute("DO") : ""; dv2->DA = valueElem->Attribute("DA") ? valueElem->Attribute("DA") : ""; if (valueElem->Attribute("BaseFlag")) { dv2->BaseFlag = valueElem->Attribute("BaseFlag"); if (dv2->BaseFlag == "1") dt->BaseFlag1++; else dt->BaseFlag0++; } else { dt->BaseFlag0++; dv2->BaseFlag = "0"; } dv2->LimitUp = valueElem->Attribute("LimitUp") ? valueElem->Attribute("LimitUp") : "not define"; dv2->LimitDown = valueElem->Attribute("LimitDown") ? valueElem->Attribute("LimitDown") : "not define"; if (!dv2->DO.empty() && !dv2->DA.empty()) dv2->strFullName = dv2->DO + "$" + dv2->DA; else dv2->strFullName = "not define"; sq->DataValueList.push_back(dv2); std::cout << "[调试] T相DataValue: " << dv2->strName << std::endl; } } } } } } } } } } } } // RTDATASOE else if (topic->strTopic == "RTDATASOE") { for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) { if (std::string(dataTypeElem->Name()) == "DataType") { CDataType* dt = new CDataType(); dt->iDataType = dataTypeElem->IntAttribute("value", 0); dt->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : ""; topic->DataTypeList.push_back(dt); for (tinyxml2::XMLElement* soeElem = dataTypeElem->FirstChildElement(); soeElem; soeElem = soeElem->NextSiblingElement()) { if (std::string(soeElem->Name()) == "SOE") { CEventData* ed = new CEventData(); ed->triggerFlag = soeElem->Attribute("TriggerFlag") ? soeElem->Attribute("TriggerFlag") : ""; ed->DO = soeElem->Attribute("DO") ? soeElem->Attribute("DO") : ""; ed->DA = soeElem->Attribute("DA") ? soeElem->Attribute("DA") : ""; ed->type = soeElem->Attribute("type") ? soeElem->Attribute("type") : ""; if (!ed->DO.empty() && !ed->DA.empty()) ed->strFullName = ed->DO + "$" + ed->DA; else ed->strFullName = "not define"; dt->SOEList.push_back(ed); std::cout << "[调试] SOE事件: " << ed->strFullName << std::endl; } } } } } // SOEDATA else if (topic->strTopic == "SOEDATA") { for (tinyxml2::XMLElement* dataTypeElem = topicElem->FirstChildElement(); dataTypeElem; dataTypeElem = dataTypeElem->NextSiblingElement()) { if (std::string(dataTypeElem->Name()) == "DataType") { CEventData* ed = new CEventData(); ed->triggerFlag = dataTypeElem->Attribute("name") ? dataTypeElem->Attribute("name") : ""; ed->DO = dataTypeElem->Attribute("DO") ? dataTypeElem->Attribute("DO") : ""; ed->DA = dataTypeElem->Attribute("DA") ? dataTypeElem->Attribute("DA") : ""; ed->type = dataTypeElem->Attribute("type") ? dataTypeElem->Attribute("type") : ""; if (!ed->DO.empty() && !ed->DA.empty()) ed->strFullName = ed->DO + "$" + ed->DA; else ed->strFullName = "not define"; cfg->SOEList.push_back(ed); std::cout << "[调试] SOEDATA事件: " << ed->strFullName << std::endl; } } } } // 其他节点 else if (strTag == "WavePhasic") { const char* flagAttr = topicElem->Attribute("Flag"); if (flagAttr) cfg->WavePhasicFlag = flagAttr; if (!cfg->WavePhasicFlag.empty() && cfg->WavePhasicFlag == "1") { const char* aAttr = topicElem->Attribute("A"); if (aAttr) cfg->WavePhasicA = aAttr; const char* bAttr = topicElem->Attribute("B"); if (bAttr) cfg->WavePhasicB = bAttr; const char* cAttr = topicElem->Attribute("C"); if (cAttr) cfg->WavePhasicC = cAttr; } std::cout << "[调试] WavePhasic解析" << std::endl; } else if (strTag == "UnitOfTime") { const char* unitAttr = topicElem->Attribute("Unit"); if (unitAttr) cfg->UnitOfTimeUnit = unitAttr; std::cout << "[调试] UnitOfTime解析" << std::endl; } else if (strTag == "ValueOfTime") { const char* unitAttr = topicElem->Attribute("Unit"); if (unitAttr) cfg->ValueOfTimeUnit = unitAttr; std::cout << "[调试] ValueOfTime解析" << std::endl; } else if (strTag == "ComtradeFile") { const char* waveTimeFlag = topicElem->Attribute("WaveTimeFlag"); if (waveTimeFlag) cfg->WaveTimeFlag = waveTimeFlag; std::cout << "[调试] ComtradeFile解析" << std::endl; } else if (strTag == "IED") { const char* nameAttr = topicElem->Attribute("name"); if (nameAttr) cfg->IEDname = nameAttr; std::cout << "[调试] IED解析" << std::endl; } else if (strTag == "LDevice") { const char* prefixAttr = topicElem->Attribute("Prefix"); if (prefixAttr) cfg->LDevicePrefix = prefixAttr; std::cout << "[调试] LDevice解析" << std::endl; } } return true; } //清理旧的映射配置 void clearXmlConfigAndTopicList(Xmldata* data) { // 清空 XmlConfig data->xmlcfg = XmlConfig(); // 通过重新赋值重置 xmlcfg // 清空 topicList std::list::iterator it; for (it = data->topicList.begin(); it != data->topicList.end(); ++it) { delete *it; // 释放内存 } data->topicList.clear(); // 清空链表 } //配置映射文件单个 void Set_xml_nodeinfo_one(const std::string& dev_type) { bool ret = false; // 假定 xmlinfo_list 是 std::map if (xmlinfo_list.count(dev_type) && xmlinfo_list[dev_type] != nullptr) { // 已存在这个类型的节点 if (xmlinfo_list[dev_type]->updataflag == true) { // 需要更新 // 删除这个点的 xmlcfg 和 topicList clearXmlConfigAndTopicList(xmlinfo_list[dev_type]); ret = ParseXMLConfig2( 0, &(xmlinfo_list[dev_type]->xmlcfg), &(xmlinfo_list[dev_type]->topicList), xmlinfo_list[dev_type]->xmlbase.MODEL_ID ); if (!ret) { std::cout << "!!!! this ledger xml config fail!!!!" << std::endl; } } } else { std::cout << "xmlinfo_list not contain this devtype" << std::endl; } // 处理角形 if (isdelta_flag) { if (xmlinfo_list2.count(dev_type) && xmlinfo_list2[dev_type] != nullptr) { // 已存在 if (xmlinfo_list2[dev_type]->updataflag == true) { // 需要更新 // 删除这个点的 xmlcfg 和 topicList clearXmlConfigAndTopicList(xmlinfo_list2[dev_type]); ret = ParseXMLConfig2( 1, &(xmlinfo_list2[dev_type]->xmlcfg), &(xmlinfo_list2[dev_type]->topicList), xmlinfo_list2[dev_type]->xmlbase.MODEL_ID ); if (!ret) { std::cout << "!!!! this ledger xml config fail!!!!" << std::endl; } } } else { std::cout << "xmlinfo_list2 not contain this devtype" << std::endl; } } } //配置映射文件全部 void Set_xml_nodeinfo() { // 配置无对应 xml 文件时的默认解析配置 if (!DEFAULT_CONFIG_FN.empty()) { //可改为在配置中读取默认映射文件名 std::string path = "not define"; ParseXMLConfig2(0, &xmlcfg, &topicList, path); // 调用 ParseXMLConfig() 解析 JiangSu_Config.xml 配置文件 if (isdelta_flag) { ParseXMLConfig2(1, &xmlcfg2, &topicList2, path); // 角型接线 } } if (!xmlinfo_list.empty()) { std::cout << "!!!!!!!!!! xmlinfo_list.size() != 0 !!!!!!!!!!!" << std::endl; for (auto& pair : xmlinfo_list) { const std::string& key = pair.first; Xmldata* value = pair.second; if (value && value->updataflag) { ParseXMLConfig2(0, &(value->xmlcfg), &(value->topicList), value->xmlbase.MODEL_ID); } } if (isdelta_flag) { for (auto& pair : xmlinfo_list2) { const std::string& key = pair.first; Xmldata* value = pair.second; if (value && value->updataflag) { ParseXMLConfig2(1, &(value->xmlcfg), &(value->topicList), value->xmlbase.MODEL_ID); } } } } else { std::cout << "!!!!!!!!!! xmlinfo_list.size() == 0 !!!!!!!!!!!" << std::endl; } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////录播波形匹配 // 工具函数:解析 "dd/MM/yyyy,hh:mm:ss.zzz" 格式的字符串为时间戳(毫秒) static long long parse_datetime_to_epoch_ms(const std::string& dt_str) { std::tm tm = {}; char dummy; int ms = 0; std::istringstream ss(dt_str); ss >> std::get_time(&tm, "%d/%m/%Y,%H:%M:%S") >> dummy >> ms; if (ss.fail()) return 0; std::time_t time_sec = std::mktime(&tm); if (time_sec < 0) return 0; return static_cast(time_sec) * 1000 + ms; } // 主函数:从 .cfg 文件中提取起始和触发时间戳(毫秒) bool extract_timestamp_from_cfg_file(const std::string& cfg_path, long long& start_tm, long long& trig_tm) { struct stat st; if (stat(cfg_path.c_str(), &st) != 0) { std::cerr << "File not found: " << cfg_path << "\n"; return false; } std::ifstream infile(cfg_path); if (!infile) { std::cerr << "Cannot open file: " << cfg_path << "\n"; return false; } std::string line, prev_line, current_line; while (std::getline(infile, line)) { // 去除前后空白 line.erase(line.find_last_not_of(" \t\r\n") + 1); line.erase(0, line.find_first_not_of(" \t\r\n")); std::string upper = line; std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper); if (upper == "ASCII" || upper == "BINARY") break; prev_line = current_line; current_line = line; } if (prev_line.length() > 3) prev_line = prev_line.substr(0, prev_line.length() - 3); if (current_line.length() > 3) current_line = current_line.substr(0, current_line.length() - 3); start_tm = parse_datetime_to_epoch_ms(prev_line); trig_tm = parse_datetime_to_epoch_ms(current_line); return start_tm > 0 && trig_tm > 0; } bool compare_qvvr_and_file(const std::string& cfg_path, const std::vector& data_list,qvvr_data& matched_data) { long long start_tm = 0; long long trig_tm = 0; // 提取 .cfg 文件中的时间戳 if (!extract_timestamp_from_cfg_file(cfg_path, start_tm, trig_tm)) { std::cerr << "Failed to extract timestamp from cfg file: " << cfg_path << "\n"; return false; } //打印提取到的时间戳 std::cout << "[调试] 提取到的起始时间戳: " << start_tm << ", 触发时间戳: " << trig_tm << "\n"; // 遍历所有暂态事件,查找与 trig_tm 匹配的 for (const auto& data : data_list) { long long diff = static_cast(data.QVVR_time) - trig_tm; if (std::abs(diff) <= 1) { matched_data = data; // 返回匹配到的事件 return true; } } return false; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////数据转换函数 // DataArrayItem to_json void to_json(nlohmann::json& j, const DataArrayItem& d) { j = nlohmann::json{ {"dataAttr", d.DataAttr}, {"dataTimeSec", d.DataTimeSec}, {"dataTimeUSec", d.DataTimeUSec}, {"dataTag", d.DataTag}, {"data", d.Data} }; } // MsgObj to_json void to_json(nlohmann::json& j, const MsgObj& m) { j = nlohmann::json{ {"clDid", m.Cldid}, {"dataType", m.DataType}, {"dataAttr", m.DataAttr}, {"dsNameIdx", m.DsNameIdx}, {"dataArray", m.DataArray} }; } // FullObj to_json void to_json(nlohmann::json& j, const FullObj& f) { j = nlohmann::json{ {"id", f.mac}, {"mid", f.Mid}, {"did", f.Did}, {"pri", f.Pri}, {"type", f.Type}, {"msg", f.Msg} }; } std::string generate_json( const std::string mac, int Mid, //需应答的报文订阅者收到后需以此ID应答,无需应答填入“-1” int Did, //设备唯一标识Ldid,填入0代表Ndid。 int Pri, //报文处理的优先级 int Type, //消息类型 int Cldid, //逻辑子设备ID,0-逻辑设备本身,无填-1 int DataType, //数据类型,0-表示以数据集方式上送 int DataAttr, //数据属性:无“0”、实时“1”、统计“2”等。 int DsNameIdx, //数据集序号(以数据集方式上送),无填-1 const std::vector& dataArray //数据数组。 ) { FullObj fobj; fobj.mac = mac; fobj.Mid = Mid; fobj.Did = Did; fobj.Pri = Pri; fobj.Type = Type; fobj.Msg.Cldid = Cldid; fobj.Msg.DataType = DataType; fobj.Msg.DataAttr = DataAttr; fobj.Msg.DsNameIdx = DsNameIdx; fobj.Msg.DataArray = dataArray; nlohmann::json j = fobj; return j.dump(); // 输出标准 json 字符串 } void upload_data_test(){ std::vector arr; arr.push_back({1, 1725477660, 0, 1, "xxxx"}); //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”,4-“Cp95” //数据时标,相对1970年的秒,无效填入“-1” //数据时标,微秒钟,无效填入“-1” //数据标识,1-标识数据异常 //数据序列(数据集上送时将二进制数据流转换成Base64字符串,其他数据为object) arr.push_back({2, 1691741340, 0, 1, "yyyy"}); std::string js = generate_json( "123",-1, 1, 1, 4866, 1, 0, 2, 1, arr ); std::cout << js << std::endl; queue_data_t data; data.monitor_no = 1; data.strTopic = TOPIC_ALARM; data.strText = js; data.mp_id = "test"; data.tag = G_ROCKETMQ_TAG_TEST; data.key = G_ROCKETMQ_KEY_TEST; std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(data); } //////////////////////////////////////////////////////////////////////////////////////////////////台账赋值给通信 std::vector GenerateDeviceInfoFromLedger(const std::vector& terminal_devlist) { std::lock_guard lock(ledgermtx); std::vector devices; for (const auto& terminal : terminal_devlist) { DeviceInfo device; device.device_id = terminal.terminal_id; device.name = terminal.terminal_name; device.model = terminal.dev_type; device.mac = terminal.addr_str; device.status = 1; for (const auto& monitor : terminal.line) { PointInfo point; point.point_id = monitor.monitor_id; point.name = monitor.monitor_name; point.device_id = terminal.terminal_id; point.PT1 = monitor.PT1; point.PT2 = monitor.PT2; point.CT1 = monitor.CT1; point.CT2 = monitor.CT2; point.strScale = monitor.voltage_level; point.nCpuNo = std::stoi(monitor.logical_device_seq); point.nPTType = std::stoi(monitor.terminal_connect); device.points.push_back(point); } devices.push_back(device); } return devices; } ////////////////////////////////////////////////////////////////////////////////////////////////////录波文件通知 bool assign_qvvr_file_list(const std::string& id, ushort nCpuNo, const std::vector& file_list_raw) { // ★新增:台账加锁(若上层已有锁,可移除本行) std::lock_guard lk(ledgermtx); std::vector file_names; // 1. 提取文件名部分 for (const auto& full_path_raw : file_list_raw) { std::string full_path = sanitize(full_path_raw); // ★修改:清洗入参路径 size_t pos = full_path.find_last_of("/\\"); std::string name = (pos != std::string::npos && pos + 1 < full_path.size()) ? full_path.substr(pos + 1) : full_path; name = sanitize(name); // ★修改:清洗提取的文件名 file_names.push_back(name); } // ★可选:去重(如果 file_list_raw 里可能有重复) std::sort(file_names.begin(), file_names.end()); file_names.erase(std::unique(file_names.begin(), file_names.end()), file_names.end()); // 2. 遍历终端 for (auto& dev : terminal_devlist) { if (dev.terminal_id == id) { // 根据终端id匹配终端 for (auto& monitor : dev.line) { try { // ★修改:清洗 logical_device_seq 再进行转换 std::string seq_str = sanitize(monitor.logical_device_seq); ushort monitor_seq = static_cast(std::stoi(seq_str)); if (monitor_seq == nCpuNo) { // 根据监测点编号匹配监测点 // 构造 qvvr_file qvvr_file qfile; qfile.file_name.assign(file_names.begin(), file_names.end()); // 终端文件列表(已清洗) qfile.is_download = false; qfile.is_pair = false; qfile.file_time_count = 0; qfile.used_status = true; // 添加到唯一的 qvvrevent monitor.qvvrevent.qvvrfile.push_back(std::move(qfile)); // 记录暂态文件组 return true; } } catch (...) { continue; } } } } return false; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////下载成功通知 //提取下载路径的文件名 std::string extract_filename1(const std::string& path) { size_t pos = path.find_last_of("/\\"); return (pos != std::string::npos) ? path.substr(pos + 1) : path; } // ★新增:dirname,返回“目录/”(保留末尾斜杠;若没有目录则返回空串) static inline std::string dirname_with_slash(const std::string& path) { size_t pos = path.find_last_of("/\\"); if (pos == std::string::npos) return std::string{}; return path.substr(0, pos + 1); } //发送匹配的所有录波文件 bool SendAllQvvrFiles(qvvr_file& qfile, std::string& out_wavepath) { std::vector wavepaths; std::string first_wavepath; bool send_success = true; for (const auto& file_localpath : qfile.file_download) { std::string file_cloudpath = "comtrade/" + dirname_with_slash(file_localpath); std::string wavepath_result; // 发送本地文件到远端,返回 wavepath SOEFileWeb(const_cast(file_localpath), file_cloudpath, wavepath_result); // 如果失败,重发一次 if (wavepath_result.empty()) { std::cerr << "[SOEFileWeb] Warning: first send failed for file: " << file_localpath << ", retrying...\n"; SOEFileWeb(const_cast(file_localpath), file_cloudpath, wavepath_result); } if (wavepath_result.empty()) { send_success = false; std::cerr << "[SOEFileWeb] Failed: wavepath empty for file: " << file_localpath << "\n"; break; } if (wavepaths.empty()) { first_wavepath = wavepath_result; } else if (wavepath_result != first_wavepath) { send_success = false; std::cerr << "[SOEFileWeb] Mismatch wavepath: " << wavepath_result << " vs " << first_wavepath << "\n"; break; } wavepaths.push_back(wavepath_result); } // 检查数量是否一致 if (!send_success || wavepaths.size() != qfile.file_download.size()) { std::cerr << "[SOEFileWeb] Failed to send all qvvr files. " << "Sent: " << wavepaths.size() << ", Expected: " << qfile.file_download.size() << "\n"; return false; } out_wavepath = first_wavepath; // 返回统一的 wavepath std::cout << "[SOEFileWeb] Success: all files sent for qfile. Wavepath = " << first_wavepath << "\n"; return true; } //文件下载结束接口 bool update_qvvr_file_download(const std::string& filename_with_mac_in, const std::string& terminal_id) { // ★ 先把原始入参清洗 std::string filename_with_mac = sanitize(filename_with_mac_in); std::cout << "[update_qvvr_file_download] raw=" << filename_with_mac_in << " | sanitized=" << filename_with_mac << " | terminal_id=" << terminal_id << std::endl; //台账加锁 std::unique_lock lock(ledgermtx); // 去除 mac 路径前缀,仅保留文件名 std::string filename = sanitize(extract_filename1(filename_with_mac)); // 提取逻辑序号(如 PQ_PQLD1 → 1) size_t under_pos1 = filename.find('_'); if (under_pos1 == std::string::npos) { std::cout << "[DEBUG] 未找到 '_',filename=" << filename << ",under_pos=npos,返回 false\n"; return false; } size_t under_pos2 = filename.find('_', under_pos1 + 1); std::string type_part = (under_pos2 == std::string::npos) ? filename.substr(0, under_pos1) // 兜底:只有一个下划线 : filename.substr(0, under_pos2); // 取到第二个下划线(得到 PQ_PQLD1) std::cout << "[DEBUG] type_part=" << type_part << " (under_pos1=" << under_pos1 << ", under_pos2=" << under_pos2 << ")\n"; size_t num_start = type_part.find_last_not_of("0123456789"); if (num_start == std::string::npos || num_start + 1 >= type_part.size()) { std::cout << "[DEBUG] 数字起始位置异常:num_start=" << num_start << ",type_part.size()=" << type_part.size() << ",type_part=\"" << type_part << "\",返回 false\n"; return false; } std::string seq_str = type_part.substr(num_start + 1); unsigned short logical_seq = static_cast(std::stoul(seq_str)); std::cout << "[DEBUG] 解析到 logical_seq=" << logical_seq << "\n"; //找终端 for (auto& dev : terminal_devlist) { if (dev.terminal_id != terminal_id) { std::cout << "[cmp-terminal-id][NOT-MATCH]" << " dev_id=" << dev.terminal_id << " target_id=" << terminal_id << std::endl; continue; } //找监测点 for (auto& monitor : dev.line) { try { // 将监测点台账中的 logical_device_seq 转换为数字进行匹配 ushort monitor_seq = static_cast(std::stoi(monitor.logical_device_seq)); if (monitor_seq != logical_seq) { // ★新增:不匹配时对比打印 std::cout << "[cmp-monitor-seq][NOT-MATCH]" << " monitor_id=" << monitor.monitor_id // ★ 这里之前打印的是 seq_str,容易误导。改为 ledger 的原始串: << " seq_in_ledger_raw=\"" << monitor.logical_device_seq << "\"" << " parsed=" << monitor_seq << " target_seq=" << logical_seq << std::endl; continue; } else{ std::cout << "[cmp-monitor-seq][MATCH!!!]" << " monitor_id=" << monitor.monitor_id << " seq_in_ledger_raw=\"" << monitor.logical_device_seq << "\"" << " parsed=" << monitor_seq << " target_seq=" << logical_seq << std::endl; } } catch (const std::exception& e) { // ★新增:解析失败详细原因 std::cout << "[cmp-monitor-seq][PARSE-FAIL]" << " monitor_id=" << monitor.monitor_id << " seq_in_ledger=\"" << monitor.logical_device_seq << "\"" << " err=" << e.what() << std::endl; continue; // logical_device_seq 非法,跳过 }catch (...) { // ★新增:未知异常 std::cout << "[cmp-monitor-seq][PARSE-FAIL]" << " monitor_id=" << monitor.monitor_id << " seq_in_ledger=\"" << monitor.logical_device_seq << "\"" << " err=" << std::endl; continue; // logical_device_seq 非法,跳过 } // 匹配监测点下 qvvrfile 中的 file_name for (size_t i = 0; i < monitor.qvvrevent.qvvrfile.size(); ++i) { auto& qfile = monitor.qvvrevent.qvvrfile[i]; // file_name 中是文件名,需与提取的 filename 比较 auto it = std::find(qfile.file_name.begin(), qfile.file_name.end(), filename); //找到匹配文件名 if (it != qfile.file_name.end()) { // 添加到 file_download(记录完整路径,避免重复) if (std::find(qfile.file_download.begin(), qfile.file_download.end(), filename_with_mac) == qfile.file_download.end()) { std::cout << "[update_qvvr_file_download] Adding downloaded file: " << filename_with_mac << std::endl; qfile.file_download.push_back(filename_with_mac); } qfile.file_time_count = 0; // 任一录波文件下载后,计时归零 // file_download 中是完整路径,需提取文件名后与 file_name 做集合比较 std::set s_name(qfile.file_name.begin(), qfile.file_name.end()); std::set s_down; for (const auto& path : qfile.file_download) { s_down.insert(sanitize(extract_filename1(path))); // 提取每个路径中的文件名 } //打印s_name和s_down内容 std::cout << "[update_qvvr_file_download] Expected files (file_name): "; for (const auto& fn : s_name) std::cout << fn << " "; std::cout << std::endl; std::cout << "[update_qvvr_file_download] Downloaded files (file_download): "; for (const auto& fn : s_down) std::cout << fn << " "; std::cout << std::endl; // 检查 file_download 是否与 file_name 完全一致(集合相同) if (s_name == s_down) { std::cout << "[update_qvvr_file_download] All files downloaded for qfile in logical_seq=" << logical_seq << std::endl; qfile.is_download = true; // 全部下载完成 // 找到其中的 .cfg 文件进行匹配 for (const auto& fpath : qfile.file_download) { std::string fname = extract_filename1(fpath); if (fname.size() >= 4 && fname.substr(fname.size() - 4) == ".cfg") { // 提取文件时标和监测点事件的时标匹配 qvvr_data matched; if (compare_qvvr_and_file(fpath, monitor.qvvrevent.qvvrdata,matched)) { qfile.is_pair = true; // 文件与事件匹配成功 // ★新增:上传前拷贝“将要上传的文件列表”,避免锁外用容器引用 std::vector files_to_send(qfile.file_download.begin(), qfile.file_download.end()); // ★新增:构造一个临时 qvvr_file,仅用于上传(不改动原结构) qvvr_file tmp_send; tmp_send.file_download.assign(files_to_send.begin(), files_to_send.end()); // 发送所有文件(已下载完成) std::string wavepath; // ★在解锁前,备份“签名”,用于回锁后定位同一个 qfile std::set sig_names(qfile.file_name.begin(), qfile.file_name.end()); std::set sig_downs(qfile.file_download.begin(), qfile.file_download.end()); // ★修改:把上传与上送 JSON 放到“解锁区间” lock.unlock(); // ★新增:提前解锁 if (SendAllQvvrFiles(qfile, wavepath)) { //文件发送成功后更新事件 transfer_json_qvvr_data(terminal_id, logical_seq, matched.QVVR_Amg, matched.QVVR_PerTime, matched.QVVR_time, matched.QVVR_type, matched.phase, wavepath); // ★新增:上传成功后再加锁,准备修改台账 lock.lock(); // 删除上传成功的文件 for (const auto& uploaded_file : qfile.file_download) { if (std::remove(uploaded_file.c_str()) != 0) { std::cerr << "[Cleanup] Failed to delete file: " << uploaded_file << "\n"; } else { std::cout << "[Cleanup] Deleted uploaded file: " << uploaded_file << "\n"; } } // ★替换原来的 i n(x.file_name.begin(), x.file_name.end()); std::set d(x.file_download.begin(), x.file_download.end()); return n==sig_names && d==sig_downs; }); if (it_qf != monitor.qvvrevent.qvvrfile.end()) { monitor.qvvrevent.qvvrfile.erase(it_qf); // ✔ 删到同一条 } else { std::cerr << "[Cleanup] qvvrfile changed; target group not found, skip erase\n"; } //清除暂态事件 auto it = std::find_if( monitor.qvvrevent.qvvrdata.begin(), monitor.qvvrevent.qvvrdata.end(), [&](const qvvr_data& d) { return d.QVVR_time == matched.QVVR_time; }); if (it != monitor.qvvrevent.qvvrdata.end()) { monitor.qvvrevent.qvvrdata.erase(it); } } else { lock.lock(); // ★新增:失败时补回锁 std::cerr << "[update_qvvr_file_download] Failed to send qvvr files for logical_seq=" << logical_seq << std::endl; } } else { std::cout << "[update_qvvr_file_download] No matching qvvr_data found for cfg file: " << fpath << std::endl; } break; // 只处理第一个 cfg 文件 } } } else{ std::cout << "qvvr file still imcomplete!!!" << std::endl; } lock.unlock(); return true; // 当前文件处理成功 } } std::cout << "file name doesnt match any file in this monitor!!!" << std::endl; } } lock.unlock(); return false; // 未匹配到终端ID或逻辑序号对应的监测点 } ////////////////////////////////////////////////////////////////////////////////////////提取mac std::string normalize_mac(const std::string &mac) { std::string res; res.reserve(mac.size()); for (char c : mac) { if (c != '-' && c != ':' && c != ' ') res.push_back(c); } return res; } ////////////////////////////////////////////////////////////////////////////////////////目录信息发送接口函数 bool send_file_list(terminal_dev* dev, const std::vector& FileList) { if (!dev) { std::cerr << "[send_file_list_locked] dev=nullptr\n"; return false; } // 判断 isbusy==1 且 busytype==READING_FILEMENU if (dev->isbusy != 1 || dev->busytype != static_cast(DeviceState::READING_FILEMENU)) { std::cerr << "[send_file_list] device not in READING_FILEMENU state." << std::endl; return false; } // 构造 JSON 报文 nlohmann::json j; j["guid"] = dev->guid; j["FrontId"] = FRONT_INST; // 这里填你的前置机id j["Node"] = g_front_seg_index; // 节点号 j["Dev_mac"] = normalize_mac(dev->addr_str); // addr_str 存的是 MAC // 构造 DirInfo 数组 nlohmann::json dirArray = nlohmann::json::array(); for (const auto &f : FileList) { nlohmann::json item; item["Name"] = f.name; item["Type"] = (f.flag == 0) ? "dir" : "file"; item["Size"] = f.size; dirArray.push_back(item); } // 构造 Detail 部分 nlohmann::json detail; detail["Type"] = 0x2131; // 读取目录 detail["Msg"] = { {"DirInfo", dirArray} }; detail["Code"] = 200; // 请求成功 // 放到顶层 j["Detail"] = detail; // 打印调试 std::cout << j.dump(4) << std::endl; // ---- 入队发送 ---- queue_data_t connect_info; connect_info.strTopic = Topic_Reply_Topic; connect_info.strText = j.dump(); // 序列化为字符串 connect_info.tag = Topic_Reply_Tag; connect_info.key = Topic_Reply_Key; { std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(std::move(connect_info)); } // 调试打印 std::cout << "[send_file_list] queued: " << j.dump() << std::endl; //发送后清除guid和标志 if (dev->isbusy > 0) { dev->isbusy--; } if(dev->isbusy == 0){ dev->guid.clear(); dev->busytype = 0; } return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////检查云前置终端的mq业务超时 std::string get_type_by_state(int state) { switch (state) { case static_cast(DeviceState::IDLE): return "空闲状态"; case static_cast(DeviceState::READING_STATS): return "读取统计数据"; case static_cast(DeviceState::READING_STATS_TIME): return "读取统计时间"; case static_cast(DeviceState::READING_REALSTAT): return "读取实时数据"; case static_cast(DeviceState::READING_EVENTFILE): return "暂态波形文件下载"; case static_cast(DeviceState::READING_FILEMENU): return "读取文件目录"; case static_cast(DeviceState::READING_FILEDATA): return "读取文件数据"; case static_cast(DeviceState::READING_FIXEDVALUE): return "读取测点定值"; case static_cast(DeviceState::READING_FIXEDVALUEDES): return "读取测点定值描述"; case static_cast(DeviceState::SET_FIXEDVALUE): return "设置测点定值"; case static_cast(DeviceState::READING_INTERFIXEDVALUE): return "读取内部定值"; case static_cast(DeviceState::READING_INTERFIXEDVALUEDES): return "读取内部定值描述"; case static_cast(DeviceState::READING_CONTROLWORD): return "读取控制字描述"; case static_cast(DeviceState::SET_INTERFIXEDVALUE): return "设置内部定值"; case static_cast(DeviceState::READING_RUNNINGINFORMATION_1): return "读取装置运行信息(主动触发)"; case static_cast(DeviceState::READING_RUNNINGINFORMATION_2): return "读取装置运行信息(定时执行)"; case static_cast(DeviceState::READING_DEVVERSION): return "读取装置版本配置信息"; case static_cast(DeviceState::SET_RIGHTTIME): return "设置装置对时"; case static_cast(DeviceState::READING_EVENTLOG): return "补招事件日志"; case static_cast(DeviceState::READING_STATSFILE): return "补招稳态数据文件"; case static_cast(DeviceState::CUSTOM_ACTION): return "自定义动作"; default: return "未知业务"; // 未匹配的类型 } } // 定时检查业务超时 void check_device_busy_timeout() { std::lock_guard lock(ledgermtx); for (auto &dev : terminal_devlist) { if (dev.isbusy != 0) // 有业务在进行 { dev.busytimecount++; if (dev.busytype == static_cast(DeviceState::READING_FILEDATA) || dev.busytype == static_cast(DeviceState::READING_STATSFILE)) //下载文件业务 { if (dev.busytimecount > 60) { std::cout << "[Timeout] Device " << dev.terminal_id << " busytype=READING_FILEDATA 超时(" << dev.busytimecount << "s)" << std::endl; //标记当前 RUNNING 的首条为 FAILED(或 TIMEOUT),而不是只清设备状态 bool marked = false; if (dev.busytype == static_cast(DeviceState::READING_STATSFILE)) { for (auto& lm : dev.line) { if (!lm.recall_list_static.empty() && lm.recall_list_static.front().recall_status == static_cast(RecallStatus::RUNNING)) { lm.recall_list_static.front().recall_status = static_cast(RecallStatus::FAILED); marked = true; break; } } } //发送超时响应 //send_reply_to_cloud(static_cast(ResponseCode::TIMEOUT),dev.terminal_id,get_type_by_state(dev.busytype),dev.guid,dev.mac); send_reply_to_queue(dev.guid, static_cast(ResponseCode::TIMEOUT), "终端 id: " + dev.terminal_id + "进行业务:" + get_type_by_state(dev.busytype) +"超时,停止该业务处理"); // 超时清空状态 //若还有未处理条目,则仅复位计时,不清设备状态,交给 check_recall_event() 弹掉 FAILED 并继续下一条 bool any_non_empty = false; for (auto& lm : dev.line) { if (!lm.recall_list_static.empty()) { any_non_empty = true; break; } } //补招业务的话 if (any_non_empty && dev.busytype == static_cast(DeviceState::READING_STATSFILE)) { dev.busytimecount = 0; // 复位计时 dev.isbusy = 1; // 仍视为在补招流程中 dev.busytype = static_cast(DeviceState::READING_STATSFILE); //不要清 dev.guid,让回调/通知能“对上号” //因为超时后发送结果需要guid,所以不能清,在补招任务中pop就行 } else { //队列都空了,或者是正在进行其他业务才清设备状态 dev.guid.clear(); dev.busytype = static_cast(DeviceState::IDLE); dev.isbusy = 0; dev.busytimecount = 0; } } } else //其他业务,包括补招日志都是20s一条,一问一答的时间 { if (dev.busytimecount > 20) { std::cout << "[Timeout] Device " << dev.terminal_id << " busytype=" << dev.busytype << " 超时(" << dev.busytimecount << "s)" << std::endl; //标记当前 RUNNING 的首条为 FAILED(或 TIMEOUT),而不是只清设备状态 bool marked = false; if (dev.busytype == static_cast(DeviceState::READING_EVENTLOG)) { for (auto& lm : dev.line) { if (!lm.recall_list.empty() && lm.recall_list.front().recall_status == static_cast(RecallStatus::RUNNING)) { lm.recall_list.front().recall_status = static_cast(RecallStatus::FAILED); marked = true; break; } } } //发送超时响应 //send_reply_to_cloud(static_cast(ResponseCode::TIMEOUT),dev.terminal_id,get_type_by_state(dev.busytype),dev.guid,dev.mac); send_reply_to_queue(dev.guid, static_cast(ResponseCode::TIMEOUT), "终端 id: " + dev.terminal_id + "进行业务:" + get_type_by_state(dev.busytype) +"超时,停止该业务处理"); // 超时清空状态 //若还有未处理条目,则仅复位计时,不清设备状态,交给 check_recall_event() 弹掉 FAILED 并继续下一条 bool any_non_empty = false; for (auto& lm : dev.line) { if (!lm.recall_list.empty()) { any_non_empty = true; break; } } //补招业务的话 if (any_non_empty && dev.busytype == static_cast(DeviceState::READING_EVENTLOG)) { dev.busytimecount = 0; // 复位计时 dev.isbusy = 1; // 仍视为在补招流程中 dev.busytype = static_cast(DeviceState::READING_EVENTLOG); //不要清 dev.guid,让回调/通知能“对上号” //因为超时后发送结果需要guid,所以不能清,在补招任务中pop就行 } else { //队列都空了,或者是正在进行其他业务才清设备状态 dev.guid.clear(); dev.busytype = static_cast(DeviceState::IDLE); dev.isbusy = 0; dev.busytimecount = 0; } } } } } } /////////////////////////////////////////////////////////////////////////////////// 一分钟调用一次:检查 qvvr_file 超时并备份 static bool ensure_dir_exists(const std::string &path) { struct stat st; if (stat(path.c_str(), &st) == 0) { if (S_ISDIR(st.st_mode)) { return true; // 已存在 } return false; // 存在但不是目录 } // 不存在则创建 if (mkdir(path.c_str(), 0755) == 0) { return true; } return false; } void check_and_backup_qvvr_files() { std::lock_guard lock(ledgermtx); const std::string data_dir = FRONT_PATH + "/data"; const std::string backup_dir = data_dir + "/comtrade_bak"; if (!ensure_dir_exists(data_dir) && !ensure_dir_exists(data_dir)) { std::cerr << "[check_and_backup_qvvr_files] 创建 data 目录失败\n"; return; } if (!ensure_dir_exists(backup_dir) && !ensure_dir_exists(backup_dir)) { std::cerr << "[check_and_backup_qvvr_files] 创建 comtrade_bak 目录失败\n"; return; } for (auto &dev : terminal_devlist) { for (auto &line : dev.line) { // 用迭代器遍历,允许在循环中 erase 当前元素 for (std::vector::iterator qit = line.qvvrevent.qvvrfile.begin(); qit != line.qvvrevent.qvvrfile.end(); /* no ++ here */) { qvvr_file &qfile = *qit; if (!qfile.used_status) { ++qit; continue; } ++qfile.file_time_count; // 每分钟+1 // 超时阈值:>10 分钟 if (qfile.file_time_count > 10) { std::cout << "[Qvvr Timeout] dev=" << dev.terminal_id << " -> move files to: " << backup_dir << std::endl; // 移动该记录内的所有文件 for (std::list::const_iterator it = qfile.file_download.begin(); it != qfile.file_download.end(); ++it) { const std::string &src = *it; const size_t pos = src.find_last_of("/\\"); const std::string base = (pos == std::string::npos) ? src : src.substr(pos + 1); const std::string dst = backup_dir + "/" + base; if (rename(src.c_str(), dst.c_str()) != 0) { std::cerr << " [ERROR] 移动失败: " << src << " -> " << dst << " , " << strerror(errno) << std::endl; } else { std::cout << " moved: " << src << " -> " << dst << std::endl; } } // ✅ 直接从 qvvrfile 向量中删除这条记录 qit = line.qvvrevent.qvvrfile.erase(qit); // 注意:不再对 qfile 读写,因为它已被删除 continue; // 不自增,由 erase 返回的新迭代器继续 } ++qit; // 正常前进 } } } } /////////////////////////////////////////////////////////////////////////////////////////定值信息发送接口函数 bool save_set_value(const std::string &dev_id, unsigned char mp_index, const std::vector &fabsf) { std::lock_guard lock(ledgermtx); // 1. 找到对应 terminal_dev auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev &dev) { return dev.terminal_id == dev_id; }); if (it == terminal_devlist.end()) { std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl; return false; } terminal_dev &dev = *it; // 2. 检查状态 if (dev.isbusy != 2 || dev.busytype != static_cast(DeviceState::READING_FIXEDVALUE)) { std::cerr << "[send_set_reply] device not in READING_FIXEDVALUE state." << std::endl; return false; } // 3. 遍历监测点,找到 logical_device_seq == mp_index 的监测点 bool found = false; for (auto &mon : dev.line) { if (std::atoi(mon.logical_device_seq.c_str()) == static_cast(mp_index)) { // ★ 清理原有的 set_values mon.set_values.clear(); // ★ 将 fabsf 依次存入该监测点的 set_values for (const auto &val : fabsf) { mon.set_values.push_back(val); } found = true; break; } } if (!found) { std::cerr << "[send_set_reply] monitor with seq=" << (int)mp_index << " not found in terminal " << dev_id << std::endl; return false; } // 4. 状态递减 dev.isbusy--; return true; } bool save_internal_value(const std::string &dev_id, const std::vector &fabsf) { // 找到对应 terminal_dev std::lock_guard lock(ledgermtx); auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev &dev) { return dev.terminal_id == dev_id; }); if (it == terminal_devlist.end()) { std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl; return false; } terminal_dev &dev = *it; // 判断 isbusy==3 且 busytype==READING_INTERFIXEDVALUE if (dev.isbusy != 3 || dev.busytype != static_cast(DeviceState::READING_INTERFIXEDVALUE)) { std::cerr << "[send_set_reply] device not in READING_INTERFIXEDVALUE state." << std::endl; return false; } // ★ 新增:清理原有的内部定值列表 dev.internal_values.clear(); //将值严格按顺序存入list中 for (const auto &val : fabsf) { dev.internal_values.push_back(val); } dev.isbusy--; return true; } bool save_internal_info(const std::string &dev_id, const std::vector &fixValueList) { // 找到对应 terminal_dev std::lock_guard lock(ledgermtx); auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev &dev) { return dev.terminal_id == dev_id; }); if (it == terminal_devlist.end()) { std::cerr << "[send_set_reply] device not found: " << dev_id << std::endl; return false; } terminal_dev &dev = *it; // 判断 isbusy==2 且 busytype==READING_INTERFIXEDVALUE if (dev.isbusy != 2 || dev.busytype != static_cast(DeviceState::READING_INTERFIXEDVALUE)) { std::cerr << "[send_set_reply] device not in READING_INTERFIXEDVALUE state." << std::endl; return false; } // ★ 新增:清理原有的内部定值列表 dev.dz_internal_info_list.clear(); //将值严格按顺序存入list中 for (const auto &info : fixValueList) { dev.dz_internal_info_list.push_back(info); } dev.isbusy--; return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////回复定值读取响应 bool send_set_value_reply(const std::string &dev_id, unsigned char mp_index, const std::vector &dz_info) { std::lock_guard lock(ledgermtx); // 1) 找终端 auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev &d) { return d.terminal_id == dev_id; }); if (it == terminal_devlist.end()) { std::cerr << "[send_set_value_reply] device not found: " << dev_id << std::endl; return false; } terminal_dev &dev = *it; // 2) 校验状态:发送“定值读取结果”回复,应处于 READING_FIXEDVALUE;isbusy == 1 if (dev.isbusy != 1 || dev.busytype != static_cast(DeviceState::READING_FIXEDVALUE)) { //定值读取 std::cerr << "[send_set_value_reply] device not in READING_FIXEDVALUE state." << std::endl; return false; } // 3) 定位监测点(mp_index ∈ [1,6]),与 logical_device_seq 比对 ledger_monitor *pMon = nullptr; for (auto &mon : dev.line) { if (std::atoi(mon.logical_device_seq.c_str()) == static_cast(mp_index)) { pMon = &mon; break; } } if (!pMon) { std::cerr << "[send_set_value_reply] monitor with seq=" << (int)mp_index << " not found in terminal " << dev_id << std::endl; return false; } //将dz_info存入监测点 pMon->dz_info_list.clear(); for (const auto &dz : dz_info) { pMon->dz_info_list.push_back(dz); } // 4) 取该监测点的 set_values,严格按顺序用于 DZ_Value std::vector ordered_vals; ordered_vals.reserve(pMon->set_values.size()); for (float v : pMon->set_values) ordered_vals.push_back(v); if (ordered_vals.empty()) { std::cerr << "[send_set_value_reply] monitor seq=" << (int)mp_index << " has empty set_values." << std::endl; return false; } // 5) 生成 JSON(结构严格贴合你给的样例) nlohmann::json j; // 顶层 j["guid"] = dev.guid; j["FrontId"] = FRONT_INST; // 你的前置机 IP(项目已有常量/变量) j["Node"] = g_front_seg_index; // 节点号(项目已有变量) j["Dev_mac"] = normalize_mac(dev.addr_str); // Detail nlohmann::json detail; detail["Type"] = 0x2106; // 设备数据 // Msg nlohmann::json msg; msg["Cldid"] = mp_index; //测点序号 msg["DataType"] = 0x0C; //定值 // DataArray(对象数组):逐个填充,DZ_Value 严格按 set_values 顺序 nlohmann::json dataArray = nlohmann::json::array(); const size_t n_meta = dz_info.size(); const size_t n_vals = ordered_vals.size(); const size_t n = std::min(n_meta, n_vals); // 以两者较短长度为准 if (n_meta != n_vals) { std::cerr << "[send_set_value_reply] warn: dz_info size(" << n_meta << ") != set_values size(" << n_vals << "), will emit " << n << " items.\n"; return false; // 或者继续发送,视需求而定 } for (size_t i = 0; i < n; ++i) { const DZ_TAB_STRUCT &dz = dz_info[i]; nlohmann::json item; item["LN_Num"] = dz.LN_Num; item["DZ_Num"] = dz.DZ_Num; item["DZ_Name"] = dz.DZ_Name; item["DZ_Value"] = ordered_vals[i]; // ★ 严格按顺序 item["DZ_Type"] = dz.DZ_Type; item["DZ_Min"] = dz.DZ_Min; item["DZ_Max"] = dz.DZ_Max; item["DZ_Default"]= dz.DZ_Default; item["DZ_UNIT"] = dz.DZ_UNIT; dataArray.push_back(std::move(item)); } msg["DataArray"] = std::move(dataArray); detail["Msg"] = std::move(msg); detail["Code"] = 200; j["Detail"] = std::move(detail); // 6) 入队发送 queue_data_t connect_info; connect_info.strTopic = Topic_Reply_Topic; connect_info.strText = j.dump(); // 序列化为字符串 connect_info.tag = Topic_Reply_Tag; connect_info.key = Topic_Reply_Key; { std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(std::move(connect_info)); } // 调试打印 std::cout << "[send_set_value_reply] queued JSON:\n" << j.dump(4) << std::endl; // 7) 发送后更新终端状态(按你现有规则) if (dev.isbusy > 0) { dev.isbusy--; } if (dev.isbusy == 0) { dev.guid.clear(); dev.busytype = 0; if (pMon) { pMon->set_values.clear();//清理本次定值记录 } } return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////回复内部定值响应读取 bool send_internal_value_reply(const std::string &dev_id, const std::vector &control_words) { std::lock_guard lock(ledgermtx); // 1) 找终端 auto it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev &d) { return d.terminal_id == dev_id; }); if (it == terminal_devlist.end()) { std::cerr << "[send_internal_value_reply] device not found: " << dev_id << std::endl; return false; } terminal_dev &dev = *it; // 2) 校验状态:发送“内部定值读取结果”回复,应处于 READING_INTERFIXEDVALUE;isbusy == 1 if (dev.isbusy != 1 || dev.busytype != static_cast(DeviceState::READING_INTERFIXEDVALUE)) { std::cerr << "[send_internal_value_reply] device not in READING_INTERFIXEDVALUE state." << std::endl; return false; } //将control_words存入dev dev.control_words.clear(); for (const auto &cw : control_words) { dev.control_words.push_back(cw); } // -------------------- [新增] 建立 internal_values 与 dz_internal_info_list 的一一对应 -------------------- // 说明:按索引次序一一对应(第 i 个 NameFixValue 对应 internal_values 的第 i 个) // 若数量不同,按 min 对齐,忽略多出来的一边并告警。 std::vector internal_vals; // [新增] internal_vals.reserve(dev.internal_values.size()); // [新增] for (float v : dev.internal_values) internal_vals.push_back(v); // [新增] const size_t n_dz = dev.dz_internal_info_list.size(); // [新增] const size_t n_val = internal_vals.size(); // [新增] const size_t n_use = std::min(n_dz, n_val); // [新增] if (n_dz != n_val) { // [新增] std::cerr << "[send_internal_value_reply] WARN: dz_internal_info_list size(" << n_dz << ") != internal_values size(" << n_val << "), will use min(" << n_use << ")." << std::endl; } // ------------------------------------------------------------------------------------------------------ // 3) 组包顶层 nlohmann::json j; j["guid"] = dev.guid; j["FrontId"] = FRONT_INST; j["Node"] = g_front_seg_index; j["Dev_mac"] = normalize_mac(dev.addr_str); nlohmann::json detail; detail["Type"] = 0x2106; // 设备数据 nlohmann::json msg; msg["DataType"] = 0x0D; // 内部定值 // 4) === 将 C# 的拼接逻辑移植为 DataArray === // C# 变量对应关系: // DevInfo.nDevIndex -> 这里用 1 // DevInfo.strGuId -> 这里用 装置id // DevInfo.controlwordlist -> 这里用参数 control_words(DZ_kzz_bit 含 kzz_bit/bit_enable) // // NameFixValue 列表:使用 dev.dz_internal_info_list // // 关键逻辑: // - 遍历每个 NameFixValue,k 从 1 递增,nStep 每个定值递增 1 // - 若 DataType == 1:将 Max/Min/Default 都 /100,并 property 输出一个空对象 [{}](保持与 C# 一致) // - 否则:为该定值构建 property 位数组,范围 [nStep*16, (nStep+1)*16), // 名称为空则提前结束本定值的 property;flag = (DefaultValue >> j) & 0x01 // nlohmann::json dataArray = nlohmann::json::array(); // [新增] int nStep = 0; // [新增] 每个 NameFixValue 递增 int kSort = 1; // [新增] 排序号,从 1 开始 // 保护:dz_internal_info_list 是引用成员,确保不会因并发被改动 //for (const auto& nf : dev.dz_internal_info_list) { // [新增] for (size_t idxNF = 0; idxNF < n_use; ++idxNF) { // [修改] 使用 idxNF 控制索引 const auto& nf = dev.dz_internal_info_list[idxNF]; // 取字段 const uint16_t dataType = nf.DataType; const uint16_t minRaw = nf.MinValue; const uint16_t maxRaw = nf.MaxValue; const uint16_t defaultRaw = nf.DefaultValue; const std::string unit = trim_cstr(nf.sDimension, sizeof(nf.sDimension)); const std::string name = trim_cstr(nf.sFixValueName, sizeof(nf.sFixValueName)); // 取对应内部值 const float internal_v_raw = internal_vals[idxNF]; // [新增] const double internal_v_out = static_cast(internal_v_raw); // [新增] 直接转 double 输出,不缩放 // 构造一条记录 nlohmann::json one; one["cpu_no"] = 1; // [新增] C#: DevInfo.nDevIndex 填设备号,固定为1 one["dev_type"] = dev_id; // [新增] C#: DevInfo.strGuId 填装置id one["type"] = 90; // [新增] 固定 "90" one["unit"] = unit; // [新增] one["describe"] = name; // [新增] one["sort"] = kSort; // [新增] one["Internal_Value"] = internal_v_out; // [新增] 精确对应 internal_values 的值(含必要缩放) // 数值:DataType == 1 时缩放 /100 if (dataType == 1) { // [新增] 缩放分支 int ChangeMaxValue = static_cast(maxRaw) / 100; int ChangeMinValue = static_cast(minRaw) / 100; int ChangeDefaultValue = static_cast(defaultRaw) / 100; one["maxvalue"] = ChangeMaxValue; one["minvalue"] = ChangeMinValue; one["defaultvalue"] = ChangeDefaultValue; one["value"] = ChangeDefaultValue; // C# 在该分支 property 写成 [{ }](一个空对象的数组) nlohmann::json prop = nlohmann::json::array(); prop.push_back(nlohmann::json::object()); // [{}] one["property"] = std::move(prop); } else { // [新增] 未缩放分支 + property 位描述 one["maxvalue"] = static_cast(maxRaw); one["minvalue"] = static_cast(minRaw); one["defaultvalue"] = static_cast(defaultRaw); one["value"] = static_cast(defaultRaw); // 构建 property:16 位窗口,从 nStep*16 到 (nStep+1)*16 - 1 nlohmann::json prop = nlohmann::json::array(); bool hasAny = false; const int begin = nStep * 16; const int end = (nStep + 1) * 16; // 不含 end for (int idx = begin, jbit = 0; idx < end; ++idx, ++jbit) { if (idx < 0 || static_cast(idx) >= control_words.size()) break; // 名称空则提前退出(仿 C#:temp=="" break) const std::string cw_name = trim_cstr(control_words[idx].kzz_bit, sizeof(control_words[idx].kzz_bit)); if (cw_name.empty()) { // 注意:C# 如果 j==0 则设置了 flag2=1,仅用于逗号处理,这里不需要 break; } int flag = (defaultRaw >> jbit) & 0x01; // 取该位默认值 nlohmann::json bitItem; bitItem["type_num"] = jbit; bitItem["bit0"] = ""; // 保持与 C# 一致 bitItem["bit1"] = ""; bitItem["describe"] = cw_name; bitItem["flag"] = (flag ? "1" : "0"); // C# 用字符串 prop.push_back(std::move(bitItem)); hasAny = true; } if (!hasAny) { // 与 C# 对齐:如果一个都没有,就给 [{}] 以避免 "property":[] 的结构差异 prop.push_back(nlohmann::json::object()); } one["property"] = std::move(prop); } dataArray.push_back(std::move(one)); // [新增] ++nStep; // [新增] 进入下一个 16 位窗口 ++kSort; // [新增] } msg["DataArray"] = std::move(dataArray); // [新增] detail["Msg"] = std::move(msg); j["Detail"] = std::move(detail); // 5) 入队发送(保持你的队列逻辑) queue_data_t connect_info; connect_info.strTopic = Topic_Reply_Topic; connect_info.strText = j.dump(); // 序列化为字符串 connect_info.tag = Topic_Reply_Tag; connect_info.key = Topic_Reply_Key; { std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(std::move(connect_info)); } // 调试打印 std::cout << "[send_internal_value_reply] queued JSON:\n" << j.dump(4) << std::endl; // 6) 发送后更新终端状态(保持你现有规则) if (dev.isbusy > 0) { dev.isbusy--; } if (dev.isbusy == 0) { dev.guid.clear(); dev.busytype = 0; dev.internal_values.clear(); // 清理本次定值记录 dev.dz_internal_info_list.clear(); // 清理本次定值描述记录(注意:如果这是引用成员,确保其实际容器存在) } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////处理补招逻辑 //发送补招响应给web void send_reply_to_kafka_recall(const std::string& guid, int dataType,int code, const std::string& result,const std::string& terminalId,const std::string& monitorId,const std::string& recallStartDate,const std::string& recallEndDate){ // 构造 JSON 字符串 std::ostringstream oss; oss << "{" << "\"guid\":\"" << guid << "\"," << "\"dataType\":\"" << dataType << "\"," << "\"code\":" << code << "," << "\"result\":\"" << result << "\"," << "\"terminalId\":\"" << terminalId << "\"," << "\"monitorId\":\"" << monitorId << "\"," << "\"recallStartDate\":\"" << recallStartDate << "\"," << "\"recallEndDate\":\"" << recallEndDate << "\"," << "\"processNo\":\"" << g_front_seg_index << "\"," << "\"nodeId\":\"" << FRONT_INST << "\"" << "}"; std::string jsonString = oss.str(); // 封装 Kafka 消息 queue_data_t connect_info; connect_info.strTopic = Topic_Reply_Topic; connect_info.strText = jsonString; connect_info.tag = "RECALL"; connect_info.key = Topic_Reply_Key; // 加入发送队列(带互斥锁保护) queue_data_list_mutex.lock(); queue_data_list.push_back(connect_info); queue_data_list_mutex.unlock(); } // ===== 一次遍历可下发“多个终端的一条” ===== void check_recall_event() { std::vector tasks; // 本轮要发送的“每终端一条” { //锁作用域 std::lock_guard lock(ledgermtx); // 遍历所有 terminal —— 每个 terminal 只挑一条,先不判断运行状态,因为正在处理其他事务的装置也可以记录待补招信息 for (auto& dev : terminal_devlist) { //如果该终端不是正在补招或者idle则直接跳过,节省运行时间 if(dev.busytype != static_cast(DeviceState::READING_EVENTLOG) && dev.busytype != static_cast(DeviceState::IDLE)){ std::cout << "[check_recall_event] skip dev=" << dev.terminal_id << std::endl; continue; } // 对正在补招或idle终端的所有监测点的待补招列表进行处理 // 1) 先弹掉首条为 DONE/FAILED/EMPTY 的记录(所有 monitor 都要处理首条) bool any_non_empty = false; for (auto& lm : dev.line) { // 标记这个测点在本轮开始时是否非空 bool had_items_before = !lm.recall_list.empty(); bool popped_any = false; // 本轮是否真的弹过 //std::string popped_guid_for_file; // 记录被弹出的条目的 guid(用于文件) while (!lm.recall_list.empty()) { const RecallMonitor& front = lm.recall_list.front(); // ===== 弹掉首条前:打印首条内容与列表大小 ===== std::cout << "[check_recall_event] before pop: list size=" << lm.recall_list.size() << " first=(" << front.StartTime << " ~ " << front.EndTime << ", status=" << front.recall_status << ")" << std::endl; if (front.recall_status == static_cast(RecallStatus::DONE)) { std::cout << "[check_recall_event] DONE dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " " << front.StartTime << " ~ " << front.EndTime << std::endl; //调用reply接口通知web端该时间段补招完成 std::string msg = std::string("监测点:") + lm.monitor_name + " 补招时间范围:" + front.StartTime + " ~ " + front.EndTime + " 补招执行完成"; //send_reply_to_kafka_recall(dev.guid,1,static_cast(ResponseCode::OK),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime); append_recall_record_line(dev.guid, lm.monitor_id, msg); lm.recall_list.pop_front(); // 弹掉首条 popped_any = true; // ===== 弹掉首条后:打印新首条与列表大小 ===== if (!lm.recall_list.empty()) { const RecallMonitor& newfront = lm.recall_list.front(); std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size() << " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime << ", status=" << newfront.recall_status << ")" << std::endl; } else { std::cout << "[check_recall_event success] after pop: list empty" << std::endl; } break; } else if (front.recall_status == static_cast(RecallStatus::EMPTY)) { std::cout << "[check_recall_event] EMPTY dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " " << front.StartTime << " ~ " << front.EndTime << std::endl; //调用reply接口通知web端该时间段补招失败 std::string msg = std::string("监测点:") + lm.monitor_name + " 补招时间范围:" + front.StartTime + " ~ " + front.EndTime + " 补招无事件日志"; //send_reply_to_kafka_recall(dev.guid,1,static_cast(ResponseCode::NOT_FOUND),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime); append_recall_record_line(dev.guid, lm.monitor_id, msg); lm.recall_list.pop_front(); // 弹掉首条 popped_any = true; // ===== 弹掉首条后:打印新首条与列表大小 ===== if (!lm.recall_list.empty()) { const RecallMonitor& newfront = lm.recall_list.front(); std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size() << " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime << ", status=" << newfront.recall_status << ")" << std::endl; } else { std::cout << "[check_recall_event empty] after pop: list empty" << std::endl; } break; } else if (front.recall_status == static_cast(RecallStatus::FAILED)) { std::cout << "[check_recall_event] FAILED dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " " << front.StartTime << " ~ " << front.EndTime << std::endl; //调用reply接口通知web端该时间段补招失败 std::string msg = std::string("监测点:") + lm.monitor_name + " 补招时间范围:" + front.StartTime + " ~ " + front.EndTime + " 补招执行失败"; //send_reply_to_kafka_recall(dev.guid,1,static_cast(ResponseCode::BAD_REQUEST),msg,dev.terminal_id,lm.monitor_id,front.StartTime,front.EndTime); append_recall_record_line(dev.guid, lm.monitor_id, msg); lm.recall_list.pop_front(); // 弹掉首条 popped_any = true; // ===== 弹掉首条后:打印新首条与列表大小 ===== if (!lm.recall_list.empty()) { const RecallMonitor& newfront = lm.recall_list.front(); std::cout << "[check_recall_event] after pop: list size=" << lm.recall_list.size() << " new first=(" << newfront.StartTime << " ~ " << newfront.EndTime << ", status=" << newfront.recall_status << ")" << std::endl; } else { std::cout << "[check_recall_event fail] after pop: list empty" << std::endl; } break; } else {//如果每个测点都有补招记录,但是首条不是 DONE/FAILED/EMPTY 则跳过该测点,检查下一个测点 std::cout << "[check_recall_event] skip line=" << lm.monitor_name<< std::endl; break; // 首条不是 2/3/4,停,如果是正在处理其他业务或者idle的装置写入了待补招列表,应该都是0;如果是正在补招的装置,新增的部分不会影响原有顺序 } } if (had_items_before && popped_any && lm.recall_list.empty()) {//处理后,当前测点补招记录为空 //通知补招全部完成 std::cout << "[check_recall_event] finish recall monitor=" << lm.monitor_id << std::endl; //读取记录文件获取响应参数 std::string file_guid, terminalId, file_monitorId, startTime, endTime, msg; if (get_recall_record_fields_by_guid_monitor(dev.guid, lm.monitor_id, file_guid, terminalId, file_monitorId, startTime, endTime, msg)) { // 校验通过,拿到参数了 std::cout << "guid=" << file_guid << std::endl; std::cout << "terminalId=" << terminalId << std::endl; std::cout << "monitor_id=" << file_monitorId << std::endl; std::cout << "start=" << startTime << std::endl; std::cout << "end=" << endTime << std::endl; std::cout << "msg:\n" << msg << std::endl; send_reply_to_kafka_recall(file_guid, 1, static_cast(ResponseCode::OK), msg, terminalId, file_monitorId, startTime, endTime); } delete_recall_record_file(dev.guid, lm.monitor_id); } //处理后,当前测点补招记录非空 if (!lm.recall_list.empty()) { any_non_empty = true; // 处理了成功/失败/无消息以后该装置只要有任一测点有一条记录就标记,可能是待处理或者正在处理的补招 } } // pop后判断是否仍有 RUNNING(pop后应该都为unstarted,没pop的才会是running) bool has_running = false; for (auto& lm : dev.line) { if (!lm.recall_list.empty() && lm.recall_list.front().recall_status == static_cast(RecallStatus::RUNNING)) { has_running = true; break; } } if (!has_running && dev.busytype == static_cast(DeviceState::READING_EVENTLOG)) { dev.busytimecount = 0; // 避免“上一条刚结束但下一条尚未起”期间被误判超时 } // 有条目但目前存在 RUNNING:继续等待该 RUNNING 完成,当前装置本轮不新发 if (any_non_empty && has_running) { std::cout << "[check_recall_event] skip dev=" << dev.terminal_id << " already running recall" << std::endl; continue; } //pop后无任何补招条目,且处于补招状态:清空运行态 if (!any_non_empty && dev.busytype == static_cast(DeviceState::READING_EVENTLOG)) { // 该终端本轮已无任何补招条目,且处于补招暂态事件的状态清空运行态 std::cout << "[check_recall_event] finish recall dev=" << dev.terminal_id << std::endl; dev.guid.clear(); // 清空 guid dev.busytype = static_cast(DeviceState::IDLE); // 业务类型归零 dev.isbusy = 0; // 清空业务标志 dev.busytimecount = 0; // 计时归零 continue; //处理完处理下一个装置 } //有待补招任务,且idle或者在补招,继续补招处理 //std::cout << "[check_recall_event] idle or continue recall dev=" << dev.terminal_id << std::endl; // 若无 RUNNING,则说明该终端空闲,可以挑选新的补招任务 if (any_non_empty && !has_running) { // 3) 选择该终端的“第一条 NOT_STARTED(0)”作为本终端本轮任务 bool picked = false; for (auto& lm : dev.line) { if (lm.recall_list.empty()) { std::cout << "[check_recall_event] skip empty line=" << lm.monitor_name<< std::endl; continue; //跳过空的监测点 } RecallMonitor& front = lm.recall_list.front(); //取非空测点的列表的第一条 if (front.recall_status == static_cast(RecallStatus::NOT_STARTED)) {//未补招 // 标记为 RUNNING,并设置终端忙状态 front.recall_status = static_cast(RecallStatus::RUNNING);//该补招记录刷新为补招中 dev.guid = front.recall_guid; // 记录本次补招的 guid dev.isbusy = 1; //标记为忙 dev.busytype = static_cast(DeviceState::READING_EVENTLOG);//装置状态正在补招和idle的都刷新为正在补招 dev.busytimecount = 0; //刷新业务超时计数 // 记录任务(每终端只取这一条,多个装置可以同时进行) tasks.push_back(RecallTask{ dev.terminal_id, front.StartTime, front.EndTime, lm.logical_device_seq//记录测点号 }); picked = true; //该装置已取 break; } } // 若该终端没有 NOT_STARTED 的首条(可能剩下的都是 RUNNING 或内部已经被清空), if (!picked) { // 没挑到 NOT_STARTED(例如都是“首条非空但非 NOT_STARTED/非 RUNNING”的情况): // 这种情况下本终端留给下一轮处理即可。 std::cout << "[check_recall_event] skip dev=" << dev.terminal_id << " no NOT_STARTED at head" << std::endl; } // 就留待下一轮;不要清空运行态,直到所有条目被处理为止。 continue; } } } // 解锁 // 对于本轮挑选到的任务,逐终端下发一条,这些终端的状态都已设为补招 for (const auto& t : tasks) { //处理入参 std::tm tm1{}, tm2{}; if (!parse_datetime_tm(t.start_time, tm1) || !parse_datetime_tm(t.end_time, tm2)) { std::cout << "[check_recall_event] parse time fail: " << t.start_time << " ~ " << t.end_time << std::endl; continue; } uint8_t mp = 1; // 默认 try { mp = static_cast(std::stoi(t.monitor_index)); } catch (...) { std::cout << "[check_recall_event] parse mpid fail: " << t.monitor_index << std::endl; continue; } // 下发补招请求,action=2 ClientManager::instance().read_eventlog_action_to_device( t.dev_id, tm1, tm2, 2, mp);//2是暂态事件 std::cout << "[check_recall_event] SEND dev=" << t.dev_id << " monitor=" << static_cast(mp) << " start=" << t.start_time << " end=" << t.end_time << " action(2)" << std::endl; } // 重要:本函数不把 RUNNING 改成 DONE/FAILED; // 应由设备回调/结果处理逻辑在完成后调用通知函数,把对应 monitor.front().recall_status 从1置为 2/3, // 下一轮本函数会弹掉 2/3,并在该终端所有 recall_list 全部清空后重置装置状态。 } //////////////////////////////////////////////////////////////////////////////////////////////////////////////处理补招逻辑统计数据 // ====== 从文件名中提取“第二段下划线分隔字段”并转换为 epoch 秒 ====== static bool extract_epoch_from_filename(const std::string& name, long long& out_epoch, int logical_device_seq) { // 拆分 std::vector parts; parts.reserve(8); size_t start = 0, pos; while ((pos = name.find('_', start)) != std::string::npos) { parts.emplace_back(name.substr(start, pos - start)); start = pos + 1; } parts.emplace_back(name.substr(start)); // 最后一段(含扩展名) if (parts.size() < 4) return false; // 第二段序号是倒数第4段 const std::string& seq_str = parts[parts.size() - 4]; // 允许前导 0:把字符串转 int 后比较 for (char c : seq_str) if (!std::isdigit(static_cast(c))) return false; int seq_val = 0; try { seq_val = std::stoi(seq_str); } catch (...) { return false; } if (seq_val != logical_device_seq) return false; // 其余与上面相同 const std::string& date_str = parts[parts.size() - 3]; const std::string& time_str = parts[parts.size() - 2]; std::string ms_str = parts.back(); size_t dot = ms_str.find('.'); if (dot != std::string::npos) { ms_str.erase(dot); } if (date_str.size() != 8 || time_str.size() != 6) return false; for (char c : date_str) if (!std::isdigit(static_cast(c))) return false; for (char c : time_str) if (!std::isdigit(static_cast(c))) return false; for (char c : ms_str) if (!std::isdigit(static_cast(c))) return false; int year = std::stoi(date_str.substr(0, 4)); int month = std::stoi(date_str.substr(4, 2)); int day = std::stoi(date_str.substr(6, 2)); int hour = std::stoi(time_str.substr(0, 2)); int min = std::stoi(time_str.substr(2, 2)); int sec = std::stoi(time_str.substr(4, 2)); // int msec = std::stoi(ms_str); std::tm tm{}; tm.tm_isdst = -1; tm.tm_year = year - 1900; tm.tm_mon = month - 1; tm.tm_mday = day; tm.tm_hour = hour; tm.tm_min = min; tm.tm_sec = sec; time_t t = timegm(&tm); if (t < 0) return false; out_epoch = static_cast(t); // 秒级 return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 从文件名中解析出 "监测点号_YYYYMMDD_HHMMSS_mmm";成功返回 true static bool make_target_key_from_filename(const std::string& fname, std::string& out_key) { // 例:PQMonitor_PQM1_000005_20250909_074657_246.dat // 例:PQ_PQLD1_000005_20250909_074656_435.dat // 以 '_' 切分,应至少 6 段 std::vector parts; parts.reserve(8); size_t start = 0, pos; while ((pos = fname.find('_', start)) != std::string::npos) { parts.emplace_back(fname.substr(start, pos - start)); start = pos + 1; } parts.emplace_back(fname.substr(start)); if (parts.size() < 6) return false; // 索引意义: // [0]=前缀(如 PQMonitor / PQ) // [1]=监测点号(如 PQM1 / PQLD1) // [2]=序号(如 000005) // [3]=YYYYMMDD // [4]=HHMMSS // [5]=mmm(.dat) const std::string& monitor = parts[1]; const std::string& ymd = parts[3]; const std::string& hms = parts[4]; // 去掉末段的扩展名(如 "246.dat" -> "246") std::string mmm = parts[5]; size_t dot = mmm.rfind('.'); if (dot != std::string::npos) mmm.erase(dot); // 基本合法性校验(长度与全数字) auto all_digits = [](const std::string& s) { return !s.empty() && std::find_if(s.begin(), s.end(), [](unsigned char c){ return !std::isdigit(c); }) == s.end(); }; if (ymd.size() != 8 || !all_digits(ymd)) return false; if (hms.size() != 6 || !all_digits(hms)) return false; if (mmm.size() != 3 || !all_digits(mmm)) return false; if (monitor.empty()) return false; // 目标 key:监测点号_YYYYMMDD_HHMMSS_mmm //out_key.reserve(monitor.size() + 1 + 8 + 1 + 6 + 1 + 3); out_key.reserve(monitor.size() + 1 + 8 + 1 + 6); out_key.clear(); //out_key.append(monitor).append("_").append(ymd).append("_").append(hms).append("_").append(mmm); out_key.append(monitor).append("_").append(ymd).append("_").append(hms); return true; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ====== ★修改:check_recall_stat —— 加入“两步法”状态机 ====== void check_recall_file() { std::vector tasks; // 本轮要发送的“每终端一条”(目录请求 或 文件下载 请求) { //锁作用域 std::lock_guard lock(ledgermtx); for (auto& dev : terminal_devlist) { // 仅处理“正在补招/空闲”终端,与你原逻辑一致 if (dev.busytype != static_cast(DeviceState::READING_STATSFILE) && dev.busytype != static_cast(DeviceState::IDLE)) { continue; } // 1) 清理首条 DONE/FAILED bool any_non_empty = false; for (auto& lm : dev.line) { // 标记这个测点在本轮开始时是否非空 bool had_items_before = !lm.recall_list_static.empty(); bool popped_any = false; // 本轮是否真的弹过 while (!lm.recall_list_static.empty()) { const RecallFile& front = lm.recall_list_static.front(); // ===== 弹掉首条前:打印首条内容与列表大小 ===== std::cout << "[check_recall_file] before pop | dev=" << dev.terminal_id << " mon=" << lm.monitor_id << " size=" << lm.recall_list_static.size() << " first{guid=" << front.recall_guid << ", status=" << front.recall_status << ", direct=" << (front.direct_mode ? 1 : 0) << ", phase=" << static_cast(front.phase) << ", cur_dir=" << front.cur_dir << ", cur_file=" << front.downloading_file << ", list_result=" << static_cast(front.list_result) << ", download_result=" << static_cast(front.download_result) << ", download_queue=" << front.download_queue.size() << "}" << std::endl; // ===== 打印 download_queue 内的全部文件 ===== if (!front.download_queue.empty()) { std::cout << " [download_queue files] (" << front.download_queue.size() << "):" << std::endl; int idx = 0; for (const auto& path : front.download_queue) { std::cout << " [" << idx++ << "] " << path << std::endl; } } else { std::cout << " [download_queue files] (empty)" << std::endl; } if (front.recall_status == static_cast(RecallStatus::DONE)) { std::cout << "[check_recall_stat] DONE dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << std::endl; lm.recall_list_static.pop_front(); //弹出后再打印首条内容与列表大小 if (!lm.recall_list_static.empty()) { const RecallFile& nf = lm.recall_list_static.front(); std::cout << "[check_recall_file] after pop | dev=" << dev.terminal_id << " mon=" << lm.monitor_id << " size=" << lm.recall_list_static.size() << " first{guid=" << nf.recall_guid << ", status=" << nf.recall_status << ", direct=" << (nf.direct_mode ? 1 : 0) << ", phase=" << static_cast(nf.phase) << ", cur_dir=" << nf.cur_dir << ", cur_file=" << nf.downloading_file << ", list_result=" << static_cast(nf.list_result) << ", download_result=" << static_cast(nf.download_result) << ", download_queue=" << nf.download_queue.size() << "}" << std::endl; // ===== 打印 download_queue 内的全部文件 ===== if (!nf.download_queue.empty()) { std::cout << " [download_queue files] (" << nf.download_queue.size() << "):" << std::endl; int idx = 0; for (const auto& path : nf.download_queue) { std::cout << " [" << idx++ << "] " << path << std::endl; } } else { std::cout << " [download_queue files] (empty)" << std::endl; } }else { std::cout << "[check_recall_file] after pop | list empty" << std::endl; } popped_any = true; break; } else if (front.recall_status == static_cast(RecallStatus::FAILED)) { std::cout << "[check_recall_stat] FAILED dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " " << front.StartTime << " ~ " << front.EndTime << std::endl; lm.recall_list_static.pop_front(); //弹出后再打印首条内容与列表大小 if (!lm.recall_list_static.empty()) { const RecallFile& nf = lm.recall_list_static.front(); std::cout << "[check_recall_file] after pop | dev=" << dev.terminal_id << " mon=" << lm.monitor_id << " size=" << lm.recall_list_static.size() << " first{guid=" << nf.recall_guid << ", status=" << nf.recall_status << ", direct=" << (nf.direct_mode ? 1 : 0) << ", phase=" << static_cast(nf.phase) << ", cur_dir=" << nf.cur_dir << ", cur_file=" << nf.downloading_file << ", list_result=" << static_cast(nf.list_result) << ", download_result=" << static_cast(nf.download_result) << ", download_queue=" << nf.download_queue.size() << "}" << std::endl; // ===== 打印 download_queue 内的全部文件 ===== if (!nf.download_queue.empty()) { std::cout << " [download_queue files] (" << nf.download_queue.size() << "):" << std::endl; int idx = 0; for (const auto& path : nf.download_queue) { std::cout << " [" << idx++ << "] " << path << std::endl; } } else { std::cout << " [download_queue files] (empty)" << std::endl; } } else { std::cout << "[check_recall_file] after pop | list empty" << std::endl; } popped_any = true; break; } else { std::cout << "[check_recall_file] skip line=" << lm.monitor_name<< std::endl; break;//找到第一条不是成功或失败的记录就退出循环 } } if (had_items_before && popped_any && lm.recall_list_static.empty()) {//处理后,当前测点补招记录为空 //通知补招全部完成 std::cout << "[check_recall_file] finish recall monitor=" << lm.monitor_id << std::endl; //读取记录文件获取响应参数 std::string file_guid, terminalId, file_monitorId, startTime, endTime, msg; if (get_recall_record_fields_by_guid_monitor(dev.guid, lm.monitor_id, file_guid, terminalId, file_monitorId, startTime, endTime, msg)) { // 校验通过,拿到参数了 std::cout << "guid=" << file_guid << std::endl; std::cout << "terminalId=" << terminalId << std::endl; std::cout << "monitor_id=" << file_monitorId << std::endl; std::cout << "start=" << startTime << std::endl; std::cout << "end=" << endTime << std::endl; std::cout << "msg:\n" << msg << std::endl; send_reply_to_kafka_recall(file_guid, 1, static_cast(ResponseCode::OK), msg, terminalId, file_monitorId, startTime, endTime); } delete_recall_record_file(dev.guid, lm.monitor_id); } //处理后,当前测点补招记录非空 if (!lm.recall_list_static.empty()) { any_non_empty = true; // 处理了成功/失败/无消息以后该装置只要有任一测点有一条记录就标记,可能是待处理或者正在处理的补招 } } if (!any_non_empty && dev.busytype == static_cast(DeviceState::READING_STATSFILE)) { std::cout << "[check_recall_file] finish recall dev=" << dev.terminal_id << std::endl; dev.guid.clear(); dev.busytype = static_cast(DeviceState::IDLE); dev.isbusy = 0; dev.busytimecount = 0; continue;//这个装置处理下一个装置 } // pop后判断是否仍有 RUNNING(pop后应该都为unstarted,没pop的才会是running) bool has_running = false; for (auto& lm : dev.line) { if (!lm.recall_list_static.empty() && lm.recall_list_static.front().recall_status == static_cast(RecallStatus::RUNNING)) { has_running = true; break; } } if (!has_running && dev.busytype == static_cast(DeviceState::READING_STATSFILE)) dev.busytimecount = 0; // ★新增:当存在 RUNNING 时,推进“该终端的首条补招记录”的两步状态机 if (has_running) { for (auto& lm : dev.line) { if (lm.recall_list_static.empty()) continue;//跳过没有记录的测点 RecallFile& front = lm.recall_list_static.front();//有记录测点的第一个补招 if (front.recall_status != static_cast(RecallStatus::RUNNING)) continue;//跳过不是正在补招的记录 // 初始化阶段:补招分为两个阶段,读文件列表和下载文件,如果是刚进入 RUNNING 状态则初始化 if (front.phase == RecallPhase::IDLE) { //暂态补招 front.phase = RecallPhase::LISTING; front.cur_dir_index = 0;//初始化查询的目录下标 front.cur_dir.clear();//初始化查询的目录名 front.list_result = ActionResult::PENDING; //目录请求结果置为待定 front.download_result = ActionResult::PENDING;//下载结果置为待定 front.download_queue.clear();//初始化下载文件队列 front.required_files.clear(); front.file_success.clear(); // 立即发起第一个目录请求 if (front.cur_dir_index < static_cast(front.dir_candidates.size())) { front.cur_dir = front.dir_candidates[front.cur_dir_index];//从第一个目录开始 // ★★ 只发一个目录请求,并等待外部线程回写结果与文件名列表 ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir); std::cout << "[check_recall_stat] LIST req dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << std::endl; } else { // 无目录可查 front.recall_status = static_cast(RecallStatus::FAILED); std::cout << "[check_recall_stat] no dir candidates, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << std::endl; } // 一个终端只推进首条,跳出 break; } // LISTING 阶段:等待其他线程回写 list_result + dir_files[cur_dir] if (front.phase == RecallPhase::LISTING) { if (front.list_result == ActionResult::PENDING) { // 还在等目录回执,本轮不再发任何请求 // (外部线程拿到目录文件列表后,应写入:front.dir_files[front.cur_dir] = {a,b,c...} 并置 list_result=OK/FAIL) break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录 } if (front.list_result == ActionResult::FAIL) { // 尝试下一个目录 front.cur_dir_index++; front.list_result = ActionResult::PENDING;//重置状态 front.download_queue.clear();//重置下载队列 front.required_files.clear(); front.file_success.clear(); if (front.cur_dir_index < static_cast(front.dir_candidates.size())) { front.cur_dir = front.dir_candidates[front.cur_dir_index]; ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir); std::cout << "[check_recall_stat] LIST retry dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << std::endl; } else { // 所有目录都失败 front.recall_status = static_cast(RecallStatus::FAILED); std::cout << "[check_recall_stat] all dir failed, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << std::endl; } break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出 } // OK:根据起止时间筛选文件 { /*//这部分用于稳态数据文件 long long beg = parse_time_to_epoch(front.StartTime); long long end = parse_time_to_epoch(front.EndTime); //错误判断:如果是直下文件的方式,会给默认的正确的时间范围 if (beg < 0 || end < 0 || beg > end) { front.recall_status = static_cast(RecallStatus::FAILED); std::cout << "[check_recall_stat] time parse ERR, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " start=" << front.StartTime << " end=" << front.EndTime << std::endl; break;//跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出 }*/ //装置消息返回后通知成功的处理: auto it = front.dir_files.find(front.cur_dir);//在map中查找当前目录名对应的目录下的文件名列表 if (it != front.dir_files.end()) { if (front.direct_mode) { // 目标时间戳(不含毫秒、形如 yyyyMMdd_HHmmss) const std::string& want_ts = front.target_filetimes; for (const auto& ent : it->second) { // 打印目录下的所有条目 std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << " file=" << ent.name << " flag=" << ent.flag << std::endl; if (ent.flag == 0) continue; // 只要文件,跳过目录 size_t n = ::strnlen(ent.name, sizeof(ent.name)); std::string fname(ent.name, n); // 解析出 key = 监测点号_YYYYMMDD_HHMMSS(注意:确保你的 make_target_key_from_filename // 已经改成“去掉毫秒”的版本;若还带毫秒,请先调整该函数) std::string key; if (!make_target_key_from_filename(fname, key)) { std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << " file=" << fname << " key=" << key << " ERR: invalid filename format, skip" << std::endl; continue; } // key 形如 MON_YYYYMMDD_HHMMSS,目标是只按时间戳匹配: if (has_suffix(key, want_ts)) { //打印放入的文件名 std::cout << "[check_recall_file] dir file dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << " file=" << fname << " key=" << key << " MATCH, add to download queue" << std::endl; front.download_queue.push_back(front.cur_dir + "/" + fname); } } } else { //稳态文件 // ☆原有:按时间窗筛选 long long beg = parse_time_to_epoch(front.StartTime); long long end = parse_time_to_epoch(front.EndTime); for (const auto& ent : it->second) { if (ent.flag != 1) continue; // 只要文件 // 文件名 size_t n = ::strnlen(ent.name, sizeof(ent.name)); std::string fname(ent.name, n); long long fe = -1; int seq = 0; try { seq = std::stoi(lm.logical_device_seq); } catch (...) { seq = 0; } if (!extract_epoch_from_filename(fname, fe, seq)) continue; if (fe >= beg && fe <= end) { front.download_queue.push_back(front.cur_dir + "/" + fname); } } } } } if (front.download_queue.empty()) { // 当前目录无匹配文件 -> 试下一个目录 front.cur_dir_index++; front.list_result = ActionResult::PENDING; if (front.cur_dir_index < static_cast(front.dir_candidates.size())) { front.cur_dir = front.dir_candidates[front.cur_dir_index]; ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir); std::cout << "[check_recall_stat] LIST next dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << std::endl; } else { // 所有目录都“无匹配文件” front.recall_status = static_cast(RecallStatus::FAILED); std::string msg_fail; if (front.direct_mode) { msg_fail = std::string("监测点:") + lm.monitor_name + " 补招波形文件未找到,目标时标:" + front.target_filetimes; } else { msg_fail = std::string("监测点:") + lm.monitor_name + " 补招波形文件未找到,时间范围:" + front.StartTime + " ~ " + front.EndTime; } append_recall_record_line(dev.guid, lm.monitor_id, msg_fail); std::cout << "[check_recall_stat] no matched files in ALL dirs, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << std::endl; } break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录;如果失败,下个循环会弹出 } else { // 进入下载阶段 front.required_files.clear(); for (const auto& p : front.download_queue) front.required_files.insert(p); front.file_success.clear(); front.phase = RecallPhase::DOWNLOADING; front.download_result = ActionResult::PENDING; front.downloading_file.clear(); std::cout << "[check_recall_stat] enter DOWNLOADING dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " count=" << front.download_queue.size() << std::endl; } } //在上面的处理中已经找到要下载的文件列表,进入下载阶段 // DOWNLOADING 阶段:一次只下一个文件,等待外部线程填充 download_result if (front.phase == RecallPhase::DOWNLOADING) { if (front.downloading_file.empty()) {//当前无正在下载文件 if (front.download_queue.empty()) {//下载队列无文件 // 所有文件下载完毕 std::cout << "dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " ok=" << front.file_success.size() << " total=" << front.required_files.size() << std::endl; if (!front.required_files.empty() && front.file_success.size() == front.required_files.size()) { front.recall_status = static_cast(RecallStatus::DONE); std::cout << "[check_recall_stat] DONE dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " ok=" << front.file_success.size() << " total=" << front.required_files.size() << std::endl; } else { front.recall_status = static_cast(RecallStatus::FAILED); std::cout << "[check_recall_stat] some files failed, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " ok=" << front.file_success.size() << " need=" << front.required_files.size() << std::endl; } break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录 } // 发起下一文件下载 front.downloading_file = front.download_queue.front(); front.download_queue.pop_front();//弹出队首 front.download_result = ActionResult::PENDING; // ★★ 只发一个下载请求,等待外部线程回写结果 ClientManager::instance().add_file_download_action_to_device(dev.terminal_id, front.downloading_file); std::cout << "[check_recall_stat] DL req dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " file=" << front.downloading_file << std::endl; break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录 } else { // 等待 download_result if (front.download_result == ActionResult::PENDING) { break; // 仍在等待 } if (front.download_result == ActionResult::OK) { front.file_success.insert(front.downloading_file); std::string msg_ok = std::string("监测点:") + lm.monitor_name + " 补招波形文件:" + front.downloading_file + " 执行完成"; append_recall_record_line(dev.guid, lm.monitor_id, msg_ok); std::cout << "[check_recall_stat] DL ok dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " file=" << front.downloading_file << std::endl; // 清空当前文件标志,进入下一轮取队首 front.downloading_file.clear(); front.download_result = ActionResult::PENDING; break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录 } else { std::string msg_fail = std::string("监测点:") + lm.monitor_name + " 补招波形文件:" + front.downloading_file + " 执行失败"; append_recall_record_line(dev.guid, lm.monitor_id, msg_fail); // 失败:直接尝试下一个文件(不中断整条补招) std::cout << "[check_recall_stat] DL fail dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " file=" << front.downloading_file << std::endl; front.downloading_file.clear(); front.download_result = ActionResult::PENDING; break; //跳出循环,一个装置一次只能处理一个测点的一个补招记录 } } } // 一个终端只推进“一个监测点的首条”状态机,避免并行 break; } // 本终端已有 RUNNING 项,且已推进;不再挑选新的 NOT_STARTED,开始下一个终端的处理 continue; } // 3) 没有 RUNNING:挑选第一条 NOT_STARTED,并发起“首个目录”的请求 for (auto& lm : dev.line) { if (lm.recall_list_static.empty()) continue; //跳过补招列表为空的测点 RecallFile& front = lm.recall_list_static.front(); //取测点第一条记录 if (front.recall_status == static_cast(RecallStatus::NOT_STARTED)) { //补招未开始 // 标记为 RUNNING,并设置终端忙状态 front.recall_status = static_cast(RecallStatus::RUNNING); //该补招记录刷新为补招中 dev.isbusy = 1; //装置由idle标记为忙 dev.busytype = static_cast(DeviceState::READING_STATSFILE);//装置状态刷新为正在补招文件 dev.busytimecount = 0; //清空业务超时计数 dev.guid = front.recall_guid; // 初始化状态机并发出第一个目录请求 front.reset_runtime(true);//保留直下文件信息 front.phase = RecallPhase::LISTING; //正在请求并等待“目录文件名列表” if (!front.dir_candidates.empty()) {//目录列表非空 front.cur_dir_index = 0; //正在尝试的目录下标 front.cur_dir = front.dir_candidates[0]; //第一个目录 front.list_result = ActionResult::PENDING; //目录状态:等待返回 ClientManager::instance().add_file_menu_action_to_device(dev.terminal_id, front.cur_dir);//调用目录请求接口 std::cout << "[check_recall_stat] LIST start dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << " dir=" << front.cur_dir << " start=" << front.StartTime << " end=" << front.EndTime << std::endl; } else { front.recall_status = static_cast(RecallStatus::FAILED); //目录列表空,失败 std::cout << "[check_recall_stat] empty dir_candidates, FAIL dev=" << dev.terminal_id << " monitor=" << lm.monitor_id << std::endl; } // 每终端本轮仅取一条 break; } } } // end for dev } // 解锁 // ★说明:本函数不主动把 RUNNING 改 DONE/FAILED; // 由“其他线程”在目录/下载结果返回后,找到对应 dev/monitor 的 lm.recall_list_static.front(), // 写入: // - 目录请求:front.dir_files[front.cur_dir] = {...}; front.list_result = OK/FAIL; // - 文件下载:front.download_result = OK/FAIL; // 下一轮本函数会推进状态机(继续本目录的文件或切到下一个目录)。 } ///////////////////////////////////////////////////////////////////////////////////////////////////////////根据下发的指令直接补招文件 // ★新增:直下文件的任务入队(终端、测点、文件名) // 约定:外部线程的回调,照旧把目录返回值写到 lm.recall_list_static.front().dir_files[dir], // 并置 front.list_result=OK/FAIL;下载回执置 front.download_result=OK/FAIL。 // 处理指令部分将文件名拼接出来调用这个函数 bool enqueue_direct_download(const std::string& dev_id, const std::string& monitor_id, const std::string& filetime, const std::string& guid) { std::lock_guard lk(ledgermtx); // 找终端 auto dev_it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d){ return d.terminal_id == dev_id; }); if (dev_it == terminal_devlist.end()) return false; // 找监测点 auto lm_it = std::find_if(dev_it->line.begin(), dev_it->line.end(), [&](const ledger_monitor& lm){ return lm.monitor_id == monitor_id; }); if (lm_it == dev_it->line.end()) return false; // 组装一条 RecallFile RecallFile rf; rf.recall_guid = guid; rf.recall_status = static_cast(RecallStatus::NOT_STARTED); rf.StartTime = "1970-01-01 00:00:00"; // 仅占位,直下文件不会用到时间窗 rf.EndTime = "1970-01-01 00:00:01"; //rf.dir_candidates = dir_candidates; // 要检索的目录列表和默认的一致 rf.direct_mode = true; // ★关键:直下文件 rf.target_filetimes=filetime; // ▲单个文件时间入“列表” lm_it->recall_list_static.push_back(std::move(rf)); // 若设备空闲,可直接置忙(可选,视你的流程而定) if (dev_it->busytype == static_cast(DeviceState::IDLE)) { dev_it->isbusy = 1; dev_it->busytype = static_cast(DeviceState::READING_STATSFILE); // 或 READING_EVENTFILE dev_it->busytimecount = 0; } return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////通讯响应处理 void filemenu_cache_put(const std::string& dev_id, std::vector FileList) { std::lock_guard lk(g_filemenu_cache_mtx); g_filemenu_cache[dev_id] = std::move(FileList); // 直接存 vector } bool filemenu_cache_take(const std::string& dev_id, std::vector& out) { std::lock_guard lk(g_filemenu_cache_mtx); auto it = g_filemenu_cache.find(dev_id); if (it == g_filemenu_cache.end()) return false; out = std::move(it->second); // 转移 vector g_filemenu_cache.erase(it); // 删除缓存 return true; } // 提取文件名列表(仅 flag==1) static inline void build_file_name_list(const std::vector& in, std::list& out) { out.clear(); for (const auto& e : in) { if (e.flag == 1) { // 安全地从固定长度 char[64] 转成 std::string size_t n = ::strnlen(e.name, sizeof(e.name)); out.emplace_back(std::string(e.name, n)); } } } void on_device_response_minimal(int response_code, const std::string& id, unsigned char cid, int device_state_int) { const bool ok = is_ok(response_code); DeviceState state = static_cast(device_state_int); switch (state) { // ================= 特殊:补招事件日志 ================= case DeviceState::READING_EVENTLOG: { std::lock_guard lk(ledgermtx); terminal_dev* dev = nullptr; for (auto& d : terminal_devlist) { if (d.terminal_id == id) { dev = &d; break; } } if (!dev) { std::cout << "[RESP][EVENTLOG] dev not found, id=" << id << " rc=" << response_code << std::endl; return; } // 找到“首条 RUNNING”的补招条目,按 rc==200 成功/失败标记 bool updated = false; // 1) 找到 RecallStatus::RUNNING 的 monitor(优先) // 若没有,则回退到按 cid -> logical_device_seq 匹配 ledger_monitor* matched_monitor = nullptr; //优先:遍历查找首条补招项处于 RUNNING 的监测点 for (auto& lm : dev->line) { if (!lm.recall_list.empty()) { const RecallMonitor& head = lm.recall_list.front(); if (head.recall_status == static_cast(RecallStatus::RUNNING)) { matched_monitor = &lm; break; } } } //未找到 RUNNING 的监测点时,按原逻辑用 cid 匹配 logical_device_seq if (!matched_monitor) { const std::string cid_str = std::to_string(static_cast(cid)); for (auto& lm : dev->line) { if (lm.logical_device_seq == cid_str) { matched_monitor = &lm; break; } } } if (!matched_monitor) { // 没有匹配的监测点 std::cout << "[RESP][EVENTLOG][WARN] dev=" << id << " cannot find monitor (no RUNNING item, no cid match), cid=" << static_cast(cid) << std::endl; } else { // 2) 仅更新该监测点 recall_list 的首条,且要求处于 RUNNING if (!matched_monitor->recall_list.empty()) { RecallMonitor& front = matched_monitor->recall_list.front(); //取出首条 if (front.recall_status == static_cast(RecallStatus::RUNNING)) { //正在补招 if (ok) { //补招成功 front.recall_status = static_cast(RecallStatus::DONE); //标记为完成,补招任务会执行下一条 std::cout << "[RESP][EVENTLOG][OK] dev=" << id << " mon=" << matched_monitor->monitor_id << " " << front.StartTime << "~" << front.EndTime << " recall_status=" << front.recall_status << std::endl; } else if (response_code == 404) { //补招无数据 front.recall_status = static_cast(RecallStatus::EMPTY); std::cout << "[RESP][EVENTLOG][WARN] dev=" << id << " mon=" << matched_monitor->monitor_id << " " << front.StartTime << "~" << front.EndTime << " rc=" << response_code << " recall_status=" << front.recall_status << std::endl; //错误响应码 //记录日志 DIY_ERRORLOG_CODE(matched_monitor->monitor_id.c_str(),2,static_cast(LogCode::LOG_CODE_RECALL),"【ERROR】监测点:%s 补招数据失败 - 失败时间点:%lld 至 %lld",matched_monitor->monitor_id.c_str(),front.StartTime,front.EndTime); } else { //补招失败 front.recall_status = static_cast(RecallStatus::FAILED); std::cout << "[RESP][EVENTLOG][FAIL] dev=" << id << " mon=" << matched_monitor->monitor_id << " " << front.StartTime << "~" << front.EndTime << " rc=" << response_code << " recall_status=" << front.recall_status<< std::endl; //错误响应码 //记录日志 DIY_ERRORLOG_CODE(matched_monitor->monitor_id.c_str(),2,static_cast(LogCode::LOG_CODE_RECALL),"【ERROR】监测点:%s 补招数据失败 - 失败时间点:%lld 至 %lld",matched_monitor->monitor_id.c_str(),front.StartTime,front.EndTime); } updated = true; } else { //首条不是 RUNNING 状态,不应该收到这条响应 std::cout << "[RESP][EVENTLOG][WARN] dev=" << id << " mon=" << matched_monitor->monitor_id << " first item not RUNNING (status=" << front.recall_status << ")" << std::endl; //打印调试 } } else { //补招列表为空,不应该收到这条响应 std::cout << "[RESP][EVENTLOG][WARN] dev=" << id << " mon=" << matched_monitor->monitor_id << " recall_list empty" << std::endl; } } if (!updated) {//不更新补招列表 std::cout << "[RESP][EVENTLOG][WARN] no RUNNING item, dev=" << id << " rc=" << response_code << std::endl; } break; } // ================= 特殊:读取文件目录 ================= case DeviceState::READING_FILEMENU: { std::lock_guard lk(ledgermtx); // 1) 找到对应终端 terminal_dev* dev = nullptr; for (auto& d : terminal_devlist) { if (d.terminal_id == id) { dev = &d; break; } } if (!dev) { std::cout << "[RESP][FILEMENU] dev not found, id=" << id << " rc=" << response_code << std::endl; break; } std::vector names; // 2) 按该终端当前 busytype 分支处理 const int bt = dev->busytype; if (bt == static_cast(DeviceState::READING_FILEMENU)) { // ====== 分支 A:当前业务就是“读取文件目录” ====== if (ok) { if (filemenu_cache_take(id, names)) { //发送目录 send_file_list(dev,names); } else { // 失败:响应web并复位为空闲 //send_reply_to_cloud(static_cast(ResponseCode::BAD_REQUEST), id, static_cast(DeviceState::READING_FILEMENU),dev->guid,dev->mac); send_reply_to_queue(dev->guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理"); std::cout << "[RESP][FILEMENU->FILEMENU][WARN] dev=" << id << " names missing in cache" << std::endl; } // 成功:复位 dev->guid.clear(); dev->isbusy = 0; dev->busytype = 0; dev->busytimecount = 0; std::cout << "[RESP][FILEMENU->FILEMENU][OK] dev=" << id << std::endl; } else { // 失败:响应web并复位为空闲 //send_reply_to_cloud(response_code, id, static_cast(DeviceState::READING_FILEMENU),dev->guid,dev->mac); send_reply_to_queue(dev->guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理"); dev->guid.clear(); dev->isbusy = 0; dev->busytype = 0; dev->busytimecount = 0; std::cout << "[RESP][FILEMENU->FILEMENU][FAIL] dev=" << id << " rc=" << response_code << std::endl; } } else if ( bt == static_cast(DeviceState::READING_EVENTFILE) || bt == static_cast(DeviceState::READING_STATSFILE) ) { // ====== 分支 B:当前业务为“事件文件/统计文件”(两者处理相同) ====== // 一个装置同一时刻只会有一个监测点在补招 ledger_monitor* running_monitor = nullptr; RecallFile* running_front = nullptr; // 在该终端下,找到“首条 recall_list_static.front() 正在运行且处于 LISTING 的监测点” for (auto& lm : dev->line) { if (lm.recall_list_static.empty()) continue; RecallFile& f = lm.recall_list_static.front(); if (f.recall_status == static_cast(RecallStatus::RUNNING)//补招中 && f.phase == RecallPhase::LISTING) {//正在请求目录 running_monitor = &lm; //补招的测点 running_front = &f; //补招记录 break; } else { // 不是正在补招的测点,跳过//打印跳过的信息 std::cout << "[check_recall_file] skip non-running monitor dev=" << dev->terminal_id << " monitor=" << lm.monitor_id << " status=" << static_cast(f.recall_status) << " phase=" << static_cast(f.phase) << std::endl; continue; } } if (!running_monitor || !running_front) { //该装置没有正在补招的测点和补招记录,退出处理 std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][WARN] dev=" << id << " no RUNNING/LISTING recall on this device, ignore resp" << " rc=" << response_code << std::endl; break; } // 根据回执结果,回写目录结果;状态机会在下一轮推进到下一个目录/结束 if (ok) { if(filemenu_cache_take(id, names)) { // 从缓存取目录列表 running_front->dir_files[running_front->cur_dir] = std::move(names); running_front->list_result = ActionResult::OK; std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][OK] dev=" << id << " monitor=" << running_monitor->monitor_id << " dir=" << running_front->cur_dir << std::endl; } else { running_front->list_result = ActionResult::FAIL; std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][WARN] dev=" << id << " monitor=" << running_monitor->monitor_id << " dir=" << running_front->cur_dir << " names missing in cache" << std::endl; } } else { running_front->list_result = ActionResult::FAIL; std::cout << "[RESP][FILEMENU->(EVENT/STATS)FILE][FAIL] dev=" << id << " monitor=" << running_monitor->monitor_id << " dir=" << running_front->cur_dir << " rc=" << response_code << std::endl; } break; } else { // ====== 分支 C:其他业务场景下收到 FILEMENU 响应,统一处理 打印提示====== if (ok) { // 成功: std::cout << "[unknown][RESP][FILEMENU->OTHER][OK] dev=" << id << " keep busytype=" << bt << std::endl; } else { // 失败: std::cout << "[unknown][RESP][FILEMENU->OTHER][FAIL] dev=" << id << " rc=" << response_code << " prev busytype=" << bt << std::endl; } } break; } // ================= 特殊:下载文件 ================= case DeviceState::READING_FILEDATA: { std::lock_guard lk(ledgermtx); // 1) 找到对应终端 terminal_dev* dev = nullptr; for (auto& d : terminal_devlist) { if (d.terminal_id == id) { dev = &d; break; } } if (!dev) { std::cout << "[RESP][FILEDATA] dev not found, id=" << id << " rc=" << response_code << std::endl; break; } // 2) 按该终端当前 busytype 分支处理 const int bt = dev->busytype; if (bt == static_cast(DeviceState::READING_FILEDATA)) { // ====== 分支 A:当前业务就是“读取文件数据” ====== if (ok) { // 成功:复位 //send_reply_to_cloud(static_cast(ResponseCode::OK), id, static_cast(DeviceState::READING_FILEDATA),dev->guid,dev->mac); send_reply_to_queue(dev->guid, static_cast(ResponseCode::OK), "终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"成功,停止该业务处理"); dev->guid.clear(); dev->isbusy = 0; dev->busytype = 0; dev->busytimecount = 0; std::cout << "[RESP][FILEDATA->FILEDATA][OK] dev=" << id << std::endl; } else { // 失败:响应web并复位 //send_reply_to_cloud(response_code, id, static_cast(DeviceState::READING_FILEDATA),dev->guid,dev->mac); send_reply_to_queue(dev->guid, static_cast(ResponseCode::BAD_REQUEST), "终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) +"失败,停止该业务处理"); dev->guid.clear(); dev->isbusy = 0; dev->busytype = 0; dev->busytimecount = 0; std::cout << "[RESP][FILEDATA->FILEDATA][FAIL] dev=" << id << " rc=" << response_code << std::endl; } } else if ( bt == static_cast(DeviceState::READING_EVENTFILE) || bt == static_cast(DeviceState::READING_STATSFILE) ) { // ====== 分支 B:当前业务为“下载事件文件/统计文件”(两者处理相同) ====== //优先:找“首条处于 RUNNING 且 phase==DOWNLOADING”的监测点 ledger_monitor* matched_monitor = nullptr; RecallFile* running_front = nullptr; for (auto& lm : dev->line) { if (lm.recall_list_static.empty()) continue; RecallFile& f = lm.recall_list_static.front(); if (f.recall_status == static_cast(RecallStatus::RUNNING) && f.phase == RecallPhase::DOWNLOADING) { matched_monitor = &lm; running_front = &f; break; } } // 若没找到 RUNNING/DOWNLOADING,用 cid 精确匹配测点号 if (!matched_monitor) { const std::string cid_str = std::to_string(static_cast(cid)); for (auto& lm : dev->line) { if (lm.logical_device_seq == cid_str) { matched_monitor = &lm; break; } } if (matched_monitor && !matched_monitor->recall_list_static.empty()) { RecallFile& f = matched_monitor->recall_list_static.front(); if (f.recall_status == static_cast(RecallStatus::RUNNING) && f.phase == RecallPhase::DOWNLOADING) { running_front = &f; // 回退成功 std::cout << "[RESP][FILEDATA][FALLBACK-CID] dev=" << id << " cid=" << static_cast(cid) << " monitor=" << matched_monitor->monitor_id << std::endl; } else { std::cout << "[RESP][FILEDATA][WARN] dev=" << id << " cid matched monitor=" << matched_monitor->monitor_id << " but status=" << f.recall_status << " phase=" << static_cast(f.phase) << " — ignore this resp" << std::endl; matched_monitor = nullptr; } } } if (!matched_monitor || !running_front) { std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][WARN] dev=" << id << " no RUNNING/DOWNLOADING recall to accept filedata" << " rc=" << response_code << " cid=" << static_cast(cid) << std::endl; break; } // 到这里一定是 RUNNING/DOWNLOADING 的 front;按回执设置下载结果 if (ok) { running_front->download_result = ActionResult::OK; std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][OK] dev=" << id << " monitor=" << matched_monitor->monitor_id << " file=" << running_front->downloading_file << std::endl; } else { running_front->download_result = ActionResult::FAIL; std::cout << "[RESP][FILEDATA->(EVENT/STATS)FILE][FAIL] dev=" << id << " monitor=" << matched_monitor->monitor_id << " file=" << running_front->downloading_file << " rc=" << response_code << std::endl; } break; } else { // ====== 分支 C:其他业务场景下收到 FILEDATA 响应,统一处理 打印提示====== if (ok) { // 成功: std::cout << "[unknown][RESP][FILEDATA->OTHER][OK] dev=" << id << " keep busytype=" << bt << std::endl; } else { // 失败: std::cout << "[unknown][RESP][FILEDATA->OTHER][FAIL] dev=" << id << " rc=" << response_code << " prev busytype=" << bt << std::endl; } } break; } // ================= 其它状态统一处理 ================= default: { std::lock_guard lk(ledgermtx); terminal_dev* dev = nullptr; for (auto& d : terminal_devlist) { if (d.terminal_id == id) { dev = &d; break; } } if (dev) { //直接根据输入响应mq //send_reply_to_cloud(response_code, id, device_state_int, dev->guid, dev->mac); send_reply_to_queue(dev->guid, response_code, "终端 id: " + dev->terminal_id + "进行业务:" + get_type_by_state(dev->busytype) + "," + ResponseCodeToString(response_code) + "停止该业务处理"); //其他的错误和成功都会结束业务 dev->guid.clear(); // 清空 guid dev->busytype = 0; // 业务类型归零 dev->isbusy = 0; // 清空业务标志 dev->busytimecount = 0; // 计时归零 std::cout << "[clear_terminal_runtime_state] Cleared runtime state for terminal_id=" << id << std::endl; } break; } } // end switch } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////记录暂态事件到本地 // 将一条暂态数据更新/写入到指定终端ID与逻辑序号的监测点 // 返回 true 表示已写入(更新或追加),false 表示未找到对应终端/监测点。 bool append_qvvr_event(const std::string& terminal_id, int logical_seq, // 监测点序号(如 1) int nType, // 事件类型 double fPersisstime_sec, // 持续时间(秒) double fMagnitude_pu, // 幅值(pu) uint64_t triggerTimeMs, // 触发时间(毫秒) int phase) // 相别 { std::cout << "[append_qvvr_event] enter" << " tid=" << std::this_thread::get_id() << " terminal_id=" << terminal_id << " logical_seq=" << logical_seq << " type=" << nType << " per_s=" << fPersisstime_sec << " mag_pu=" << fMagnitude_pu << " time_ms=" << static_cast(triggerTimeMs) << " phase=" << phase << std::endl; { std::lock_guard lk(ledgermtx); std::cout << "[append_qvvr_event] lock acquired. terminal_devlist.size=" << terminal_devlist.size() << std::endl; // 1) 找终端 auto dev_it = std::find_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d){ return d.terminal_id == terminal_id; }); if (dev_it == terminal_devlist.end()) { std::cout << "[append_qvvr_event][MISS] terminal not found: " << terminal_id << std::endl; return false; } std::cout << "[append_qvvr_event][HIT] terminal_id=" << terminal_id << " monitors(line).size=" << dev_it->line.size() << std::endl; // 2) 找监测点(按逻辑序号匹配:字符串等于 或 数值等于) ledger_monitor* pMon = nullptr; for (size_t i = 0; i < dev_it->line.size(); ++i) { auto& m = dev_it->line[i]; bool eq_str = (m.logical_device_seq == std::to_string(logical_seq)); bool eq_num = false; try { if (!m.logical_device_seq.empty()) eq_num = (std::stoi(m.logical_device_seq) == logical_seq); } catch (...) { // 仅调试提示,不改变原逻辑 std::cout << "[append_qvvr_event][monitor #" << i << "] stoi fail, logical_device_seq=\"" << m.logical_device_seq << "\"" << std::endl; } std::cout << "[append_qvvr_event][probe monitor #" << i << "] monitor_id=" << m.monitor_id << " logical_device_seq=\"" << m.logical_device_seq << "\"" << " eq_str=" << eq_str << " eq_num=" << eq_num << std::endl; if (eq_str || eq_num) { pMon = &m; break; } } if (!pMon) { std::cout << "[append_qvvr_event][MISS] monitor not found by seq=" << logical_seq << " in terminal_id=" << terminal_id << std::endl; return false; } std::cout << "[append_qvvr_event][HIT] monitor_id=" << pMon->monitor_id << " logical_device_seq=" << pMon->logical_device_seq << " qvvrdata.size=" << pMon->qvvrevent.qvvrdata.size() << std::endl; qvvr_event& qe = pMon->qvvrevent; // 3) 先尝试“就地更新”(同类型 + 同时间戳 视为同一事件) for (size_t i = 0; i < qe.qvvrdata.size(); ++i) { auto& q = qe.qvvrdata[i]; if (q.QVVR_type == nType && q.QVVR_time == triggerTimeMs) { std::cout << "[append_qvvr_event][UPDATE match idx=" << i << "]" << " old{used=" << q.used_status << ", per=" << q.QVVR_PerTime << ", mag=" << q.QVVR_Amg << ", phase=" << q.phase << "} -> new{used=true" << ", per=" << fPersisstime_sec << ", mag=" << fMagnitude_pu << ", phase=" << phase << "}" << std::endl; q.used_status = true; q.QVVR_PerTime = fPersisstime_sec; q.QVVR_Amg = fMagnitude_pu; q.phase = phase; std::cout << "[append_qvvr_event] done(update)." << std::endl; return true; //更新完毕 } } //新的事件 // 4) 复用空槽(used_status=false) for (size_t i = 0; i < qe.qvvrdata.size(); ++i) { auto& q = qe.qvvrdata[i]; if (!q.used_status) { std::cout << "[append_qvvr_event][REUSE idx=" << i << "]" << " set{type=" << nType << ", time_ms=" << static_cast(triggerTimeMs) << ", per=" << fPersisstime_sec << ", mag=" << fMagnitude_pu << ", phase=" << phase << "}" << std::endl; q.used_status = true; q.QVVR_type = nType; q.QVVR_time = triggerTimeMs; q.QVVR_PerTime = fPersisstime_sec; q.QVVR_Amg = fMagnitude_pu; q.phase = phase; std::cout << "[append_qvvr_event] done(reuse)." << std::endl; return true; //复用完毕 } } // 5) 直接追加 qvvr_data q{}; q.used_status = true; q.QVVR_type = nType; q.QVVR_time = triggerTimeMs; // ms q.QVVR_PerTime = fPersisstime_sec; // s q.QVVR_Amg = fMagnitude_pu; q.phase = phase; qe.qvvrdata.push_back(q); std::cout << "[append_qvvr_event][PUSH_BACK]" << " new_size=" << qe.qvvrdata.size() << " last_idx=" << (qe.qvvrdata.empty() ? -1 : (int)qe.qvvrdata.size()-1) << " {type=" << nType << ", time_ms=" << static_cast(triggerTimeMs) << ", per=" << fPersisstime_sec << ", mag=" << fMagnitude_pu << ", phase=" << phase << "}" << std::endl; } std::cout << "[append_qvvr_event] done(push_back)." << std::endl; return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////实时数据封装发送 void enqueue_realtime_pq(const RealtagPqDate_float& realdata, int nPTType, unsigned char cid, const std::string& mac, const std::string& devid) { // 先根据 devIdxMap 的配置决定编码分支: // 2: 基础数据 3: 谐波电压含有率 4: 谐波电流有效值 5: 间谐波电压含有率 int idx = 0; std::string base64; // 这里尝试用 mac 作为 key 获取 idx;如果项目里 devIdxMap 的 key 不是 mac, // 你可以把这里改成对应的设备 id(devid)。未命中则再尝试用规范化后的 mac。 if (devidx_get(devid, idx)) { switch (idx) { case 2: // 基础数据(根据接线方式选择转换方法 数据全集解析) base64 = realdata.ConvertToBase64(nPTType); break; case 3: // 谐波电压含有率 base64 = realdata.ConvertToBase64_RtHarmV(nPTType); break; case 4: // 谐波电流有效值(幅值) base64 = realdata.ConvertToBase64_RtHarmI(); break; case 5: // 间谐波电压含有率 base64 = realdata.ConvertToBase64_RtInHarmV(); break; default: // 未知 idx,回退到基础数据 base64 = realdata.ConvertToBase64(nPTType); break; } } else { // 未配置 idx,回退到基础数据 base64 = realdata.ConvertToBase64(nPTType); } //std::cout << base64 << std::endl; //lnk实时数据使用接口发送20250711 time_t data_time = ConvertToTimestamp(realdata.time); { std::lock_guard lk(g_last_ts_mtx); auto it = g_last_ts_by_devid.find(devid); if (it != g_last_ts_by_devid.end() && it->second == data_time) { std::cout << "[enqueue_realtime_pq] duplicate timestamp, devid=" << devid << " time=" << data_time << std::endl; // 同一设备与上次时间相同 → 丢弃本次 return; } // 记录本次时间 g_last_ts_by_devid[devid] = data_time; std::cout << "[enqueue_realtime_pq] record timestamp, devid=" << devid << " time=" << data_time << std::endl; } std::vector arr; arr.push_back({1, //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”,4-“Cp95” data_time, //数据转换出来的时间,数据时标,相对1970年的秒,无效填入“-1” 0, //数据时标,微秒钟,无效填入“-1” 0, //数据标识,1-标识数据异常 base64}); std::string js = generate_json( normalize_mac(mac), -1, //需应答的报文订阅者收到后需以此ID应答,无需应答填入“-1” 1, //设备唯一标识Ldid,填入0代表Ndid,后续根据商议决定填id还是数字 1, //报文处理的优先级:1 I类紧急请求/响应 2 II类紧急请求/响应 3 普通请求/响应 4 广播报文 0x1302, //设备数据主动上送的数据类型 cid, //逻辑子设备ID,0-逻辑设备本身,无填-1 0x04, //数据类型固定为电能质量数据 1, //数据属性:无“0”、实时“1”、统计“2”等 idx, //数据集序号(以数据集方式上送),无填-1 arr //数据数组 ); //std::cout << js << std::en queue_data_t data; data.monitor_no = 1; //上送的实时数据没有测点序号,统一填1 data.strTopic = TOPIC_RTDATA; //实时topic data.strText = js; data.mp_id = ""; //监测点id,暂时不需要 data.tag = G_RT_TAG; //实时tag data.key = G_RT_KEY; //实时key std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(data); } ////////////////////////////////////////////////////////////////////////////////统计数据打包发送 // 封装:组装统计数据并入队发送 void enqueue_stat_pq(const std::string& max_base64Str, const std::string& min_base64Str, const std::string& avg_base64Str, const std::string& cp95_base64Str, time_t data_time, const std::string& mac, short cid) { std::vector arr; arr.push_back({1, //数据属性 -1-无, 0-“Rt”,1-“Max”,2-“Min”,3-“Avg”,4-“Cp95” data_time, //数据转换出来的时间,数据时标,相对1970年的秒,无效填入“-1” 0, //数据时标,微秒钟,无效填入“-1” 0, //数据标识,1-标识数据异常 max_base64Str}); arr.push_back({2, data_time, 0, 0, min_base64Str}); arr.push_back({3, data_time, 0, 0, avg_base64Str}); arr.push_back({4, data_time, 0, 0, cp95_base64Str}); std::string js = generate_json( normalize_mac(mac), -1, //需应答的报文订阅者收到后需以此ID应答,无需应答填入“-1” 1, //设备唯一标识Ldid,填入0代表Ndid,后续根据商议决定填id还是数字 1, //报文处理的优先级:1 I类紧急请求/响应 2 II类紧急请求/响应 3 普通请求/响应 4 广播报文 0x1302, //设备数据主动上送的数据类型 cid, //逻辑子设备ID,0-逻辑设备本身,无填-1(原:avg_data.name) 0x04, //数据类型固定为电能质量 2, //数据属性:无“0”、实时“1”、统计“2”等 1, //数据集序号(以数据集方式上送),无填-1 arr //数据数组 ); //std::cout << js << std::endl; queue_data_t data; data.monitor_no = cid; //监测点序号(原:avg_data.name) data.strTopic = TOPIC_STAT;//统计topic data.strText = js; data.mp_id = ""; //监测点id,暂时不需要 data.tag = G_ROCKETMQ_TAG; //统计tag data.key = G_ROCKETMQ_KEY; //统计key std::lock_guard lock(queue_data_list_mutex); queue_data_list.push_back(data); std::cout << "Successfully assembled tagPqData for line: " << cid << std::endl; } /////////////////////////////////////////////////////////////////////////////////////清空一个装置运行数据 size_t erase_one_terminals_by_id(const std::string& terminal_id) { // 先对所有匹配项做日志清理 for (const auto& d : terminal_devlist) { if (d.terminal_id == terminal_id) { remove_loggers_by_terminal_id(terminal_id); } } // 再统一擦除 const auto old_size = terminal_devlist.size(); terminal_devlist.erase( std::remove_if(terminal_devlist.begin(), terminal_devlist.end(), [&](const terminal_dev& d){ return d.terminal_id == terminal_id; }), terminal_devlist.end()); return old_size - terminal_devlist.size(); } /////////////////////////////////////////////////////////////////////////////////////////////// DeviceInfo make_device_from_terminal(const terminal_dev& t) { DeviceInfo d; // 基本信息 d.device_id = t.terminal_id; d.name = t.terminal_name; d.model = t.dev_type; d.mac = t.mac; // status:优先按数字解析,其次按 online/true/yes/on 识别;默认 0 int status = 0; if (!t.tmnl_status.empty()) { bool parsed_num = false; // 尝试解析为整数 char* endp = nullptr; long v = std::strtol(t.tmnl_status.c_str(), &endp, 10); if (endp && *endp == '\0') { parsed_num = true; status = (v != 0) ? 1 : 0; } if (!parsed_num) { std::string s = t.tmnl_status; for (char& c : s) c = static_cast(std::tolower(static_cast(c))); status = (s == "online" || s == "true" || s == "yes" || s == "on" || s == "1") ? 1 : 0; } } d.status = status; // righttime:支持 "1/true/yes/on";默认 false bool rt = false; if (!t.Righttime.empty()) { std::string s = t.Righttime; for (char& c : s) c = static_cast(std::tolower(static_cast(c))); rt = (s == "1" || s == "true" || s == "yes" || s == "on"); } d.righttime = rt; // points d.points.clear(); d.points.reserve(t.line.size()); for (const auto& m : t.line) { PointInfo p; p.point_id = m.monitor_id; p.name = m.monitor_name; p.device_id = t.terminal_id; // nCpuNo:从 logical_device_seq 转 ushort,非法则置 0 unsigned short cpu_no = 0; if (!m.logical_device_seq.empty()) { char* endp = nullptr; long v = std::strtol(m.logical_device_seq.c_str(), &endp, 10); if (endp && *endp == '\0' && v >= 0) { if (v > 0xFFFF) v = 0xFFFF; cpu_no = static_cast(v); } } p.nCpuNo = cpu_no; // 变比与电压等级 p.PT1 = m.PT1; p.PT2 = m.PT2; p.CT1 = m.CT1; p.CT2 = m.CT2; p.strScale = m.voltage_level; // 接线方式:0-星型 1-角型;支持 "0/1"、包含“角/三角/delta/Δ” int pttype = 0; // 默认星型 if (!m.terminal_connect.empty()) { std::string s = m.terminal_connect; for (char& c : s) c = static_cast(std::tolower(static_cast(c))); if (s == "1" || s.find("角") != std::string::npos || s.find("三角") != std::string::npos || s.find("delta") != std::string::npos || s.find("△") != std::string::npos || s.find("Δ") != std::string::npos) { pttype = 1; } } p.nPTType = pttype; d.points.push_back(std::move(p)); } return d; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////补招文件 // --------- mkdir -p ---------- static bool mkdir_p(const std::string& path, mode_t mode = 0755) { if (path.empty()) return false; if (access(path.c_str(), F_OK) == 0) return true; std::string cur; for (size_t i = 0; i < path.size(); ++i) { cur.push_back(path[i]); if (path[i] == '/' && !cur.empty()) { if (access(cur.c_str(), F_OK) != 0) { if (mkdir(cur.c_str(), mode) != 0 && errno != EEXIST) { return false; } } } } // 最后一层(不以 / 结尾时) if (access(path.c_str(), F_OK) != 0) { if (mkdir(path.c_str(), mode) != 0 && errno != EEXIST) { return false; } } return true; } static std::string build_recall_filepath(const std::string& guid, const std::string& monitorId, const std::string& start, const std::string& end) { std::string dir = "/FeProject/dat/recall/"; ensure_dir_exists("/FeProject"); ensure_dir_exists("/FeProject/dat"); ensure_dir_exists(dir); // 替换非法字符(例如空格、冒号) auto sanitize = [](std::string s) { for (auto& c : s) { if (c == ' ' || c == ':' || c == '/' || c == '\\') c = '_'; } return s; }; std::string filename = sanitize(guid) + "_" + sanitize(monitorId) + "_" + sanitize(start) + "_" + sanitize(end) + ".txt"; return dir + filename; } std::string sanitize_for_filename(std::string s) { // 把空格->'_',冒号/波浪号/斜杠去掉,避免非法字符 for (char& c : s) { if (c == ' ') c = '_'; else if (c == ':') c = '-'; else if (c == '~' || c == '/' || c == '\\') c = '-'; } return s; } bool init_recall_record_file(const std::string& guid, const std::string& terminalId, const std::string& monitorId, const std::string& start, const std::string& end) { try { std::string path = build_recall_filepath(guid, monitorId, start, end); // 建立索引:guid + monitorId -> path { std::lock_guard lk(g_recall_file_mtx); g_recall_file_index[std::make_pair(guid, monitorId)] = path; } std::ofstream ofs(path.c_str(), std::ios::out | std::ios::trunc); if (!ofs.is_open()) { std::cerr << "[recall_file] ERROR: cannot open file: " << path << " errno=" << errno << " (" << strerror(errno) << ")" << std::endl; return false; } // 写入头 ofs << "guid: " << guid << "\n"; ofs << "terminalId: " << terminalId << "\n"; ofs << "monitor_id: " << monitorId << "\n"; ofs << "start_time: " << start << "\n"; ofs << "end_time: " << end << "\n"; ofs << "msglist:\n"; ofs.close(); std::cout << "[recall_file] created file OK: " << path << std::endl; return true; } catch (const std::exception& e) { std::cerr << "[recall_file] Exception in init_recall_record_file: " << e.what() << std::endl; return false; } catch (...) { std::cerr << "[recall_file] Unknown exception in init_recall_record_file" << std::endl; return false; } } bool append_recall_record_line(const std::string& guid, const std::string& monitorId, const std::string& msg) { std::string path; { std::lock_guard lk(g_recall_file_mtx); auto it = g_recall_file_index.find(std::make_pair(guid, monitorId)); if (it == g_recall_file_index.end()) return false; path = it->second; } std::ofstream ofs(path.c_str(), std::ios::out | std::ios::app); if (!ofs.is_open()) return false; ofs << msg << "\n"; ofs.close(); return true; } bool delete_recall_record_file(const std::string& guid, const std::string& monitorId) { std::string path; { std::lock_guard lk(g_recall_file_mtx); auto it = g_recall_file_index.find(std::make_pair(guid, monitorId)); if (it == g_recall_file_index.end()) return false; path = it->second; g_recall_file_index.erase(it); } // 删除文件 if (access(path.c_str(), F_OK) == 0) { ::remove(path.c_str()); } return true; } static std::string find_recall_file(const std::string& guid, const std::string& monitorId) { std::string dir = "/FeProject/dat/recall/"; ensure_dir_exists("/FeProject"); ensure_dir_exists("/FeProject/dat"); ensure_dir_exists(dir); std::string prefix = sanitize_for_filename(guid) + "_" + sanitize_for_filename(monitorId) + "_"; DIR* d = opendir(dir.c_str()); if (!d) return ""; std::string found; struct dirent* ent; while ((ent = readdir(d)) != nullptr) { if (ent->d_type == DT_REG) { std::string name = ent->d_name; if (name.compare(0, prefix.size(), prefix) == 0) { found = dir + name; break; } } } closedir(d); return found; } bool get_recall_record_fields_by_guid_monitor(const std::string& guid, const std::string& monitorId, std::string& outGuid, std::string& terminalId, std::string& outMonitorId, std::string& startTime, std::string& endTime, std::string& msg) { std::string filepath = find_recall_file(guid, monitorId); if (filepath.empty()) { std::cerr << "[recall_file] 未找到匹配文件 guid=" << guid << " monitorId=" << monitorId << std::endl; return false; } std::ifstream ifs(filepath.c_str()); if (!ifs.is_open()) { std::cerr << "[recall_file] 打开文件失败: " << filepath << std::endl; return false; } std::string line; bool inMsglist = false; std::ostringstream msgbuf; while (std::getline(ifs, line)) { line = trim_copy(line); if (line.empty()) continue; if (!inMsglist) { parse_kv_line(line, "guid", outGuid); parse_kv_line(line, "terminalId", terminalId); parse_kv_line(line, "monitor_id", outMonitorId); parse_kv_line(line, "start_time", startTime); parse_kv_line(line, "end_time", endTime); if (line == "msglist:" || line == "msglist:") { inMsglist = true; } } else { msgbuf << line << "\n"; } } ifs.close(); msg = msgbuf.str(); if (!msg.empty() && msg.back() == '\n') msg.pop_back(); // ===== 校验 guid 和 monitor_id ===== if (outGuid != guid || outMonitorId != monitorId) { std::cerr << "[recall_file] 文件头与请求不一致: guid=" << outGuid << " monitor_id=" << outMonitorId << std::endl; return false; } // 简单检查字段完整性 if (terminalId.empty()) { std::cerr << "[recall_file] 文件字段不完整: " << filepath << std::endl; return false; } std::cout << "[recall_file] 读取成功: " << filepath << std::endl; return true; } ///////////////////////////////////////////////////////////////////////////////////////////上送文件选择 // ========================= 自动选择上传URL的封装函数 ========================= // 功能:根据终端 id 判断当前业务类型(busytype),自动选择不同上传接口。 // 入参和 SendFileWeb 完全一致。 bool SendFileWebAuto(const std::string& id, const std::string& local_path, const std::string& remote_path, std::string& out_filename) { try { // 查找对应装置并判断状态 std::lock_guard lk(ledgermtx); const terminal_dev* dev_ptr = nullptr; for (const auto& d : terminal_devlist) { if (d.terminal_id == id) { dev_ptr = &d; break; } } std::string file_cloudpath; if (dev_ptr) { const int bt = dev_ptr->busytype; // 若处于“事件文件/统计文件”补招阶段,则使用补招专用上传接口 if (bt == static_cast(DeviceState::READING_EVENTFILE) || bt == static_cast(DeviceState::READING_STATSFILE)) { file_cloudpath = "comtrade/" + dirname_with_slash(local_path); std::cout << "[SendFileWebAuto] dev=" << id << " busytype=" << bt << " -> use recall upload URL" << std::endl; } else { file_cloudpath = "download/" + dirname_with_slash(local_path); std::cout << "[SendFileWebAuto] dev=" << id << " busytype=" << bt << " -> use default upload URL" << std::endl; } } else { std::cout << "[SendFileWebAuto][WARN] device not found for id=" << id << ", fallback to default URL" << std::endl; } // 实际上传调用 SendFileWeb(WEB_FILEUPLOAD, local_path, file_cloudpath, out_filename); std::cout << "[SendFileWebAuto] File upload complete: " << out_filename << std::endl; return true; } catch (const std::exception& e) { std::cerr << "[SendFileWebAuto][ERROR] Exception: " << e.what() << std::endl; } catch (...) { std::cerr << "[SendFileWebAuto][ERROR] Unknown exception" << std::endl; } return false; }