Prechádzať zdrojové kódy

chore: remove old backup files and add Q&A archive

steven_roc 10 hodín pred
rodič
commit
4d608333cc

+ 0 - 311
src/frpc_manager1.cpp

@@ -1,311 +0,0 @@
-/**
- * frpc_manager.cpp - frpc 服务管理实现
- * fix24 v24: 通过 frpc admin HTTP API 获取状态,systemctl 控制重启
- */
-#include "frpc_manager.h"
-#include "common.h"
-
-#include <fstream>
-#include <sstream>
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-#include <unistd.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <sys/stat.h>
-#include <curl/curl.h>
-
-
-namespace {
-// 获取 systemctl 命令前缀(非 root 时需要 sudo)
-std::string systemctl_cmd() {
-    if (getuid() == 0) return "systemctl";
-    return "sudo systemctl";
-}
-}  // anonymous namespace
-
-namespace frpc_mgr {
-
-// ========== libcurl 回调 ==========
-static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
-    size_t total = size * nmemb;
-    std::string *str = static_cast<std::string*>(userp);
-    str->append(static_cast<char*>(contents), total);
-    return total;
-}
-
-// ========== 调用 frpc admin API ==========
-std::string call_admin_api(const std::string &endpoint) {
-    std::string url = "http://" + g_frpc_admin_addr + ":" +
-                      std::to_string(g_frpc_admin_port) + endpoint;
-
-    CURL *curl = curl_easy_init();
-    if (!curl) return "";
-
-    std::string response;
-    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
-    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);
-    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1L);
-
-    // Basic Auth
-    if (!g_frpc_admin_user.empty()) {
-        std::string auth = g_frpc_admin_user + ":" + g_frpc_admin_password;
-        curl_easy_setopt(curl, CURLOPT_USERPWD, auth.c_str());
-        curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
-    }
-
-    CURLcode res = curl_easy_perform(curl);
-    curl_easy_cleanup(curl);
-
-    if (res != CURLE_OK) return "";
-    return response;
-}
-
-// ========== 检查 frpc 进程是否存活 ==========
-bool is_process_alive() {
-    FILE *fp = popen((systemctl_cmd() + " is-active frpc 2>/dev/null").c_str(), "r");
-    if (!fp) return false;
-    char buf[64] = {0};
-    if (fgets(buf, sizeof(buf) - 1, fp)) {
-        // trim newline
-        char *nl = strchr(buf, '\n');
-        if (nl) *nl = '\0';
-        pclose(fp);
-        return (strcmp(buf, "active") == 0);
-    }
-    pclose(fp);
-    return false;
-}
-
-// ========== 获取 frpc 状态(带缓存,10秒TTL) ==========
-static FrpcStatus g_cached_status;
-static time_t g_cache_time = 0;
-static const int CACHE_TTL = 30; // fix24 v38: 缓存有效期30秒(减少popen/curl频率,降低RPi卡顿)
-
-FrpcStatus get_status() {
-    time_t now = time(nullptr);
-    if (now - g_cache_time < CACHE_TTL && g_cache_time > 0) {
-        return g_cached_status;
-    }
-
-    FrpcStatus st;
-    st.running = is_process_alive();
-
-    if (!st.running) {
-        st.error = "frpc 服务未运行";
-        g_cached_status = st;
-        g_cache_time = now;
-        return st;
-    }
-
-    // 调用 admin API 获取隧道状态
-    // frpc admin API: GET /api/proxy/tcp, /api/proxy/udp, etc.
-    // 或者使用 status 接口
-    const char* proxy_types[] = {"tcp", "udp", "http", "https", nullptr};
-
-    for (int i = 0; proxy_types[i]; i++) {
-        std::string endpoint = std::string("/api/proxy/") + proxy_types[i];
-        std::string resp = call_admin_api(endpoint);
-        if (resp.empty()) continue;
-
-        // 简单解析 JSON 响应
-        // frpc admin API 返回格式: {"proxies": [...], "stats": [...]}
-        // 这里做基本解析,实际应用中可用 cJSON
-        // 为了简化,将原始 JSON 存入隧道信息的扩展字段
-
-        // 简单提取 proxies 数组中的 name 和 status
-        // 这是一个简化的解析器
-        size_t pos = resp.find("\"proxies\"");
-        if (pos == std::string::npos) continue;
-
-        pos = resp.find('[', pos);
-        if (pos == std::string::npos) continue;
-
-        // 遍历 proxies 数组
-        size_t arr_end = resp.find(']', pos);
-        if (arr_end == std::string::npos) arr_end = resp.size();
-
-        std::string arr_content = resp.substr(pos + 1, arr_end - pos - 1);
-
-        // 分割各个对象
-        size_t obj_start = 0;
-        while ((obj_start = arr_content.find('{', obj_start)) != std::string::npos) {
-            size_t obj_end = arr_content.find('}', obj_start);
-            if (obj_end == std::string::npos) break;
-
-            std::string obj = arr_content.substr(obj_start, obj_end - obj_start + 1);
-
-            FrpcTunnel tunnel;
-            tunnel.type = proxy_types[i];
-
-            // 提取 name
-            size_t name_pos = obj.find("\"name\"");
-            if (name_pos != std::string::npos) {
-                size_t colon = obj.find(':', name_pos);
-                size_t q1 = obj.find('"', colon + 1);
-                size_t q2 = obj.find('"', q1 + 1);
-                if (q1 != std::string::npos && q2 != std::string::npos) {
-                    tunnel.name = obj.substr(q1 + 1, q2 - q1 - 1);
-                }
-            }
-
-            // 提取 status
-            size_t status_pos = obj.find("\"status\"");
-            if (status_pos != std::string::npos) {
-                size_t colon = obj.find(':', status_pos);
-                size_t q1 = obj.find('"', colon + 1);
-                size_t q2 = obj.find('"', q1 + 1);
-                if (q1 != std::string::npos && q2 != std::string::npos) {
-                    tunnel.status = obj.substr(q1 + 1, q2 - q1 - 1);
-                }
-            }
-
-            // 提取 local_addr
-            size_t la_pos = obj.find("\"local_addr\"");
-            if (la_pos == std::string::npos) la_pos = obj.find("\"local_ip\"");
-            if (la_pos != std::string::npos) {
-                size_t colon = obj.find(':', la_pos);
-                size_t q1 = obj.find('"', colon + 1);
-                size_t q2 = obj.find('"', q1 + 1);
-                if (q1 != std::string::npos && q2 != std::string::npos) {
-                    tunnel.local_addr = obj.substr(q1 + 1, q2 - q1 - 1);
-                }
-                // 尝试获取 local_port
-                size_t lp_pos = obj.find("\"local_port\"", la_pos);
-                if (lp_pos != std::string::npos && lp_pos < obj_end) {
-                    size_t colon2 = obj.find(':', lp_pos);
-                    size_t val_start = obj.find_first_of("0123456789", colon2 + 1);
-                    size_t val_end = obj.find_first_not_of("0123456789", val_start);
-                    if (val_start != std::string::npos) {
-                        tunnel.local_addr += ":" + obj.substr(val_start, val_end - val_start);
-                    }
-                }
-            }
-
-            // 提取 remote_addr
-            size_t ra_pos = obj.find("\"remote_addr\"");
-            if (ra_pos != std::string::npos) {
-                size_t colon = obj.find(':', ra_pos);
-                size_t q1 = obj.find('"', colon + 1);
-                size_t q2 = obj.find('"', q1 + 1);
-                if (q1 != std::string::npos && q2 != std::string::npos) {
-                    tunnel.remote_addr = obj.substr(q1 + 1, q2 - q1 - 1);
-                }
-            }
-
-            if (!tunnel.name.empty()) {
-                st.tunnels.push_back(tunnel);
-            }
-
-            obj_start = obj_end + 1;
-        }
-    }
-
-    g_cached_status = st;
-    g_cache_time = now;
-    return st;
-}
-
-// ========== 读取 frpc.toml ==========
-std::string read_config() {
-    std::ifstream f(g_frpc_config_path);
-    if (!f.is_open()) return "";
-    return std::string((std::istreambuf_iterator<char>(f)),
-                        std::istreambuf_iterator<char>());
-}
-
-// ========== 保存 frpc.toml(自动备份) ==========
-bool save_config(const std::string &content, std::string &error) {
-    // 备份现有配置
-    if (!g_frpc_backup_path.empty()) {
-        std::ifstream src(g_frpc_config_path, std::ios::binary);
-        if (src.is_open()) {
-            std::ofstream dst(g_frpc_backup_path, std::ios::binary);
-            if (dst.is_open()) {
-                dst << src.rdbuf();
-                dst.close();
-            }
-            src.close();
-        }
-    }
-
-    // 写入新配置
-    std::ofstream f(g_frpc_config_path);
-    if (!f.is_open()) {
-        error = "无法打开配置文件: " + g_frpc_config_path;
-        return false;
-    }
-    f << content;
-    f.close();
-
-    if (!f.good()) {
-        error = "写入配置文件失败";
-        return false;
-    }
-
-    std::cout << "[frpc] 配置已保存并备份到 " << g_frpc_backup_path << std::endl;
-    return true;
-}
-
-// ========== 重启 frpc 服务 ==========
-bool restart_service(std::string &error) {
-    // 如果有自动回滚,先确保备份存在
-    std::string backup_content;
-    if (g_frpc_auto_rollback) {
-        backup_content = read_config();
-        if (backup_content.empty()) {
-            // 尝试从备份文件恢复
-            std::ifstream bf(g_frpc_backup_path);
-            if (bf.is_open()) {
-                backup_content = std::string((std::istreambuf_iterator<char>(bf)),
-                                              std::istreambuf_iterator<char>());
-            }
-        }
-    }
-
-    // 重启服务
-    std::string cmd = systemctl_cmd() + " restart frpc 2>&1";
-    int ret = system(cmd.c_str());
-    if (ret != 0) {
-        int exit_code = WEXITSTATUS(ret);
-        error = "systemctl restart frpc 失败 (exit code: " + std::to_string(exit_code) + ")";
-
-        // 自动回滚
-        if (g_frpc_auto_rollback && !backup_content.empty()) {
-            std::cout << "[frpc] 重启失败,执行自动回滚..." << std::endl;
-            std::ofstream f(g_frpc_config_path);
-            if (f.is_open()) {
-                f << backup_content;
-                f.close();
-                system((systemctl_cmd() + " restart frpc 2>&1").c_str());
-            }
-        }
-        return false;
-    }
-
-    // 等待并检查服务状态
-    sleep(g_frpc_restart_check_timeout);
-    if (!is_process_alive()) {
-        error = "frpc 重启后未正常运行";
-
-        // 自动回滚
-        if (g_frpc_auto_rollback && !backup_content.empty()) {
-            std::cout << "[frpc] 服务异常,执行自动回滚..." << std::endl;
-            std::ofstream f(g_frpc_config_path);
-            if (f.is_open()) {
-                f << backup_content;
-                f.close();
-                system((systemctl_cmd() + " restart frpc 2>&1").c_str());
-            }
-        }
-        return false;
-    }
-
-    std::cout << "[frpc] 服务重启成功" << std::endl;
-    return true;
-}
-
-}  // namespace frpc_mgr

+ 0 - 202
src/mqtt_client1.cpp

@@ -1,202 +0,0 @@
-/**
- * mqtt_client.cpp — MQTT客户端完整实现
- * fix20: 从 plate_log2mqtt.cpp 完整迁移 F13-F15 功能
- * 包括:mosquittopp连接管理、自动重连、消息发布、ACK确认
- */
-#include "mqtt_client.h"
-#include <mosquitto.h>
-#include <mosquittopp.h>
-#include <unistd.h>
-
-// ==================== MQTT状态枚举 ====================
-enum class MqttState { DISCONNECTED, CONNECTING, CONNECTED, STABLE };
-
-// ==================== MQTT客户端类 ====================
-class MqttClient : public mosqpp::mosquittopp {
-public:
-    std::atomic<bool> connected{false};
-    std::atomic<bool> acked{false};
-    std::atomic<int> reconnect_attempts{0};
-    std::atomic<int> pending_reconnects{0};
-    std::atomic<MqttState> current_state{MqttState::DISCONNECTED};
-    static const int MAX_RECONNECT_ATTEMPTS = 30;
-    int mid = 0;
-
-    MqttClient(const std::string& client_id)
-        : mosqpp::mosquittopp(
-            (client_id + "_" + std::to_string(getpid()) + "_" + std::to_string(std::time(nullptr))).c_str()) {
-        if (!g_mqtt_username.empty()) {
-            username_pw_set(g_mqtt_username.c_str(), g_mqtt_password.c_str());
-        }
-        loop_start();
-    }
-
-    ~MqttClient() {
-        if (connected.load()) {
-            disconnect();
-            std::this_thread::sleep_for(std::chrono::milliseconds(300));
-        }
-        loop_stop(true);
-        // 等待重连线程退出
-        int wait_count = 0;
-        while (pending_reconnects.load() > 0 && wait_count < 30) {
-            std::this_thread::sleep_for(std::chrono::milliseconds(100));
-            wait_count++;
-        }
-        if (pending_reconnects.load() > 0) {
-            std::cerr << "[MQTT] 析构超时,仍有" << pending_reconnects.load() << "个重连线程" << std::endl;
-        }
-    }
-
-    bool connect_server() {
-        current_state = MqttState::CONNECTING;
-        return connect_async(g_mqtt_host.c_str(), g_mqtt_port, 60) == MOSQ_ERR_SUCCESS;
-    }
-
-    bool publish_message(const std::string& msg) {
-        if (!connected.load() || !g_running) return false;
-        acked = false;
-        return publish(&mid, g_mqtt_topic.c_str(), msg.size(), msg.data(), 1, false) == MOSQ_ERR_SUCCESS;
-    }
-
-    // ===== 回调函数 =====
-    void on_connect(int rc) override {
-        if (rc == 0) {
-            connected = true;
-            current_state = MqttState::CONNECTED;
-            std::cout << "[MQTT] 已连接到 " << g_mqtt_host << ":" << g_mqtt_port << std::endl;
-            // 5秒后标记为稳定连接,重置重连计数
-            std::thread([this]() {
-                std::this_thread::sleep_for(std::chrono::seconds(5));
-                if (current_state.load() == MqttState::CONNECTED) {
-                    current_state = MqttState::STABLE;
-                    reconnect_attempts = 0;
-                    std::cout << "[MQTT] 连接稳定,重连计数器已重置" << std::endl;
-                }
-            }).detach();
-        } else {
-            connected = false;
-            current_state = MqttState::DISCONNECTED;
-            std::cerr << "[MQTT] 连接失败 rc=" << rc << std::endl;
-        }
-    }
-
-    void on_disconnect(int rc) override {
-        connected = false;
-        MqttState prev = current_state.exchange(MqttState::DISCONNECTED);
-        if (rc != 0 && g_running) {
-            int curr = reconnect_attempts.load();
-            if (prev != MqttState::STABLE && curr < MAX_RECONNECT_ATTEMPTS) {
-                int ms = std::min(30000, 1000 * (1 << std::min(curr, 5)));
-                std::cerr << "[MQTT] 断开," << ms/1000 << "s后重连(" << curr+1
-                          << "/" << MAX_RECONNECT_ATTEMPTS << ")" << std::endl;
-                pending_reconnects++;
-                std::thread([this, ms, curr]() {
-                    for (int i = 0; i < ms/100 && g_running; i++)
-                        std::this_thread::sleep_for(std::chrono::milliseconds(100));
-                    if (g_running) {
-                        int expected = curr;
-                        int desired = curr + 1;
-                        if (reconnect_attempts.compare_exchange_strong(expected, desired)) {
-                            reconnect_async();
-                        }
-                    }
-                    pending_reconnects--;
-                }).detach();
-            } else if (prev != MqttState::STABLE) {
-                std::cerr << "[MQTT] 重连超限(" << MAX_RECONNECT_ATTEMPTS << "次)" << std::endl;
-            } else {
-                std::cerr << "[MQTT] 稳定连接断开,立即重连..." << std::endl;
-                pending_reconnects++;
-                std::thread([this]() {
-                    std::this_thread::sleep_for(std::chrono::seconds(1));
-                    if (g_running) reconnect_async();
-                    pending_reconnects--;
-                }).detach();
-            }
-        }
-    }
-
-    void on_publish(int m) override {
-        if (m == mid) acked = true;
-    }
-};
-
-// ==================== 全局MQTT实例 ====================
-static MqttClient* g_mqtt_client = nullptr;
-
-// ==================== 公开API ====================
-
-bool mqtt_client_init() {
-    if (!g_mqtt_enabled || g_mqtt_host.empty()) {
-        std::cout << "[MQTT] 未启用或未配置host" << std::endl;
-        return false;
-    }
-
-    if (g_mqtt_port < 1 || g_mqtt_port > 65535) {
-        std::cerr << "[MQTT] 无效端口: " << g_mqtt_port << std::endl;
-        return false;
-    }
-
-    std::cout << "[MQTT] 初始化 - " << g_mqtt_host << ":" << g_mqtt_port
-              << " topic=" << g_mqtt_topic
-              << " client_id=" << g_mqtt_client_id << std::endl;
-
-    mosqpp::lib_init();
-    g_mqtt_client = new MqttClient(g_mqtt_client_id);
-
-    // 尝试连接(最多3次)
-    for (int attempt = 0; attempt < 3; attempt++) {
-        g_mqtt_client->connect_server();
-        std::cout << "[MQTT] 尝试连接(" << (attempt+1) << "/3)..." << std::endl;
-        for (int i = 0; i < 100 && !g_mqtt_client->connected.load() && g_running; i++)
-            std::this_thread::sleep_for(std::chrono::milliseconds(100));
-        if (g_mqtt_client->connected.load()) {
-            std::cout << "[MQTT] 连接成功" << std::endl;
-            return true;
-        }
-        if (attempt < 2) {
-            std::cerr << "[MQTT] 连接失败,2秒后重试..." << std::endl;
-            std::this_thread::sleep_for(std::chrono::seconds(2));
-        }
-    }
-
-    std::cerr << "[MQTT] 初始连接超时,将在后台继续重试" << std::endl;
-    return false;  // 不阻止程序启动,后台自动重连
-}
-
-bool mqtt_client_publish(const std::string& topic, const std::string& payload) {
-    if (!g_mqtt_client || !g_mqtt_enabled) return false;
-
-    const std::string& use_topic = topic.empty() ? g_mqtt_topic : topic;
-
-    if (!g_mqtt_client->connected.load()) {
-        std::cerr << "[MQTT] 未连接,无法发布" << std::endl;
-        return false;
-    }
-
-    // 发布消息
-    int mid = 0;
-    g_mqtt_client->acked = false;
-    int rc = g_mqtt_client->publish(&mid, use_topic.c_str(),
-                                     payload.size(), payload.data(), 1, false);
-    if (rc != MOSQ_ERR_SUCCESS) {
-        std::cerr << "[MQTT] publish失败 rc=" << rc << std::endl;
-        return false;
-    }
-
-    // 等待ACK(最多10秒)
-    for (int i = 0; i < 100 && !g_mqtt_client->acked.load() && g_running; i++)
-        std::this_thread::sleep_for(std::chrono::milliseconds(100));
-
-    return g_mqtt_client->acked.load();
-}
-
-void mqtt_client_close() {
-    if (g_mqtt_client) {
-        delete g_mqtt_client;
-        g_mqtt_client = nullptr;
-    }
-    mosqpp::lib_cleanup();
-    std::cout << "[MQTT] 已关闭" << std::endl;
-}

+ 0 - 742
src/network_client1.cpp

@@ -1,742 +0,0 @@
-/**
- * network_client.cpp — 网络通信实现
- */
-#include "network_client.h"
-#include "feishu_client.h"
-#include "utils.h"
-#include "md5ex1.h"
-#include <iostream>
-
-size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, std::string* str);
-
-// ==================== 飞书配置 ====================
-
-void InitCurlCommon(CURL* curl, std::string* response) {
-	curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
-	curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
-	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
-	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
-	curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
-}
-
-int parse_create_bill_response(const std::string& response, const std::string& plate, bool is_in, std::string& tb_num_out) {
-    tb_num_out.clear();
-    cJSON* root = cJSON_Parse(response.c_str());
-    if (!root) {
-        std::cerr << "[解析失败] 响应不是合法JSON: " << response << std::endl;
-        return static_cast<int>(ErrorCode::PARSE_ERROR);
-    }
-    std::cout << response << std::endl;
-  
-    cJSON* status = cJSON_GetObjectItem(root, "status");
-    cJSON* data = cJSON_GetObjectItem(root, "data");
-    cJSON* msg = cJSON_GetObjectItem(root, "msg");
-
-    if (msg && cJSON_IsString(msg) && strstr(msg->valuestring, "已确认上线") != NULL) {
-        std::cout << "检测到Test环境已确认上线,自动切换到正式环境" << std::endl;
-        TestFlag = 0;
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::BUSINESS_ERROR);
-    }
-
-    // 业务错误在任何日志级别下都生效
-    if (DEBUG_LOG == 0)//调试模式DEBUG_LOG = 1,正式运行DEBUG_LOG =0屏蔽掉
-  	{
-      if (msg && cJSON_IsString(msg) && strstr(msg->valuestring, "非在网车辆") != NULL) {
-        std::cerr << "车牌 " << plate << (is_in ? " 进站" : " 出站") 
-                  << " 非在网车辆" << std::endl;
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::BUSINESS_ERROR);
-      }	
-    }
-    if (status && status->valueint == BUSINESS_ERROR_CODE) {
-        std::cerr << "车牌 " << plate << (is_in ? " 进站" : " 出站") 
-                  << " 业务异常: " << (msg ? msg->valuestring : "未知") << std::endl;
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::BUSINESS_ERROR);
-    }
-    if (DEBUG_LOG == 0)//调试模式DEBUG_LOG = 1,正式运行DEBUG_LOG =0屏蔽掉
-  	{
-      if (!status || status->valueint != HTTP_STATUS_OK) {
-        std::cerr << "[创建工单失败] 服务器返回非200状态: " << (status ? status->valueint : -1) << std::endl;
-        if (msg && cJSON_IsString(msg)) {
-            std::cout << "错误信息: " << msg->valuestring << std::endl;
-        }
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::NETWORK_ERROR);
-      }
-    }
-    if (!data) {
-        std::cerr << "[解析失败] 响应缺少data字段" << std::endl;
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::PARSE_ERROR);
-    }
-
-    cJSON* tbNo = cJSON_GetObjectItem(data, "tbNo");
-    if (!tbNo || !cJSON_IsString(tbNo) || strlen(tbNo->valuestring) == 0) {
-        std::cerr << "[解析失败] 未找到有效的tbNo" << std::endl;
-        cJSON_Delete(root);
-        return static_cast<int>(ErrorCode::PARSE_ERROR);
-    }
-
-    tb_num_out = tbNo->valuestring;
-    update_bill_cache_tb_num(plate, is_in, tb_num_out);
-    std::cout << "[解析成功] " << (is_in ? "进站" : "出站") << "联单编号: " << tb_num_out << std::endl;
-
-    cJSON_Delete(root);
-    g_metrics.record_createbill(true);
-    return static_cast<int>(ErrorCode::SUCCESS);
-}
-
-int parse_online_status_response(std::string resp)
-{
-	std::cout << resp << std::endl;
-	cJSON* root = cJSON_Parse(resp.c_str());
-	if (NULL == root)
-	{
-		std::cout << "parse_upload_image_response error!!!" << std::endl;
-		return -1;
-	}
-	cJSON* status = cJSON_GetObjectItem(root, "status");
-	if (status && status->valueint == HTTP_STATUS_OK)
-	{
-		std::cout << "online status success!!!" << std::endl;
-	}
-	cJSON_Delete(root);
-	return 0;
-}
-
-int parse_upload_image_response(std::string resp)
-{
-	std::cout << resp << std::endl;
-	cJSON* root = cJSON_Parse(resp.c_str());
-	if (NULL == root)
-	{
-		std::cout << "parse_upload_image_response error!!!" << std::endl;
-		return -1;
-	}
-	cJSON* status = cJSON_GetObjectItem(root, "status");
-	if (status && status->valueint == HTTP_STATUS_OK)
-	{
-		std::cout << "upload image success!!!" << std::endl;
-		cJSON_Delete(root);
-		return 0;
-	}
-	std::cerr << "upload image failed" << std::endl;
-	cJSON_Delete(root);
-	return -1;
-}
-
-std::string json_create_bill_encode(std::shared_ptr<CarPlateInfo> plate_info)
-{
-	CaptureInfo* plate_cap_info = plate_info->cap_info_copy.get();
-	if (!plate_cap_info) {
-		std::cerr << "json_create_bill_encode: cap_info为空" << std::endl;
-		return std::string("");
-	}
-
-	cJSON* json = cJSON_CreateObject();
-	bool is_special = false;
-	{
-		std::lock_guard<std::mutex> lock(g_special_plates_mtx);
-		for (auto& p : g_special_plates) {
-			if (p == plate_info->code) {
-				is_special = true;
-				break;
-			}
-		}
-	}
-
-	if (g_wType == 1)
-	{
-		cJSON_AddStringToObject(json, "plateNumber", plate_info->code.c_str());
-		cJSON_AddStringToObject(json, "outWorkSiteDate", get_format_time().c_str());
-		cJSON_AddStringToObject(json, "workSiteNo", point_number.c_str());
-		cJSON_AddStringToObject(json, "doorNo", throughway.c_str());
-		if (is_special) {
-			cJSON_AddStringToObject(json, "platenumcolor", "黄色");
-			cJSON_AddStringToObject(json, "vehicleType", "渣土车");
-		}
-		else {
-			cJSON_AddStringToObject(json, "platenumcolor", plate_info->type.c_str());
-			cJSON_AddStringToObject(json, "vehicleType", "");
-		}
-		cJSON_AddNumberToObject(json, "dType", plate_cap_info->dtype);
-	}
-	else if (g_wType == 3)
-	{
-		cJSON_AddStringToObject(json, "plateNumber", plate_info->code.c_str());
-		cJSON_AddStringToObject(json, "inDisposalDate", get_format_time().c_str());
-		cJSON_AddStringToObject(json, "disposalNo", point_number.c_str());
-		cJSON_AddStringToObject(json, "doorNo", throughway.c_str());
-		if (is_special) {
-			cJSON_AddStringToObject(json, "platenumcolor", "黄色");
-		}
-		else {
-			cJSON_AddStringToObject(json, "platenumcolor", plate_info->type.c_str());
-		}
-		cJSON_AddNumberToObject(json, "dType", plate_cap_info->dtype);
-	}
-
-	char* string = cJSON_Print(json);
-	cJSON_Delete(json);
-	if (string == NULL)
-	{
-		return std::string("");
-	}
-	std::string res = std::string(string);
-	free(string);
-	return res;
-}
-
-std::string json_online_status_encode(CaptureInfo* cap_info)
-{
-	if (!cap_info) {
-		std::cerr << "json_online_status_encode: cap_info为空" << std::endl;
-		return std::string("");
-	}
-
-	cJSON* json = cJSON_CreateObject();
-	cJSON_AddStringToObject(json, "workSiteNo", point_number.c_str());
-	cJSON_AddNumberToObject(json, "wType", g_wType);
-	cJSON_AddNumberToObject(json, "dType", cap_info->dtype);
-	cJSON_AddNumberToObject(json, "gType", cap_info->gtype);
-	cJSON_AddNumberToObject(json, "status", cap_info->status);
-	cJSON_AddStringToObject(json, "remark", "");
-	cJSON_AddStringToObject(json, "timestamp", get_format_time().c_str());
-
-	char* string = cJSON_Print(json);
-	cJSON_Delete(json);
-	if (string == NULL)
-	{
-		return std::string("");
-	}
-	std::string res = std::string(string);
-	free(string);
-	return res;
-}
-
-// ============================================
-// v41.2 新增: 批量状态上报 - 将4个摄像头合并为1个请求
-// ============================================
-std::string json_online_status_batch_encode()
-{
-	cJSON* root = cJSON_CreateObject();
-	cJSON_AddStringToObject(root, "workSiteNo", point_number.c_str());
-	cJSON_AddNumberToObject(root, "wType", g_wType);
-	cJSON_AddStringToObject(root, "timestamp", get_format_time().c_str());
-	
-	// 创建摄像头状态数组
-	cJSON* cameraList = cJSON_CreateArray();
-	
-	// 4个摄像头的状态
-	CaptureInfo* cameras[4] = {
-		&plate_rec_app.front_info_in,   // 进站低位
-		&plate_rec_app.front_info_out,  // 出站低位
-		&plate_rec_app.side_info_in,    // 进站高位
-		&plate_rec_app.side_info_out    // 出站高位
-	};
-	
-	for (int i = 0; i < 4; i++) {
-		cJSON* cam = cJSON_CreateObject();
-		cJSON_AddNumberToObject(cam, "dType", cameras[i]->dtype);
-		cJSON_AddNumberToObject(cam, "gType", cameras[i]->gtype);
-		cJSON_AddNumberToObject(cam, "status", cameras[i]->status);
-		cJSON_AddStringToObject(cam, "remark", "");
-		cJSON_AddItemToArray(cameraList, cam);
-	}
-	
-	cJSON_AddItemToObject(root, "cameraList", cameraList);
-	
-	char* string = cJSON_PrintUnformatted(root);
-	cJSON_Delete(root);
-	if (string == NULL) {
-		return std::string("");
-	}
-	std::string res = std::string(string);
-	free(string);
-	return res;
-}
-
-// 前向声明
-std::string get_new_timestamp();
-
-// ==================== 签名计算 ====================
-std::string calculate_sign(const std::string& app_key, const std::string& app_secret, const std::string& timestamp) {
-	std::string sign_str = app_secret + "#app_key" + app_key + "timestamp" + timestamp + "#" + app_secret;
-	char md5_hex[33];
-	md5_hexEx1(sign_str.c_str(), md5_hex);
-	return std::string(md5_hex);
-}
-
-// ==================== libcurl初始化/清理 ====================
-int lib_curl_init()
-{
-	curl_global_init(CURL_GLOBAL_DEFAULT);
-	return 0;
-}
-
-void lib_curl_deinit()
-{
-	curl_global_cleanup();
-}
-
-// ==================== 飞书Token缓存 ====================
-std::string get_cached_tenant_token() {
-	time_t now = time(NULL);
-	if (!g_tenant_token.empty() && now < g_token_expire_time.load() - TOKEN_REFRESH_BUFFER_SECONDS) {
-		return g_tenant_token;
-	}
-
-	std::lock_guard<std::mutex> lock(g_token_mtx);
-	now = time(NULL);
-	if (!g_tenant_token.empty() && now < g_token_expire_time.load() - TOKEN_REFRESH_BUFFER_SECONDS) {
-		return g_tenant_token;
-	}
-
-	std::string new_token = get_tenant_token();
-	if (!new_token.empty()) {
-		g_tenant_token = new_token;
-		g_token_expire_time.store(time(NULL) + TOKEN_EXPIRE_SECONDS);
-		std::cout << "[Token刷新] 飞书租户Token已更新,过期时间: " << g_token_expire_time.load() << std::endl;
-	} else {
-		std::cerr << "[Token刷新] 获取飞书Token失败,使用旧Token(如果存在)" << std::endl;
-	}
-
-	return g_tenant_token;
-}
-
-// 批量状态上报请求
-
-int lib_curl_online_status_batch_request()
-{
-	std::string response;
-	std::string timestamp = get_new_timestamp();
-	std::string sign = calculate_sign(app_key, app_secret, timestamp);
-	std::string req_url = "";
-
-	if (TestFlag == 1) {
-		req_url = app_api + "onlineStatusBatchTest?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	} else {
-		// 使用批量接口
-		req_url = app_api + "onlineStatusBatch?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-
-	// v41.2: MAX_RETRY=4 表示 初始请求 + 3次重试 = 4次请求
-	const int MAX_RETRY = 4;
-	for (int retry = 0; retry < MAX_RETRY; retry++) {
-		if (retry > 0) {
-			// 指数退让:2^retry = 2, 4, 8 秒
-			int backoff_seconds = (1 << retry);
-			std::cerr << "[批量状态] 重试第" << retry << "次(共" << MAX_RETRY-1 << "次),等待" << backoff_seconds << "秒..." << std::endl;
-			std::this_thread::sleep_for(std::chrono::seconds(backoff_seconds));
-		}
-
-		CURL* curl = curl_easy_init();
-		if (!curl) {
-			std::cerr << "curl_easy_init failed" << std::endl;
-			return -1;
-		}
-
-		std::string postdata = json_online_status_batch_encode();
-		std::cout << "[批量状态] data: " << postdata << std::endl;
-
-		struct curl_slist* headers = NULL;
-		headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8");
-		curl_easy_setopt(curl, CURLOPT_URL, req_url.c_str());
-		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-    
-    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);//3L
-		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 增加超时时间
-    
-		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str());
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
-		CURLcode last_res = curl_easy_perform(curl);
-		if (last_res == CURLE_OK) {
-			std::cout << "[批量状态] success: " << response << std::endl;
-			curl_slist_free_all(headers);
-			curl_easy_cleanup(curl);
-			return 0;
-		}
-
-		std::cerr << "[批量状态] Error: " << curl_easy_strerror(last_res) << std::endl;
-		curl_slist_free_all(headers);
-		curl_easy_cleanup(curl);
-		
-		if (last_res != CURLE_OPERATION_TIMEDOUT && last_res != CURLE_COULDNT_CONNECT) {
-			break;
-		}
-		response.clear();
-	}
-
-	return -1;
-}
-
-// ============================================
-
-// ==================== 缺失的函数实现 ====================
-std::string get_new_timestamp() {
-	auto now = std::chrono::system_clock::now();
-	auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
-	return std::to_string(timestamp);
-}
-
-std::string get_format_time_s() {
-	time_t now = time(nullptr);
-	struct tm tm_now;
-	localtime_r(&now, &tm_now);
-	char buf[64];
-	strftime(buf, sizeof(buf), "%Y年%m月%d日%H:%M:%S", &tm_now);
-	return std::string(buf);
-}
-
-std::string calculate_md5(const std::string& input) {
-	char md5_hex[33];
-	md5_hexEx1(input.c_str(), md5_hex);
-	return std::string(md5_hex);
-}
-
-
-bool lib_curl_image_upload_request(std::shared_ptr<CarPlateInfo> plate_info, GTYPE gtype)
-{
-	CaptureInfo* cap_info = plate_info->cap_info_copy.get();
-	if (!cap_info) {
-		std::cerr << "lib_curl_image_upload_request: cap_info为空" << std::endl;
-		return false;
-	}
-
-	auto upload_start = std::chrono::steady_clock::now();
-	std::string timestamp = get_new_timestamp();
-	std::string sign = calculate_sign(app_key, app_secret, timestamp);
-	std::string req_url = "";
-
-	if (TestFlag == 1)
-	{
-		req_url = app_api + "uploadImgTest?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-	else
-	{
-		req_url = app_api + "uploadImg?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-
-	std::string tb_no = plate_info->tb_num;
-	std::string f_type = (g_wType == 3) ? "2" : "1";
-	std::string d_type = std::to_string(cap_info->dtype);
-	std::string g_type = std::to_string(gtype);
-	std::string filePath = "";
-
-	if (gtype == POSITION_LO)
-	{
-		if (plate_info->pic_path_front.empty()) {
-			std::cerr << "pic_path_front 为空,跳过上传" << std::endl;
-			return false;
-		}
-		filePath = std::string(pathComm + "PlateJPG/") + plate_info->pic_path_front;
-	}
-	else if (gtype == POSITION_HI)
-	{
-		if (plate_info->pic_path_side.empty()) {
-			std::cerr << "pic_path_side 为空,跳过上传" << std::endl;
-			return false;
-		}
-		filePath = std::string(pathComm + "PlateJPG/") + plate_info->pic_path_side;
-	}
-
-	std::cout << "dtype:" << d_type << " gtype:" << g_type << " file:" << filePath << std::endl;
-	if (filePath.empty()) {
-		std::cerr << "文件路径为空,跳过上传" << std::endl;
-		return false;
-	}
-
-	std::string response;
-	// v41.2: MAX_RETRY=4 表示 初始请求 + 3次重试 = 4次请求
-	const int MAX_RETRY = 4;
-	for (int retry = 0; retry < MAX_RETRY; retry++) {
-		if (retry > 0) {
-			// 指数退让:2^retry = 2, 4, 8 秒
-			int backoff_seconds = (1 << retry);
-			std::cerr << "[图片上传] 重试第" << retry << "次(共" << MAX_RETRY-1 << "次),等待" << backoff_seconds << "秒..." << std::endl;
-			std::this_thread::sleep_for(std::chrono::seconds(backoff_seconds));
-		}
-
-		CURL* curl = curl_easy_init();
-		if (!curl) {
-			std::cerr << "curl_easy_init failed" << std::endl;
-			return false;
-		}
-
-		curl_mime* mime = curl_mime_init(curl);
-		if (!mime) {
-			std::cerr << "curl_mime_init failed" << std::endl;
-			curl_easy_cleanup(curl);
-			return false;
-		}
-
-		curl_mimepart* part = nullptr;
-		part = curl_mime_addpart(mime);
-		curl_mime_name(part, "tbNo");
-		curl_mime_data(part, tb_no.c_str(), CURL_ZERO_TERMINATED);
-
-		part = curl_mime_addpart(mime);
-		curl_mime_name(part, "fType");
-		curl_mime_data(part, f_type.c_str(), CURL_ZERO_TERMINATED);
-
-		part = curl_mime_addpart(mime);
-		curl_mime_name(part, "dType");
-		curl_mime_data(part, d_type.c_str(), CURL_ZERO_TERMINATED);
-
-		part = curl_mime_addpart(mime);
-		curl_mime_name(part, "gType");
-		curl_mime_data(part, g_type.c_str(), CURL_ZERO_TERMINATED);
-
-		std::string file_display_name = filePath;
-		size_t last_slash = filePath.find_last_of("/");
-		if (last_slash != std::string::npos) {
-			file_display_name = filePath.substr(last_slash + 1);
-		}
-
-		part = curl_mime_addpart(mime);
-		curl_mime_name(part, "file");
-		curl_mime_filedata(part, filePath.c_str());
-		curl_mime_filename(part, file_display_name.c_str());
-		curl_mime_type(part, "image/jpeg");
-
-		curl_easy_setopt(curl, CURLOPT_URL, req_url.c_str());
-		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-		curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
-		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
-		curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
-		CURLcode last_res = curl_easy_perform(curl);
-		if (last_res == CURLE_OK) {
-			int parse_res = parse_upload_image_response(response);
-			auto upload_end = std::chrono::steady_clock::now();
-			int duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(upload_end - upload_start).count();
-			g_metrics.record_upload_time(duration_ms);
-			curl_mime_free(mime);
-			curl_easy_cleanup(curl);
-			return (parse_res == 0);
-		}
-
-		std::cout << "[图片上传] Error curl_easy_perform" << std::endl;
-		g_metrics.record_error(classify_upload_error(last_res, 0));
-
-		if (last_res != CURLE_OPERATION_TIMEDOUT && last_res != CURLE_COULDNT_CONNECT) {
-			curl_mime_free(mime);
-			curl_easy_cleanup(curl);
-			return false;
-		}
-
-		curl_mime_free(mime);
-		curl_easy_cleanup(curl);
-		response.clear();
-	}
-
-	auto upload_end = std::chrono::steady_clock::now();
-	int duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(upload_end - upload_start).count();
-	g_metrics.record_upload_time(duration_ms);
-	return false;
-}
-
-
-int lib_curl_create_bill_request(std::shared_ptr<CarPlateInfo> plate_info, std::string& tb_num_out)
-{
-    tb_num_out.clear();
-	std::string response;
-	std::string postdata = json_create_bill_encode(plate_info);
-	std::string timestamp = get_new_timestamp();
-	std::string sign = calculate_sign(app_key, app_secret, timestamp);
-	std::string req_url = "";
-    CaptureInfo* plate_cap_info = plate_info->cap_info_copy.get();
-    if (!plate_cap_info) {
-        std::cerr << "lib_curl_create_bill_request: cap_info为空" << std::endl;
-        return static_cast<int>(ErrorCode::INVALID_PARAM);
-    }
-    bool is_in = (plate_cap_info->dtype == STATION_IN);
-    bool is_business_error = false;
-
-	if (TestFlag == 1)
-	{
-		req_url = app_api + "createBillTest?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-	else
-	{
-		if (g_wType == 1)
-		{
-			req_url = app_api + "createBill?app_key=" +
-				app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-		}
-		else if (g_wType == 3)
-		{
-			req_url = app_api + "consumeBill?app_key=" +
-				app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-		}
-		else
-		{
-			req_url = app_api + "createBill?app_key=" +
-				app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-		}
-	}
-
-	std::cout << "url: " << req_url << std::endl;
-	std::cout << "data: " << postdata << std::endl;
-
-	const int MAX_RETRY = 3;  // 初始请求 + 3次重试 = 4次请求
-	for (int retry = 0; retry <= MAX_RETRY; retry++) {
-		int backoff_seconds = (retry > 0) ? (1 << retry) : 0;  // 指数退让: 2, 4, 8秒
-		if (retry > 0) {
-			std::cerr << "[创建工单] 重试第" << retry << "次(共" << MAX_RETRY << "次),等待" << backoff_seconds << "秒..." << std::endl;
-			sleep(backoff_seconds);
-		}
-
-		CURL* curl = curl_easy_init();
-		if (!curl) {
-			std::cerr << "curl_easy_init failed" << std::endl;
-			return static_cast<int>(ErrorCode::NETWORK_ERROR);
-		}
-
-		curl_easy_setopt(curl, CURLOPT_URL, req_url.c_str());
-		struct curl_slist* headers = NULL;
-		headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8");
-		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-		curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
-		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
-		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str());
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
-		CURLcode last_res = curl_easy_perform(curl);
-		if (last_res == CURLE_OK) {
-			int parse_res = parse_create_bill_response(response, plate_info->code, is_in, tb_num_out);
-			if (parse_res == static_cast<int>(ErrorCode::SUCCESS)) {
-				curl_slist_free_all(headers);
-				curl_easy_cleanup(curl);
-                increment_bill_count(plate_info->code, is_in);
-				return static_cast<int>(ErrorCode::SUCCESS);
-			}
-			if (parse_res == static_cast<int>(ErrorCode::BUSINESS_ERROR)) {
-				std::cerr << "[创建工单] 业务错误,不再重试" << std::endl;
-                is_business_error = true;
-				curl_slist_free_all(headers);
-				curl_easy_cleanup(curl);
-				break;
-			}
-			std::cerr << "[创建工单] 解析响应失败,准备重试" << std::endl;
-			curl_slist_free_all(headers);
-			curl_easy_cleanup(curl);
-			response.clear();
-			continue;
-		}
-
-		std::cout << "[创建工单] Error curl_easy_perform" << std::endl;
-		g_metrics.record_createbill(false);
-		g_metrics.record_error(classify_upload_error(last_res, 0));
-
-		if (last_res != CURLE_OPERATION_TIMEDOUT && last_res != CURLE_COULDNT_CONNECT) {
-			curl_slist_free_all(headers);
-			curl_easy_cleanup(curl);
-			return static_cast<int>(ErrorCode::NETWORK_ERROR);
-		}
-
-		curl_slist_free_all(headers);
-		curl_easy_cleanup(curl);
-		response.clear();
-	}
-
-    if (!is_business_error) {
-        std::cerr << "[创建工单] 所有重试都失败" << std::endl;
-	    g_metrics.record_createbill(false);
-    } else {
-        g_metrics.record_createbill(false, true);
-    }
-	return is_business_error ? static_cast<int>(ErrorCode::BUSINESS_ERROR) : static_cast<int>(ErrorCode::NETWORK_ERROR);
-}
-
-
-int lib_curl_online_status_request(CaptureInfo* cap_info)
-{
-	std::string response;
-	std::string timestamp = get_new_timestamp();
-	std::string sign = calculate_sign(app_key, app_secret, timestamp);
-	std::string req_url = "";
-
-	if (TestFlag == 1)
-	{
-		req_url = app_api + "onlineStatusTest?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-	else
-	{
-		req_url = app_api + "onlineStatus?app_key=" +
-			app_key + "&sign=" + sign + "&timestamp=" + timestamp;
-	}
-
-	// v41.2优化:指数退让重试 (2秒、4秒、8秒),最多3次
-	// v41.2: MAX_RETRY=4 表示 初始请求 + 3次重试 = 4次请求
-	const int MAX_RETRY = 4;
-	for (int retry = 0; retry < MAX_RETRY; retry++) {
-		if (retry > 0) {
-			// 指数退让:2^retry = 2, 4, 8 秒
-			int backoff_seconds = (1 << retry);
-			std::cerr << "[在线状态] 重试第" << retry << "次(共" << MAX_RETRY-1 << "次),等待" << backoff_seconds << "秒..." << std::endl;
-			std::this_thread::sleep_for(std::chrono::seconds(backoff_seconds));
-		}
-
-		CURL* curl = curl_easy_init();
-		if (!curl) {
-			std::cerr << "curl_easy_init failed" << std::endl;
-			return -1;
-		}
-
-		std::string postdata = json_online_status_encode(cap_info);
-		std::cout << "data: " << postdata << std::endl;
-
-		curl_easy_setopt(curl, CURLOPT_URL, req_url.c_str());
-		struct curl_slist* headers = NULL;
-		headers = curl_slist_append(headers, "Content-Type: application/json;charset=UTF-8");
-		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-		curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-		curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
-		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
-		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str());
-		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
-		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
-
-		CURLcode last_res = curl_easy_perform(curl);
-		if (last_res == CURLE_OK) {
-			parse_online_status_response(response);
-			curl_slist_free_all(headers);
-			curl_easy_cleanup(curl);
-			return 0;
-		}
-
-		std::cout << "[在线状态] Error curl_easy_perform" << std::endl;
-		if (last_res != CURLE_OPERATION_TIMEDOUT && last_res != CURLE_COULDNT_CONNECT) {
-			curl_slist_free_all(headers);
-			curl_easy_cleanup(curl);
-			return -1;
-		}
-
-		curl_slist_free_all(headers);
-		curl_easy_cleanup(curl);
-		response.clear();
-	}
-
-	return -1;
-}

+ 0 - 247
src/system_monitor1.cpp

@@ -1,247 +0,0 @@
-/**
- * system_monitor.cpp - 系统资源监控实现
- * fix24 v24: 读取 /proc, /sys, statvfs 获取系统信息
- */
-#include "system_monitor.h"
-#include "common.h"
-
-#include <fstream>
-#include <sstream>
-#include <cstring>
-#include <cstdlib>
-#include <cstdio>
-#include <unistd.h>
-#include <sys/statvfs.h>
-#include <sys/sysinfo.h>
-#include <dirent.h>
-#include <algorithm>
-
-namespace sysmon {
-
-// ========== CPU 使用率(两次采样差值) ==========
-double get_cpu_usage(int sample_ms) {
-    // 读取 /proc/stat 两次,计算差值
-    auto read_cpu_times = []() -> std::vector<long long> {
-        std::ifstream f("/proc/stat");
-        std::string line;
-        std::getline(f, line);  // "cpu  user nice system idle iowait irq softirq steal"
-        std::istringstream iss(line);
-        std::string cpu_label;
-        iss >> cpu_label;
-        std::vector<long long> times;
-        long long val;
-        while (iss >> val) times.push_back(val);
-        return times;
-    };
-
-    auto t1 = read_cpu_times();
-    usleep(sample_ms * 1000);
-    auto t2 = read_cpu_times();
-
-    if (t1.size() < 4 || t2.size() < 4) return 0.0;
-
-    // 补齐到相同长度
-    while (t1.size() < t2.size()) t1.push_back(0);
-    while (t2.size() < t1.size()) t2.push_back(0);
-
-    long long total1 = 0, total2 = 0, idle1 = 0, idle2 = 0;
-    for (size_t i = 0; i < t1.size(); i++) total1 += t1[i];
-    for (size_t i = 0; i < t2.size(); i++) total2 += t2[i];
-    // idle = index 3 (idle) + index 4 (iowait) if present
-    idle1 = t1[3];
-    if (t1.size() > 4) idle1 += t1[4];
-    idle2 = t2[3];
-    if (t2.size() > 4) idle2 += t2[4];
-
-    long long total_diff = total2 - total1;
-    long long idle_diff = idle2 - idle1;
-    if (total_diff <= 0) return 0.0;
-    return (double)(total_diff - idle_diff) / total_diff * 100.0;
-}
-
-// ========== CPU 温度 ==========
-double get_cpu_temperature() {
-    // 尝试多个温度传感器路径
-    const char* paths[] = {
-        "/sys/class/thermal/thermal_zone0/temp",
-        "/sys/class/hwmon/hwmon0/temp1_input",
-        "/sys/devices/virtual/thermal/thermal_zone0/temp",
-        nullptr
-    };
-    for (int i = 0; paths[i]; i++) {
-        std::ifstream f(paths[i]);
-        if (f.is_open()) {
-            long long millideg = 0;
-            f >> millideg;
-            if (millideg > 0) {
-                return millideg / 1000.0;
-            }
-        }
-    }
-    return -1.0;  // 温度不可用
-}
-
-// ========== 内存信息 ==========
-void get_memory_info(long &total_mb, long &used_mb, long &avail_mb, double &percent) {
-    total_mb = used_mb = avail_mb = 0;
-    percent = 0.0;
-
-    std::ifstream f("/proc/meminfo");
-    if (!f.is_open()) return;
-
-    long mem_total = 0, mem_free = 0, mem_available = 0, buffers = 0, cached = 0;
-    std::string line;
-    while (std::getline(f, line)) {
-        if (line.find("MemTotal:") == 0) sscanf(line.c_str(), "MemTotal: %ld kB", &mem_total);
-        else if (line.find("MemFree:") == 0) sscanf(line.c_str(), "MemFree: %ld kB", &mem_free);
-        else if (line.find("MemAvailable:") == 0) sscanf(line.c_str(), "MemAvailable: %ld kB", &mem_available);
-        else if (line.find("Buffers:") == 0) sscanf(line.c_str(), "Buffers: %ld kB", &buffers);
-        else if (line.find("Cached:") == 0 && line.find("SwapCached") == std::string::npos)
-            sscanf(line.c_str(), "Cached: %ld kB", &cached);
-    }
-
-    total_mb = mem_total / 1024;
-    avail_mb = mem_available / 1024;
-    used_mb = total_mb - avail_mb;
-    if (total_mb > 0) {
-        percent = (double)used_mb / total_mb * 100.0;
-    }
-}
-
-// ========== 磁盘信息 ==========
-void get_disk_info(const std::string &path, long &total_gb, long &used_gb, long &avail_gb, double &percent) {
-    total_gb = used_gb = avail_gb = 0;
-    percent = 0.0;
-
-    struct statvfs stat;
-    if (statvfs(path.c_str(), &stat) != 0) return;
-
-    long long total = (long long)stat.f_blocks * stat.f_frsize;
-    long long avail = (long long)stat.f_bavail * stat.f_frsize;
-    long long used = total - ((long long)stat.f_bfree * stat.f_frsize);
-
-    total_gb = total / (1024LL * 1024 * 1024);
-    used_gb = used / (1024LL * 1024 * 1024);
-    avail_gb = avail / (1024LL * 1024 * 1024);
-    if (total > 0) {
-        percent = (double)used / total * 100.0;
-    }
-}
-
-// ========== 系统启动时间 ==========
-std::string get_uptime_str(long &seconds) {
-    struct sysinfo si;
-    if (sysinfo(&si) != 0) {
-        seconds = 0;
-        return "unknown";
-    }
-    seconds = si.uptime;
-    long days = seconds / 86400;
-    long hours = (seconds % 86400) / 3600;
-    long mins = (seconds % 3600) / 60;
-
-    std::ostringstream oss;
-    if (days > 0) oss << days << "天";
-    oss << hours << "小时" << mins << "分钟";
-    return oss.str();
-}
-
-// ========== 负载均值 ==========
-void get_load_avg(double &load1, double &load5, double &load15) {
-    double loads[3] = {0, 0, 0};
-    if (getloadavg(loads, 3) != -1) {
-        load1 = loads[0];
-        load5 = loads[1];
-        load15 = loads[2];
-    } else {
-        load1 = load5 = load15 = 0;
-    }
-}
-
-// ========== 进程资源占用 ==========
-void get_process_resource(int pid, long &rss_mb, double &cpu_percent) {
-    rss_mb = 0;
-    cpu_percent = 0.0;
-
-    char path[64];
-    snprintf(path, sizeof(path), "/proc/%d/statm", pid);
-    std::ifstream f(path);
-    if (!f.is_open()) return;
-
-    long pages = 0;
-    f >> pages;  // 第一个字段是 total program size
-    f >> pages;  // 第二个字段是 resident set size
-    rss_mb = pages * (sysconf(_SC_PAGESIZE) / 1024) / 1024;
-}
-
-// ========== 格式化文件大小 ==========
-std::string format_size(long mb) {
-    if (mb >= 1024) {
-        char buf[32];
-        snprintf(buf, sizeof(buf), "%.1f GB", mb / 1024.0);
-        return buf;
-    }
-    return std::to_string(mb) + " MB";
-}
-
-// ========== CPU 核心数 ==========
-static int get_cpu_core_count() {
-    return sysconf(_SC_NPROCESSORS_ONLN);
-}
-
-// ========== 完整系统状态快照 ==========
-SystemStatus get_system_status() {
-    SystemStatus s;
-    memset(&s, 0, sizeof(s));
-
-    // CPU
-    s.cpu_core_count = get_cpu_core_count();
-    s.cpu_usage_percent = get_cpu_usage(100);
-    get_load_avg(s.cpu_load_1min, s.cpu_load_5min, s.cpu_load_15min);
-
-    // 内存
-    get_memory_info(s.mem_total_mb, s.mem_used_mb, s.mem_available_mb, s.mem_usage_percent);
-
-    // 温度
-    s.cpu_temp = get_cpu_temperature();
-    s.temp_available = (s.cpu_temp > 0);
-
-    // 磁盘(根分区)
-    get_disk_info("/", s.disk_total_gb, s.disk_used_gb, s.disk_available_gb, s.disk_usage_percent);
-
-    // 系统信息
-    char hostname_buf[256] = {0};
-    gethostname(hostname_buf, sizeof(hostname_buf) - 1);
-    s.hostname = hostname_buf;
-
-    // 内核版本
-    {
-        std::ifstream f("/proc/version");
-        if (f.is_open()) {
-            std::string line;
-            std::getline(f, line);
-            // 提取 "Linux version X.Y.Z-..." 部分
-            size_t pos = line.find("version ");
-            if (pos != std::string::npos) {
-                pos += 8;
-                size_t end = line.find(' ', pos);
-                if (end != std::string::npos) {
-                    s.kernel_version = line.substr(pos, end - pos);
-                } else {
-                    s.kernel_version = line.substr(pos, 20);
-                }
-            }
-        }
-    }
-
-    // 启动时间
-    s.uptime_string = get_uptime_str(s.uptime_seconds);
-
-    // 进程信息
-    s.plate_rec_pid = getpid();
-    get_process_resource(s.plate_rec_pid, s.plate_rec_rss_mb, s.plate_rec_cpu_percent);
-
-    return s;
-}
-
-}  // namespace sysmon

+ 0 - 1732
src/web_server1.cpp

@@ -1,1732 +0,0 @@
-/**
- * web_server.cpp — Web服务实现
- * v43.2 fix18: 4路独立视频页面 + MJPEG流 + ROI手动绘制 + P0重复照片修复
- */
-#include "web_server.h"
-#include "database.h"
-#include "station_lock.h"
-#include "network_client.h"
-#include "feishu_client.h"
-#include "weight_scale.h"
-#include "utils.h"
-#include "rtsp_capture.h"
-#define CPPHTTPLIB_OPENSSL_SUPPORT  // fix24-v12: 启用HTTPS/SSL支持
-#include "httplib.h"
-#include <openssl/ssl.h>  // fix24-v16: 显式包含OpenSSL头文件以使用SSL_CTX API
-#include <cjson/cJSON.h>
-#include <iostream>
-#include <sstream>
-#include <fstream>
-#include <map>
-#include <set>
-
-// ==================== fix18新增:4路视频辅助函数 ====================
-
-// 根据stream参数获取摄像头帧
-static cv::Mat get_frame_by_stream(const std::string& stream) {
-    if (stream == "in-low") {
-        return plate_rec_app.capture_front_in->getFrame();
-    } else if (stream == "in-high") {
-        return plate_rec_app.capture_side_in->getFrame();
-    } else if (stream == "out-low") {
-        // ✅ fix24-v9: 共享模式下出站未创建独立摄像头,复用进站摄像头画面
-        if (plate_rec_app.capture_front_out) {
-            return plate_rec_app.capture_front_out->getFrame();
-        }
-        return plate_rec_app.capture_front_in->getFrame();
-    } else if (stream == "out-high") {
-        return plate_rec_app.capture_side_out->getFrame();
-    }
-    return cv::Mat();
-}
-
-// 根据stream参数获取ROI配置(高位摄像头返回nullptr)
-static const ROIConfig* get_roi_by_stream(const std::string& stream) {
-    if (stream == "in-low") return &g_roi_in;
-    if (stream == "out-low") return &g_roi_out;
-    return nullptr;
-}
-
-// ✅ fix24-v10: stream标签使用英文(cv::putText不支持中文会显示问号)
-// 中文标签由前端CSS overlay显示,画面叠加英文缩写供截图/裸流场景识别
-static std::string get_stream_label(const std::string& stream) {
-    if (stream == "in-low") return "IN-LOW";
-    if (stream == "in-high") return "IN-HIGH";
-    if (stream == "out-low") return "OUT-LOW";
-    if (stream == "out-high") return "OUT-HIGH";
-    return "UNKNOWN";
-}
-
-// ==================== HTML转义 ====================
-
-std::string escape_html(const std::string& str) {
-	std::string result;
-	for (char c : str) {
-		switch (c) {
-		case '&': result += "&amp;"; break;
-		case '<': result += "&lt;"; break;
-		case '>': result += "&gt;"; break;
-		case '"': result += "&quot;"; break;
-		case '\'': result += "&#39;"; break;
-		default: result += c;
-		}
-	}
-	return result;
-}
-
-std::string generate_monitor_report() {
-	std::ostringstream json;
-	json << "{";
-	json << "\"createbill_success_rate\":" << g_metrics.get_createbill_success_rate() << ",";
-	json << "\"createbill_total\":" << g_metrics.createbill_total.load() << ",";
-	json << "\"createbill_success\":" << g_metrics.createbill_success.load() << ",";
-	json << "\"createbill_business_error\":" << g_metrics.createbill_business_error.load() << ",";
-    json << "\"createbill_quota_exceeded\":" << g_metrics.createbill_quota_exceeded.load() << ",";
-	json << "\"avg_upload_time\":" << g_metrics.get_avg_upload_time() << ",";
-	json << "\"avg_infer_time\":" << g_metrics.get_avg_inference_time() << ",";
-	json << "\"last_infer_time\":" << g_metrics.get_last_inference_time() << ",";
-	json << "\"time_window_hits\":" << g_metrics.time_window_hits.load() << ",";
-	json << "\"in_cache_hit_rate\":" << g_metrics.get_cache_hit_rate(true) << ",";
-	json << "\"out_cache_hit_rate\":" << g_metrics.get_cache_hit_rate(false) << ",";
-	json << "\"in_cache_hits\":" << g_metrics.in_cache_hits.load() << ",";
-	json << "\"in_cache_misses\":" << g_metrics.in_cache_misses.load() << ",";
-	json << "\"out_cache_hits\":" << g_metrics.out_cache_hits.load() << ",";
-	json << "\"out_cache_misses\":" << g_metrics.out_cache_misses.load() << ",";
-	json << "\"in_station_count\":" << g_metrics.in_station_count.load() << ",";
-	json << "\"out_station_count\":" << g_metrics.out_station_count.load() << ",";
-	json << "\"total_records\":" << db_get_total_count() << ",";
-	json << "\"failed_records\":" << db_get_failed_count() << ",";
-	json << "\"feishu_success_count\":" << g_metrics.feishu_success_count.load() << ",";
-	json << "\"uptime\":\"" << escape_html(g_metrics.get_uptime_string()) << "\",";
-	json << "\"upload_errors\":" << g_metrics.upload_errors.load() << ",";
-	json << "\"network_timeouts\":" << g_metrics.network_timeouts.load() << ",";
-	json << "\"server_errors\":" << g_metrics.server_errors.load() << ",";
-    json << "\"photo_lo_success\":" << g_metrics.photo_lo_success.load() << ",";
-    json << "\"photo_hi_success\":" << g_metrics.photo_hi_success.load() << ",";
-    json << "\"photo_group_success\":" << g_metrics.photo_group_success.load() << ",";
-    json << "\"db_duplicate_skip_count\":" << g_metrics.db_duplicate_skip_count.load() << ",";
-    json << "\"dir_create_error_count\":" << g_metrics.dir_create_error_count.load() << ",";
-    json << "\"db_insert_error_count\":" << g_metrics.db_insert_error_count.load() << ",";
-    json << "\"db_insert_retry_success\":" << g_metrics.db_insert_retry_success.load() << ",";
-    json << "\"permanent_failure_count\":" << g_metrics.permanent_failure_count.load() << ",";
-    json << "\"alert_sent_count\":" << g_metrics.alert_sent_count.load() << ",";
-    json << "\"main_loop_block_count\":" << g_metrics.main_loop_block_count.load() << ",";
-    json << "\"main_loop_max_block_ms\":" << g_metrics.main_loop_max_block_ms.load() << ",";
-    json << "\"station_lock_in_count\":" << g_metrics.station_lock_in_count.load() << ",";
-    json << "\"station_lock_out_count\":" << g_metrics.station_lock_out_count.load() << ",";
-    json << "\"station_in_block_count\":" << g_metrics.station_in_block_count.load() << ",";
-    json << "\"station_out_block_count\":" << g_metrics.station_out_block_count.load() << ",";
-    json << "\"station_timeout_cleanup_count\":" << g_metrics.station_timeout_cleanup_count.load() << ",";
-    // ✅ v43新增:ROI指标
-    json << "\"roi_in_crop_count\":" << g_metrics.roi_in_crop_count.load() << ",";
-    json << "\"roi_out_crop_count\":" << g_metrics.roi_out_crop_count.load() << ",";
-    json << "\"roi_in_fullframe_count\":" << g_metrics.roi_in_fullframe_count.load() << ",";
-    json << "\"roi_out_fullframe_count\":" << g_metrics.roi_out_fullframe_count.load() << ",";
-    // ✅ v43新增:飞书/解码/Web指标
-    json << "\"feishu_skip_count\":" << g_metrics.feishu_skip_count.load() << ",";
-    json << "\"drm_decode_count\":" << g_metrics.drm_decode_count.load() << ",";
-    json << "\"soft_decode_count\":" << g_metrics.soft_decode_count.load() << ",";
-    json << "\"web_retry_count\":" << g_metrics.web_retry_count.load() << ",";
-    json << "\"web_retry_success_count\":" << g_metrics.web_retry_success_count.load() << ",";
-    // ✅ v43新增:配置状态
-    json << "\"alternating_merge_enabled\":" << (g_alternating_merge_enabled ? "true" : "false") << ",";
-    json << "\"emergency_alert_suppressed\":" << (g_suppress_emergency_alert ? "true" : "false") << ",";
-    json << "\"roi_in_enabled\":" << (g_roi_in.enabled ? "true" : "false") << ",";
-    json << "\"roi_out_enabled\":" << (g_roi_out.enabled ? "true" : "false") << ",";
-    json << "\"in_out_interval_sec\":" << g_in_out_interval_sec;
-	json << "}";
-	return json.str();
-}
-
-std::string generate_message_list_json(int page, const std::string& search_keyword) {
-	const int page_size = 20;
-	std::vector<DbRecord> records = db_get_records(page, page_size, search_keyword);
-	std::ostringstream json;
-	json << "{\"page\":" << page << ",\"page_size\":" << page_size;
-	json << ",\"records\":[";
-	bool first = true;
-	for (const auto& rec : records) {
-		if (!first) json << ",";
-		first = false;
-		json << "{";
-		json << "\"id\":" << rec.id << ",";
-		// ✅ fix24-v13: capture_time存在时用抓拍时刻格式化,否则用create_time
-		{
-			std::string display_time = rec.create_time;
-			if (rec.capture_time > 0) {
-				struct tm tm_buf;
-				struct tm* tm_ptr = localtime_r(&rec.capture_time, &tm_buf);
-				if (tm_ptr) {
-					char buf[64];
-					strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_ptr);
-					display_time = buf;
-				}
-			}
-			json << "\"create_time\":\"" << escape_html(display_time) << "\",";
-		}
-		json << "\"capture_time\":" << rec.capture_time << ",";
-		json << "\"plate_number\":\"" << escape_html(rec.plate_number) << "\",";
-		json << "\"tb_num\":\"" << escape_html(rec.tb_num) << "\",";
-		json << "\"station_type\":" << rec.station_type << ",";
-		json << "\"lo_photo_path\":\"" << escape_html(rec.lo_photo_path) << "\",";
-		json << "\"hi_photo_path\":\"" << escape_html(rec.hi_photo_path) << "\",";
-		json << "\"lo_upload_status\":" << rec.lo_upload_status << ",";
-		json << "\"hi_upload_status\":" << rec.hi_upload_status << ",";
-		json << "\"feishu_status\":" << rec.feishu_status << ",";
-		json << "\"retry_count\":" << rec.retry_count;
-		json << "}";
-	}
-	json << "]}";
-	return json.str();
-}
-
-bool is_safe_file_path(const std::string& base_dir, const std::string& user_path, std::string& real_path) {
-    if (user_path.find("..") != std::string::npos || user_path.find("/") != std::string::npos || user_path.find("\\") != std::string::npos) {
-        std::cerr << "[路径校验] 非法字符: " << user_path << std::endl;
-        return false;
-    }
-
-    std::string full_path = base_dir + "/" + user_path;
-    
-    char abs_path[PATH_MAX];
-    if (realpath(full_path.c_str(), abs_path) == nullptr) {
-        std::cerr << "[路径校验] 转换绝对路径失败: " << full_path << " - " << strerror(errno) << std::endl;
-        return false;
-    }
-    real_path = abs_path;
-
-    std::string base_abs_path;
-    if (realpath(base_dir.c_str(), abs_path) == nullptr) {
-        std::cerr << "[路径校验] 基础目录不存在: " << base_dir << std::endl;
-        return false;
-    }
-    base_abs_path = abs_path;
-    if (base_abs_path.back() != '/') base_abs_path += '/';
-
-    if (real_path.find(base_abs_path) != 0) {
-        std::cerr << "[路径校验] 非法路径: " << user_path << " (尝试访问基础目录外的文件)" << std::endl;
-        return false;
-    }
-
-    return true;
-}
-
-// fix22: 前向声明
-static std::string generate_config_page();
-static std::string generate_locks_page();
-
-void start_web_server() {
-	if (g_web_server_running.load()) return;
-	g_web_server_running = true;
-	g_metrics.start_time = time(NULL);
-	
-	// ✅ fix24-v12: 根据SSL配置创建HTTP或HTTPS服务器
-	if (g_ssl_enabled && !g_ssl_cert_path.empty() && !g_ssl_key_path.empty()) {
-		// 验证证书和私钥文件是否存在
-		struct stat st;
-		if (stat(g_ssl_cert_path.c_str(), &st) != 0) {
-			std::cerr << "[SSL] 证书文件不存在: " << g_ssl_cert_path << ",回退到HTTP模式" << std::endl;
-			g_web_server = std::make_unique<httplib::Server>();
-		} else if (stat(g_ssl_key_path.c_str(), &st) != 0) {
-			std::cerr << "[SSL] 私钥文件不存在: " << g_ssl_key_path << ",回退到HTTP模式" << std::endl;
-			g_web_server = std::make_unique<httplib::Server>();
-		} else {
-			// ✅ fix24-v16: SSL性能优化 - 使用自定义ctx_setup回调,启用会话缓存和快速密码套件
-			auto ssl_setup = [](void* ctx) -> bool {
-				SSL_CTX* ssl_ctx = static_cast<SSL_CTX*>(ctx);
-				// 启用SSL会话缓存,减少重复握手开销
-				SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER);
-				SSL_CTX_set_timeout(ssl_ctx, 600); // 会话缓存10分钟
-				// 优先使用ECDHE+AESGCM和ChaCha20(AESGCM适合x86,ChaCha20适合ARM/无AES-NI)
-				SSL_CTX_set_cipher_list(ssl_ctx,
-					"ECDHE-ECDSA-AES128-GCM-SHA256:"
-					"ECDHE-RSA-AES128-GCM-SHA256:"
-					"ECDHE-ECDSA-CHACHA20-POLY1305:"
-					"ECDHE-RSA-CHACHA20-POLY1305:"
-					"ECDHE-ECDSA-AES256-GCM-SHA384:"
-					"ECDHE-RSA-AES256-GCM-SHA384:"
-					"DHE-RSA-AES128-GCM-SHA256:"
-					"DHE-RSA-AES256-GCM-SHA384");
-				// 启用EC曲线自动选择(优先X25519,握手快)
-				SSL_CTX_set_ecdh_auto(ssl_ctx, 1);
-				return true;
-			};
-			auto ssl_server = std::make_unique<httplib::SSLServer>(ssl_setup);
-			// 手动加载证书(因为用了callback构造器,需要单独设置证书)
-			if (ssl_server->is_valid()) {
-				// 使用update_certs_pem加载证书(需要从文件读取)
-				std::ifstream cert_file(g_ssl_cert_path);
-				std::ifstream key_file(g_ssl_key_path);
-				if (cert_file.is_open() && key_file.is_open()) {
-					std::string cert_pem((std::istreambuf_iterator<char>(cert_file)),
-										  std::istreambuf_iterator<char>());
-					std::string key_pem((std::istreambuf_iterator<char>(key_file)),
-										 std::istreambuf_iterator<char>());
-					if (ssl_server->update_certs_pem(cert_pem.c_str(), key_pem.c_str())) {
-						std::cout << "[SSL] HTTPS服务器创建成功(已优化性能),证书: "
-								  << g_ssl_cert_path << " 私钥: " << g_ssl_key_path << std::endl;
-						g_web_server = std::move(ssl_server);
-					} else {
-						std::cerr << "[SSL] 证书加载失败,回退到HTTP模式" << std::endl;
-						g_web_server = std::make_unique<httplib::Server>();
-					}
-				} else {
-					std::cerr << "[SSL] 证书文件读取失败,回退到HTTP模式" << std::endl;
-					g_web_server = std::make_unique<httplib::Server>();
-				}
-			} else {
-				std::cerr << "[SSL] SSL上下文创建失败,回退到HTTP模式" << std::endl;
-				g_web_server = std::make_unique<httplib::Server>();
-			}
-		}
-	} else {
-		g_web_server = std::make_unique<httplib::Server>();
-	}
-	auto& svr = *g_web_server;
-	
-	svr.Get("/", [](const httplib::Request& req, httplib::Response& res) {
-		std::string html = generate_html_page();
-		res.set_content(html, "text/html; charset=utf-8");
-		});
-
-	svr.Get("/api/monitor", [](const httplib::Request& req, httplib::Response& res) {
-		std::string json = generate_monitor_report();
-		res.set_content(json, "application/json; charset=utf-8");
-		});
-
-	svr.Get("/api/station_locks", [](const httplib::Request& req, httplib::Response& res) {
-		std::ostringstream json;
-		json << "{\"locks\":[";
-		
-		std::lock_guard<std::mutex> lock(g_station_cache_mtx);
-		time_t now = time(NULL);
-		bool first = true;
-		
-		for (const auto& pair : g_plate_station_cache) {
-			if (!first) json << ",";
-			first = false;
-			
-			const PlateStationState& state = pair.second;
-			time_t last_time = std::max(state.last_in_time, state.last_out_time);
-			int elapsed = (last_time > 0) ? static_cast<int>(difftime(now, last_time)) : 0;
-			
-			std::string status;
-			if (state.last_out_time == 0 && state.last_in_time > 0) {
-				status = "只进不出";
-			} else if (state.last_in_time == 0 && state.last_out_time > 0) {
-				status = "只出不进";
-			} else {
-				status = "正常交替";
-			}
-			
-			json << "{";
-			json << "\"plate\":\"" << escape_html(pair.first) << "\",";
-			json << "\"current_mode\":" << static_cast<int>(state.current_mode) << ",";
-			json << "\"last_in_time\":" << state.last_in_time << ",";
-			json << "\"last_out_time\":" << state.last_out_time << ",";
-			json << "\"elapsed_seconds\":" << elapsed << ",";
-			// ✅ fix24-v7: 计算并输出剩余等待时间
-			{
-				// 临时释放 station_cache_mtx(避免死锁),因为 get_remaining_wait_time 内部会获取锁
-				// 但此处已在 station_cache_mtx 内,改为直接内联计算
-				int remaining = -1; // -1 表示无法计算或非时间等待
-				if (state.current_mode == StationMode::OUTBOUND_ALLOWED && state.last_in_time > 0) {
-					int el = static_cast<int>(difftime(now, state.last_in_time));
-					remaining = g_in_out_interval_sec - el;
-					if (remaining < 0) remaining = 0;
-				} else if (state.current_mode == StationMode::INBOUND_ALLOWED && state.last_out_time > 0) {
-					int el = static_cast<int>(difftime(now, state.last_out_time));
-					int total_wait = g_in_out_interval_sec + g_time_window_min * 60;
-					remaining = total_wait - el;
-					if (remaining < 0) remaining = 0;
-				} else if (state.current_mode == StationMode::OUTBOUND_ALLOWED && state.last_in_time == 0) {
-					remaining = -2; // 严格拦截:需先出站
-				} else if (state.current_mode == StationMode::INBOUND_ALLOWED && state.last_out_time == 0) {
-					remaining = -2; // 严格拦截:需先进站
-				}
-				json << "\"remaining_seconds\":" << remaining << ",";
-			}
-			json << "\"status\":\"" << status << "\"";
-			json << "}";
-		}
-		
-		json << "]}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	// ✅ fix24: 清除指定车牌的交替锁定记录
-	svr.Post("/api/clear_lock", [](const httplib::Request& req, httplib::Response& res) {
-		std::string body = req.body;
-		// 解析JSON: {"plate":"沪EQ3803"}
-		std::string plate;
-		size_t pos = body.find("\"plate\"");
-		if (pos != std::string::npos) {
-			size_t colon = body.find(":", pos);
-			if (colon != std::string::npos) {
-				size_t q1 = body.find("\"", colon + 1);
-				if (q1 != std::string::npos) {
-					size_t q2 = body.find("\"", q1 + 1);
-					if (q2 != std::string::npos) {
-						plate = body.substr(q1 + 1, q2 - q1 - 1);
-					}
-				}
-			}
-		}
-		
-		if (plate.empty()) {
-			res.set_content("{\"ok\":false,\"error\":\"缺少plate参数\"}", "application/json");
-			return;
-		}
-		
-		bool cleared = clear_station_lock(plate);
-		if (cleared) {
-			res.set_content("{\"ok\":true,\"message\":\"已清除 " + plate + " 的锁定记录\"}", "application/json");
-		} else {
-			res.set_content("{\"ok\":false,\"error\":\"" + plate + " 无锁定记录或交替锁定未启用\"}", "application/json");
-		}
-	});
-
-	svr.Get("/api/messages", [](const httplib::Request& req, httplib::Response& res) {
-		int page = 1;
-		std::string keyword;
-		auto page_it = req.params.find("page");
-		if (page_it != req.params.end()) {
-			page = std::atoi(page_it->second.c_str());
-			if (page < 1) page = 1;
-		}
-		auto kw_it = req.params.find("keyword");
-		if (kw_it != req.params.end()) {
-			keyword = kw_it->second;
-		}
-		std::string json = generate_message_list_json(page, keyword);
-		res.set_content(json, "application/json; charset=utf-8");
-		});
-
-	svr.Post("/api/retry", [](const httplib::Request& req, httplib::Response& res) {
-		std::string body = req.body;
-		std::string id_str;
-		size_t id_pos = body.find("id=");
-		if (id_pos != std::string::npos) {
-			id_pos += 3;
-			size_t end_pos = body.find('&', id_pos);
-			if (end_pos == std::string::npos) end_pos = body.size();
-			id_str = body.substr(id_pos, end_pos - id_pos);
-		}
-		int id = std::atoi(id_str.c_str());
-		// ✅ v43: 使用增强重试(重新获取联单编号+重传失败照片+更新DB)
-		RetryResult result = enhanced_retry_record(id);
-		g_metrics.record_web_retry(result.success);
-		std::ostringstream json;
-		json << "{\"success\":" << (result.success ? "true" : "false") << ",";
-		json << "\"message\":\"" << escape_html(result.message) << "\"";
-		if (!result.tb_num.empty()) {
-			json << ",\"tb_num\":\"" << escape_html(result.tb_num) << "\"";
-		}
-		json << ",\"uploaded_photos\":" << result.uploaded_photos;
-		json << "}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-		});
-
-	// ✅ fix18修改:ROI快照接口支持4路(stream参数)+红框+兼容旧side参数
-	svr.Get("/api/roi/snapshot", [](const httplib::Request& req, httplib::Response& res) {
-		// 支持 stream 参数(新)和 side 参数(旧兼容)
-		std::string stream = "in-low";
-		auto stream_it = req.params.find("stream");
-		auto side_it = req.params.find("side");
-		if (stream_it != req.params.end()) {
-			stream = stream_it->second;
-		} else if (side_it != req.params.end()) {
-			// 旧参数兼容:side=in → in-low, side=out → out-low
-			stream = (side_it->second == "out") ? "out-low" : "in-low";
-		}
-		
-		cv::Mat frame = get_frame_by_stream(stream);
-		if (frame.empty()) {
-			res.status = 404;
-			// ✅ fix24-v10: 添加charset=utf-8,防止浏览器用GBK解码UTF-8中文导致乱码
-			res.set_content("Camera frame unavailable", "text/plain; charset=utf-8");
-			return;
-		}
-		
-		// 低位摄像头画ROI红框(fix18: 绿框→红框)
-		const ROIConfig* roi = get_roi_by_stream(stream);
-		if (roi && !roi->isFullFrame()) {
-			cv::Rect roi_rect = roi->getRect(frame.cols, frame.rows);
-			cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 3);
-			cv::putText(frame, "ROI", cv::Point(roi_rect.x + 5, roi_rect.y + 25),
-					cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
-		}
-		
-		// ✅ fix24-v10: 叠加英文标签(cv::putText不支持中文)
-		std::string label = get_stream_label(stream);
-		cv::putText(frame, label, cv::Point(10, 30),
-				cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
-
-		std::vector<uchar> buf;
-		cv::imencode(".jpg", frame, buf, {cv::IMWRITE_JPEG_QUALITY, 80});
-		res.set_content(std::string(buf.begin(), buf.end()), "image/jpeg");
-	});
-
-	// ✅ fix18修改:ROI配置查询接口,增加4路摄像头分辨率
-	svr.Get("/api/roi/config", [](const httplib::Request& req, httplib::Response& res) {
-		// 获取4路摄像头实际分辨率
-		int in_low_w = 0, in_low_h = 0, in_high_w = 0, in_high_h = 0;
-		int out_low_w = 0, out_low_h = 0, out_high_w = 0, out_high_h = 0;
-		
-		cv::Mat frame;
-		frame = plate_rec_app.capture_front_in->getFrame();
-		if (!frame.empty()) { in_low_w = frame.cols; in_low_h = frame.rows; }
-		frame = plate_rec_app.capture_side_in->getFrame();
-		if (!frame.empty()) { in_high_w = frame.cols; in_high_h = frame.rows; }
-		// ✅ fix24-v9: 共享模式下复用进站分辨率作为出站分辨率
-		if (plate_rec_app.capture_front_out) {
-			frame = plate_rec_app.capture_front_out->getFrame();
-			if (!frame.empty()) { out_low_w = frame.cols; out_low_h = frame.rows; }
-		} else if (plate_rec_app.capture_front_in) {
-			frame = plate_rec_app.capture_front_in->getFrame();
-			if (!frame.empty()) { out_low_w = frame.cols; out_low_h = frame.rows; }
-		}
-		frame = plate_rec_app.capture_side_out->getFrame();
-		if (!frame.empty()) { out_high_w = frame.cols; out_high_h = frame.rows; }
-
-		std::ostringstream json;
-		json << "{";
-		json << "\"roi_in\":{\"x\":" << g_roi_in.x << ",\"y\":" << g_roi_in.y 
-			 << ",\"w\":" << g_roi_in.width << ",\"h\":" << g_roi_in.height 
-			 << ",\"enabled\":" << (g_roi_in.enabled ? "true" : "false") << "},";
-		json << "\"roi_out\":{\"x\":" << g_roi_out.x << ",\"y\":" << g_roi_out.y 
-			 << ",\"w\":" << g_roi_out.width << ",\"h\":" << g_roi_out.height 
-			 << ",\"enabled\":" << (g_roi_out.enabled ? "true" : "false") << "},";
-		json << "\"roi_debug_enabled\":" << (g_roi_debug_enabled ? "true" : "false") << ",";
-		// fix18新增:4路摄像头分辨率,供前端ROI绘制坐标换算
-		json << "\"resolution\":{";
-		json << "\"in-low\":{\"width\":" << in_low_w << ",\"height\":" << in_low_h << "},";
-		json << "\"in-high\":{\"width\":" << in_high_w << ",\"height\":" << in_high_h << "},";
-		json << "\"out-low\":{\"width\":" << out_low_w << ",\"height\":" << out_low_h << "},";
-		json << "\"out-high\":{\"width\":" << out_high_w << ",\"height\":" << out_high_h << "}";
-		json << "},";
-		json << "\"shared_mode\":" << (g_shared_capture_mode ? "true" : "false");
-		json << "}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	// ✅ fix18新增:单帧JPEG快照(绘制ROI时使用静态帧)
-	svr.Get("/api/stream", [](const httplib::Request& req, httplib::Response& res) {
-		std::string stream = "in-low";
-		auto it = req.params.find("stream");
-		if (it != req.params.end()) stream = it->second;
-
-		cv::Mat frame = get_frame_by_stream(stream);
-		if (frame.empty()) {
-			res.status = 503;
-			// ✅ fix24-v10: 添加charset=utf-8 + 改英文,防止中文乱码
-			res.set_content("Camera frame unavailable", "text/plain; charset=utf-8");
-			return;
-		}
-
-		// 低位摄像头叠加ROI红色边框(仅Web预览,不影响保存照片)
-		const ROIConfig* roi = get_roi_by_stream(stream);
-		if (roi && roi->enabled && !roi->isFullFrame()) {
-			cv::Rect roi_rect = roi->getRect(frame.cols, frame.rows);
-			cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 3);
-			cv::putText(frame, "ROI", cv::Point(roi_rect.x + 5, roi_rect.y + 25),
-					cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
-		}
-
-		// ✅ fix24-v10: 叠加英文标签(cv::putText不支持中文)
-		std::string label = get_stream_label(stream);
-		cv::putText(frame, label, cv::Point(10, 30),
-				cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
-
-		std::vector<uchar> buf;
-		cv::imencode(".jpg", frame, buf, {cv::IMWRITE_JPEG_QUALITY, 80});
-		res.set_content(std::string(buf.begin(), buf.end()), "image/jpeg");
-	});
-
-	// ✅ fix18新增:MJPEG实时视频流(浏览器<img>标签原生支持)
-	svr.Get("/api/mjpeg", [](const httplib::Request& req, httplib::Response& res) {
-		std::string stream = "in-low";
-		auto it = req.params.find("stream");
-		if (it != req.params.end()) stream = it->second;
-
-		res.set_chunked_content_provider(
-			"multipart/x-mixed-replace; boundary=frame",
-			[stream](size_t offset, httplib::DataSink& sink) -> bool {
-				cv::Mat frame = get_frame_by_stream(stream);
-				if (frame.empty()) {
-					std::this_thread::sleep_for(std::chrono::milliseconds(200));
-					return true;  // 继续等待下一帧
-				}
-
-				// 低位摄像头叠加ROI红色边框(仅Web预览,不影响保存照片)
-				const ROIConfig* roi = get_roi_by_stream(stream);
-				if (roi && roi->enabled && !roi->isFullFrame()) {
-					cv::Rect roi_rect = roi->getRect(frame.cols, frame.rows);
-					cv::rectangle(frame, roi_rect, cv::Scalar(0, 0, 255), 3);
-					cv::putText(frame, "ROI", cv::Point(roi_rect.x + 5, roi_rect.y + 25),
-							cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
-				}
-
-				// ✅ fix24-v10: 叠加英文标签(cv::putText不支持中文)
-				std::string label = get_stream_label(stream);
-				cv::putText(frame, label, cv::Point(10, 30),
-						cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
-
-				// JPEG编码
-				std::vector<uchar> buf;
-				cv::imencode(".jpg", frame, buf, {cv::IMWRITE_JPEG_QUALITY, 70});
-
-				// MJPEG帧格式
-				std::string header = "--frame\r\nContent-Type: image/jpeg\r\n\r\n";
-				sink.write(header.c_str(), header.size());
-				sink.write(reinterpret_cast<const char*>(buf.data()), buf.size());
-				sink.write("\r\n", 2);
-
-				// 控制帧率:~5 FPS
-				std::this_thread::sleep_for(std::chrono::milliseconds(200));
-				return true;  // 返回true继续发送
-			}
-		);
-	});
-
-	// ✅ fix18新增:ROI配置更新接口(含写回config.ini持久化)
-	svr.Post("/api/roi/update", [](const httplib::Request& req, httplib::Response& res) {
-		std::string body = req.body;
-
-		cJSON* root = cJSON_Parse(body.c_str());
-		if (!root) {
-			res.set_content("{\"success\":false,\"message\":\"JSON解析失败\"}",
-							"application/json; charset=utf-8");
-			return;
-		}
-
-		// 解析参数
-		std::string side = "in";
-		int x = 0, y = 0, w = 0, h = 0;
-		bool enabled = false;
-
-		cJSON* j_side = cJSON_GetObjectItem(root, "side");
-		cJSON* j_x = cJSON_GetObjectItem(root, "x");
-		cJSON* j_y = cJSON_GetObjectItem(root, "y");
-		cJSON* j_w = cJSON_GetObjectItem(root, "w");
-		cJSON* j_h = cJSON_GetObjectItem(root, "h");
-		cJSON* j_enabled = cJSON_GetObjectItem(root, "enabled");
-
-		if (j_side && j_side->valuestring) side = j_side->valuestring;
-		if (j_x) x = j_x->valueint;
-		if (j_y) y = j_y->valueint;
-		if (j_w) w = j_w->valueint;
-		if (j_h) h = j_h->valueint;
-		if (j_enabled) enabled = (j_enabled->type == cJSON_True || j_enabled->valueint == 1);
-
-		// 坐标校验
-		if (x < 0 || y < 0 || w < 0 || h < 0) {
-			cJSON_Delete(root);
-			res.set_content("{\"success\":false,\"message\":\"ROI坐标不能为负数\"}",
-							"application/json; charset=utf-8");
-			return;
-		}
-
-		// 更新全局变量
-		ROIConfig& roi = (side == "in") ? g_roi_in : g_roi_out;
-		roi.x = x;
-		roi.y = y;
-		roi.width = w;
-		roi.height = h;
-		roi.enabled = enabled;
-
-		// 写回config.ini持久化
-		bool save_ok = save_roi_to_config(g_config_ini_path, side, x, y, w, h, enabled);
-
-		cJSON_Delete(root);
-
-		std::ostringstream json;
-		json << "{\"success\":true,\"message\":\"ROI已更新"
-			 << (save_ok ? "并保存到config.ini" : "(config.ini保存失败,运行时已生效)")
-			 << "\"}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	// ✅ fix18新增:4路独立视频页面
-	svr.Get("/video", [](const httplib::Request& req, httplib::Response& res) {
-		std::string html = generate_video_page();
-		res.set_content(html, "text/html; charset=utf-8");
-	});
-
-	// ✅ fix24-v9新增:称重管理页面
-	svr.Get("/weight", [](const httplib::Request& req, httplib::Response& res) {
-		std::string html_path = pathComm + "assets/web/weight.html";
-		std::ifstream ifs(html_path);
-		if (!ifs.is_open()) {
-			res.status = 404;
-			res.set_content("称重页面不存在", "text/plain; charset=utf-8");
-			return;
-		}
-		std::string html((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
-		res.set_content(html, "text/html; charset=utf-8");
-	});
-
-	// ✅ fix24-v9新增:称重记录查询API
-	svr.Get("/api/weight/records", [](const httplib::Request& req, httplib::Response& res) {
-		int page = 1, page_size = 20;
-		std::string keyword;
-		auto it = req.params.find("page");
-		if (it != req.params.end()) page = std::atoi(it->second.c_str());
-		it = req.params.find("size");
-		if (it != req.params.end()) page_size = std::atoi(it->second.c_str());
-		it = req.params.find("keyword");
-		if (it != req.params.end()) keyword = it->second;
-
-		auto records = db_get_weight_records(page, page_size, keyword);
-		int total = db_get_weight_total_count();
-		int failed = db_get_weight_failed_count();
-
-		std::ostringstream json;
-		json << "{\"page\":" << page << ",\"page_size\":" << page_size
-			 << ",\"total\":" << total << ",\"failed\":" << failed
-			 << ",\"records\":[";
-		for (size_t i = 0; i < records.size(); i++) {
-			const auto& rec = records[i];
-			if (i > 0) json << ",";
-			double weight = 0;
-			try { weight = std::stod(rec.hi_photo_path); } catch (...) {}
-			json << "{\"id\":" << rec.id
-				 << ",\"create_time\":\"" << escape_html(rec.create_time) << "\""
-				 << ",\"plate_number\":\"" << escape_html(rec.plate_number) << "\""
-				 << ",\"tb_num\":\"" << escape_html(rec.tb_num) << "\""
-				 << ",\"station_type\":" << rec.station_type
-				 << ",\"weight_kg\":" << weight
-				 << ",\"upload_status\":" << rec.lo_upload_status
-				 << ",\"retry_count\":" << rec.hi_upload_status
-				 << ",\"response_msg\":\"" << escape_html(rec.lo_photo_path) << "\"}";
-		}
-		json << "]}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	// ✅ fix24-v9新增:称重手动重试API
-	svr.Post("/api/weight/retry", [](const httplib::Request& req, httplib::Response& res) {
-		std::string body = req.body;
-		cJSON* root = cJSON_Parse(body.c_str());
-		if (!root) {
-			// 尝试解析 form-urlencoded
-			auto it = req.params.find("id");
-			if (it == req.params.end()) {
-				res.status = 400;
-				res.set_content("{\"success\":false,\"message\":\"缺少id参数\"}", "application/json");
-				return;
-			}
-			int id = std::atoi(it->second.c_str());
-			if (id <= 0) {
-				res.status = 400;
-				res.set_content("{\"success\":false,\"message\":\"无效的id\"}", "application/json");
-				return;
-			}
-			bool ok = weight_manual_retry(id);
-			std::ostringstream json;
-			json << "{\"success\":" << (ok ? "true" : "false") << ",\"message\":\"" << (ok ? "重试成功" : "重试失败") << "\"}";
-			res.set_content(json.str(), "application/json; charset=utf-8");
-			return;
-		}
-		cJSON* j_id = cJSON_GetObjectItem(root, "id");
-		if (!j_id || !cJSON_IsNumber(j_id)) {
-			cJSON_Delete(root);
-			res.status = 400;
-			res.set_content("{\"success\":false,\"message\":\"缺少id参数\"}", "application/json");
-			return;
-		}
-		int id = j_id->valueint;
-		cJSON_Delete(root);
-		if (id <= 0) {
-			res.status = 400;
-			res.set_content("{\"success\":false,\"message\":\"无效的id\"}", "application/json");
-			return;
-		}
-		bool ok = weight_manual_retry(id);
-		std::ostringstream json;
-		json << "{\"success\":" << (ok ? "true" : "false") << ",\"message\":\"" << (ok ? "重试成功" : "重试失败") << "\"}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	svr.Get("/photo", [](const httplib::Request& req, httplib::Response& res) {
-		auto path_it = req.params.find("path");
-		if (path_it == req.params.end() || path_it->second.empty()) {
-			res.status = 400;
-			res.set_content("参数错误: path不能为空", "text/plain; charset=utf-8");
-			return;
-		}
-
-		std::string base_dir = pathComm + "PlateJPG";
-		std::string safe_path;
-		if (!is_safe_file_path(base_dir, path_it->second, safe_path)) {
-			res.status = 403;
-			res.set_content("访问拒绝: 非法路径", "text/plain; charset=utf-8");
-			return;
-		}
-
-		FILE* fp = fopen(safe_path.c_str(), "rb");
-		if (!fp) {
-			res.status = 404;
-			res.set_content("文件不存在", "text/plain; charset=utf-8");
-			return;
-		}
-
-		fseek(fp, 0, SEEK_END);
-		long file_size = ftell(fp);
-		fseek(fp, 0, SEEK_SET);
-		std::vector<char> buffer(file_size);
-		fread(buffer.data(), 1, file_size, fp);
-		fclose(fp);
-
-		res.set_content(buffer.data(), file_size, "image/jpeg");
-	});
-
-	// ==================== fix21: 配置页面与API ====================
-	svr.Get("/config", [](const httplib::Request& req, httplib::Response& res) {
-		res.set_content(generate_config_page(), "text/html; charset=utf-8");
-	});
-
-	// ✅ fix24: 锁定管理页面
-	svr.Get("/locks", [](const httplib::Request& req, httplib::Response& res) {
-		res.set_content(generate_locks_page(), "text/html; charset=utf-8");
-	});
-
-	svr.Get("/api/config", [](const httplib::Request& req, httplib::Response& res) {
-		if (g_config_ini_path.empty()) {
-			res.status = 500;
-			res.set_content("{\"error\":\"config.ini路径未设置\"}", "application/json");
-			return;
-		}
-		std::ifstream ifs(g_config_ini_path);
-		if (!ifs.is_open()) {
-			res.status = 500;
-			res.set_content("{\"error\":\"无法打开config.ini\"}", "application/json");
-			return;
-		}
-		// 解析INI为JSON
-		cJSON* root = cJSON_CreateObject();
-		std::string current_section;
-		std::string line;
-		while (std::getline(ifs, line)) {
-			// 去除首尾空白
-			size_t start = line.find_first_not_of(" \t\r\n");
-			if (start == std::string::npos) continue;
-			line = line.substr(start);
-			size_t end = line.find_last_not_of(" \t\r\n");
-			if (end != std::string::npos) line = line.substr(0, end + 1);
-			if (line.empty() || line[0] == '#' || line[0] == ';') continue;
-			if (line[0] == '[') {
-				size_t rb = line.find(']');
-				if (rb != std::string::npos) current_section = line.substr(1, rb - 1);
-				continue;
-			}
-			size_t eq = line.find('=');
-			if (eq == std::string::npos) continue;
-			std::string key = line.substr(0, eq);
-			std::string value = line.substr(eq + 1);
-			// 去除key和value的空白
-			auto trim = [](std::string& s) {
-				size_t a = s.find_first_not_of(" \t");
-				size_t b = s.find_last_not_of(" \t");
-				if (a == std::string::npos) { s = ""; return; }
-				s = s.substr(a, b - a + 1);
-			};
-			trim(key); trim(value);
-			// 去掉注释
-			for (char c : {'#', ';'}) {
-				size_t pos = value.find(c);
-				if (pos != std::string::npos) {
-					value = value.substr(0, pos);
-					trim(value);
-				}
-			}
-			std::string sec = current_section.empty() ? "global" : current_section;
-			cJSON* sec_obj = cJSON_GetObjectItem(root, sec.c_str());
-			if (!sec_obj) {
-				sec_obj = cJSON_CreateObject();
-				cJSON_AddItemToObject(root, sec.c_str(), sec_obj);
-			}
-			cJSON_AddStringToObject(sec_obj, key.c_str(), value.c_str());
-		}
-		char* json_str = cJSON_PrintUnformatted(root);
-		res.set_content(json_str, "application/json; charset=utf-8");
-		cJSON_free(json_str);
-		cJSON_Delete(root);
-	});
-
-	svr.Post("/api/config", [](const httplib::Request& req, httplib::Response& res) {
-		if (g_config_ini_path.empty()) {
-			res.status = 500;
-			res.set_content("{\"ok\":false,\"error\":\"config.ini路径未设置\"}", "application/json");
-			return;
-		}
-		cJSON* body = cJSON_Parse(req.body.c_str());
-		if (!body) {
-			res.status = 400;
-			res.set_content("{\"ok\":false,\"error\":\"JSON格式错误\"}", "application/json");
-			return;
-		}
-		std::ifstream ifs(g_config_ini_path);
-		if (!ifs.is_open()) {
-			cJSON_Delete(body);
-			res.status = 500;
-			res.set_content("{\"ok\":false,\"error\":\"无法打开config.ini\"}", "application/json");
-			return;
-		}
-		std::vector<std::string> lines;
-		std::string line;
-		while (std::getline(ifs, line)) lines.push_back(line);
-		ifs.close();
-
-		// Build updates map, and extract plates list from PlateRec section
-		std::map<std::string, std::string> updates;
-		std::vector<std::string> new_plates;
-		bool has_plates_textarea = false;
-		cJSON* sec = body->child;
-		while (sec) {
-			std::string sec_name = sec->string ? sec->string : "global";
-			cJSON* kv = sec->child;
-			while (kv) {
-				std::string key = kv->string ? kv->string : "";
-				std::string value = cJSON_IsString(kv) ? kv->valuestring : std::to_string(kv->valueint);
-				// Handle __plates__ textarea for PlateRec section
-				if (sec_name == "PlateRec" && key == "__plates__") {
-					has_plates_textarea = true;
-					// Parse newline-separated plate list
-					std::istringstream ss(value);
-					std::string plate_line;
-					while (std::getline(ss, plate_line)) {
-						// Trim whitespace
-						size_t a = plate_line.find_first_not_of(" \t\r\n");
-						size_t b = plate_line.find_last_not_of(" \t\r\n");
-						if (a != std::string::npos) {
-							new_plates.push_back(plate_line.substr(a, b - a + 1));
-						}
-					}
-				} else if (!(sec_name == "PlateRec" && key.rfind("car[", 0) == 0)) {
-					// Skip old car[*] entries from frontend (will be regenerated)
-					updates[sec_name + "|" + key] = value;
-				}
-				kv = kv->next;
-			}
-			sec = sec->next;
-		}
-
-		// If we have new plates, update Total and add car entries
-		if (has_plates_textarea) {
-			updates["PlateRec|Total"] = std::to_string(new_plates.size());
-			for (size_t i = 0; i < new_plates.size(); i++) {
-				updates["PlateRec|car[" + std::to_string(i) + "]"] = new_plates[i];
-			}
-		}
-
-		// Phase 1: Find and mark car[*] lines in PlateRec section for removal
-		std::string current_section;
-		std::set<std::string> applied;
-		std::set<int> car_lines_to_remove;
-		int total_line_in_platerec = -1;
-		int last_platerec_content_line = -1;
-
-		for (int i = 0; i < (int)lines.size(); i++) {
-			std::string trimmed = lines[i];
-			size_t s = trimmed.find_first_not_of(" \t\r\n");
-			if (s == std::string::npos) continue;
-			trimmed = trimmed.substr(s);
-			size_t e = trimmed.find_last_not_of(" \t\r\n");
-			if (e != std::string::npos) trimmed = trimmed.substr(0, e + 1);
-			if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') continue;
-			if (trimmed[0] == '[') {
-				size_t rb = trimmed.find(']');
-				if (rb != std::string::npos) current_section = trimmed.substr(1, rb - 1);
-				continue;
-			}
-			if (current_section == "PlateRec") {
-				size_t eq = trimmed.find('=');
-				if (eq != std::string::npos) {
-					std::string key = trimmed.substr(0, eq);
-					size_t ks2 = key.find_first_not_of(" \t");
-					size_t ke2 = key.find_last_not_of(" \t");
-					if (ks2 != std::string::npos) key = key.substr(ks2, ke2 - ks2 + 1);
-					if (key.rfind("car[", 0) == 0) {
-						car_lines_to_remove.insert(i);
-					}
-					if (key == "Total") {
-						total_line_in_platerec = i;
-					}
-					last_platerec_content_line = i;
-				}
-			}
-		}
-
-		// Phase 2: Remove old car lines (reverse order to preserve indices)
-		for (auto it = car_lines_to_remove.rbegin(); it != car_lines_to_remove.rend(); ++it) {
-			lines.erase(lines.begin() + *it);
-			// Adjust indices
-			if (total_line_in_platerec > *it) total_line_in_platerec--;
-			if (last_platerec_content_line > *it) last_platerec_content_line--;
-		}
-
-		// Phase 3: Update existing keys and track what's applied
-		current_section = "";
-		for (auto& l : lines) {
-			std::string trimmed = l;
-			size_t s = trimmed.find_first_not_of(" \t\r\n");
-			if (s != std::string::npos) trimmed = trimmed.substr(s);
-			size_t e = trimmed.find_last_not_of(" \t\r\n");
-			if (e != std::string::npos) trimmed = trimmed.substr(0, e + 1);
-			if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') continue;
-			if (trimmed[0] == '[') {
-				size_t rb = trimmed.find(']');
-				if (rb != std::string::npos) current_section = trimmed.substr(1, rb - 1);
-				continue;
-			}
-			size_t eq = trimmed.find('=');
-			if (eq == std::string::npos) continue;
-			std::string key = trimmed.substr(0, eq);
-			size_t ks2 = key.find_first_not_of(" \t");
-			size_t ke2 = key.find_last_not_of(" \t");
-			if (ks2 != std::string::npos) key = key.substr(ks2, ke2 - ks2 + 1);
-			std::string lookup = current_section + "|" + key;
-			auto it = updates.find(lookup);
-			if (it != updates.end()) {
-				size_t orig_eq = l.find('=');
-				if (orig_eq != std::string::npos) {
-					std::string prefix = l.substr(0, orig_eq + 1);
-					l = prefix + " " + it->second;
-					applied.insert(lookup);
-				}
-			}
-		}
-
-		// Phase 4: Add new car entries and Total if they were new
-		std::vector<std::string> new_lines_to_add;
-		for (auto& [lookup, value] : updates) {
-			if (applied.find(lookup) == applied.end()) {
-				// This key wasn't found in existing file, need to add it
-				size_t sep = lookup.find('|');
-				std::string sec_name = lookup.substr(0, sep);
-				std::string key_name = lookup.substr(sep + 1);
-				if (sec_name == "PlateRec" && (key_name.rfind("car[", 0) == 0 || key_name == "Total")) {
-					new_lines_to_add.push_back(key_name + "=" + value);
-				}
-			}
-		}
-
-		// Insert new car lines after the last PlateRec content line (or after [PlateRec] header)
-		if (!new_lines_to_add.empty()) {
-			// Find the [PlateRec] section header line in current (modified) lines
-			int insert_pos = -1;
-			int platerec_end = (int)lines.size();
-			bool in_platerec = false;
-			for (int i = 0; i < (int)lines.size(); i++) {
-				std::string trimmed = lines[i];
-				size_t s = trimmed.find_first_not_of(" \t\r\n");
-				if (s != std::string::npos) trimmed = trimmed.substr(s);
-				size_t e = trimmed.find_last_not_of(" \t\r\n");
-				if (e != std::string::npos) trimmed = trimmed.substr(0, e + 1);
-				if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') continue;
-				if (trimmed[0] == '[') {
-					if (in_platerec) {
-						platerec_end = i;
-						break;
-					}
-					size_t rb = trimmed.find(']');
-					if (rb != std::string::npos) {
-						std::string sec = trimmed.substr(1, rb - 1);
-						if (sec == "PlateRec") {
-							in_platerec = true;
-							insert_pos = i + 1;
-						}
-					}
-				}
-			}
-			if (in_platerec && insert_pos >= 0) {
-				// Insert at the end of PlateRec section (before next section or EOF)
-				for (int i = insert_pos; i < (int)lines.size(); i++) {
-					std::string trimmed = lines[i];
-					size_t s = trimmed.find_first_not_of(" \t\r\n");
-					if (s != std::string::npos) trimmed = trimmed.substr(s);
-					if (!trimmed.empty() && trimmed[0] == '[') {
-						platerec_end = i;
-						break;
-					}
-				}
-				// Insert before platerec_end
-				for (int i = 0; i < (int)new_lines_to_add.size(); i++) {
-					lines.insert(lines.begin() + platerec_end + i, new_lines_to_add[i]);
-				}
-			}
-		}
-
-		// Write file
-		std::ofstream ofs(g_config_ini_path);
-		if (!ofs.is_open()) {
-			res.status = 500;
-			res.set_content("{\"ok\":false,\"error\":\"无法写入config.ini\"}", "application/json");
-			return;
-		}
-		for (const auto& l : lines) ofs << l << "\n";
-		ofs.close();
-		res.set_content("{\"ok\":true,\"updated\":" + std::to_string(applied.size() + new_lines_to_add.size()) + "}", "application/json");
-	});
-
-
-	g_web_server_thread = std::make_unique<std::thread>([&svr]() {
-		const char* proto = g_ssl_enabled ? "https" : "http";
-		// ✅ fix24-v16: 使用可配置端口替代硬编码
-		std::cout << "✅ Web服务启动成功 (" << proto << "),端口 " << g_web_server_port << std::endl;
-		g_web_server_initialized = true;
-		svr.listen("0.0.0.0", g_web_server_port);
-		std::cout << "Web服务已停止监听" << std::endl;
-	});
-
-	// ✅ fix24-v19: HTTP 到 HTTPS 自动跳转(仅当 SSL 启用时)
-	if (g_ssl_enabled && g_web_server_port > 1) {
-		int http_port = g_web_server_port - 1;
-		g_http_redirect_server = std::make_unique<httplib::Server>();
-		auto& http_svr = *g_http_redirect_server;
-		
-		// 对所有请求返回 301 重定向到 HTTPS
-		http_svr.Get(".*", [&http_port](const httplib::Request& req, httplib::Response& res) {
-			// 构造 HTTPS URL
-			std::string host = req.get_header_value("Host");
-			// 移除可能的端口号
-			size_t colon_pos = host.find(':');
-			if (colon_pos != std::string::npos) {
-				host = host.substr(0, colon_pos);
-			}
-			std::string https_url = "https://" + host + ":" + std::to_string(g_web_server_port) + req.path;
-			// ✅ fix24-v19: 转发查询参数(httplib::Request::params 是 multimap)
-			if (!req.params.empty()) {
-				https_url += "?";
-				bool first = true;
-				for (const auto& param : req.params) {
-					if (!first) https_url += "&";
-					https_url += param.first + "=" + param.second;
-					first = false;
-				}
-			}
-			res.status = 301;
-			res.set_header("Location", https_url);
-			res.set_content("Redirecting to HTTPS...", "text/plain");
-		});
-		
-		// HTTP 重定向服务器在独立线程中运行
-		g_http_redirect_thread = std::make_unique<std::thread>([http_port]() {
-			std::cout << "[HTTP重定向] 启动 HTTP->HTTPS 重定向服务,端口 " << http_port << std::endl;
-			if (g_http_redirect_server) {
-				g_http_redirect_server->listen("0.0.0.0", http_port);
-			}
-			std::cout << "[HTTP重定向] HTTP 重定向服务已停止" << std::endl;
-		});
-	}
-}
-
-void stop_web_server() {
-	if (!g_web_server_running.load()) return;
-	g_web_server_running = false;
-	
-	while (!g_web_server_initialized.load()) {
-		std::this_thread::sleep_for(std::chrono::milliseconds(100));
-	}
-	
-	if (g_web_server) {
-		g_web_server->stop();
-	}
-	
-	if (g_web_server_thread && g_web_server_thread->joinable()) {
-		g_web_server_thread->join();
-	}
-	
-	// ✅ fix24-v19: 停止 HTTP 重定向服务器
-	if (g_http_redirect_server) {
-		g_http_redirect_server->stop();
-	}
-	if (g_http_redirect_thread && g_http_redirect_thread->joinable()) {
-		g_http_redirect_thread->join();
-	}
-	
-	g_web_server.reset();
-	g_web_server_thread.reset();
-	g_http_redirect_server.reset();
-	g_http_redirect_thread.reset();
-	g_web_server_initialized = false;
-	std::cout << "✅ Web服务已完全停止" << std::endl;
-}
-
-std::string generate_html_page() {
-	std::string html_path = pathComm + "assets/web/index.html";
-	std::ifstream html_file(html_path);
-	if (!html_file.is_open()) {
-		std::ostringstream error_html;
-		error_html << "<!DOCTYPE html><html><head><title>错误</title></head>"
-			<< "<body><h1>无法加载页面</h1>"
-			<< "<p>无法找到 HTML 文件: " << html_path << "</p>"
-			<< "</body></html>";
-		return error_html.str();
-	}
-	std::ostringstream buffer;
-	buffer << html_file.rdbuf();
-	html_file.close();
-	return buffer.str();
-}
-
-// ✅ fix18新增:生成4路独立视频页面
-std::string generate_video_page() {
-	std::string html_path = pathComm + "assets/web/video.html";
-	std::ifstream html_file(html_path);
-	if (!html_file.is_open()) {
-		std::ostringstream error_html;
-		error_html << "<!DOCTYPE html><html><head><title>错误</title></head>"
-			<< "<body><h1>无法加载页面</h1>"
-			<< "<p>无法找到视频页面: " << html_path << "</p>"
-			<< "</body></html>";
-		return error_html.str();
-	}
-	std::ostringstream buffer;
-	buffer << html_file.rdbuf();
-	html_file.close();
-	return buffer.str();
-}
-
-// ==================== fix21: 配置页面HTML ====================
-static std::string generate_config_page() {
-	return R"HTML(<!DOCTYPE html>
-<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
-<title>系统配置 - config.ini</title>
-<style>
-*{box-sizing:border-box;margin:0;padding:0}
-body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;color:#333}
-.navbar{background:#1a1a2e;color:#fff;padding:12px 20px;display:flex;align-items:center;gap:20px;box-shadow:0 2px 8px rgba(0,0,0,0.15)}
-.navbar a{color:#ccc;text-decoration:none;padding:6px 12px;border-radius:4px;font-size:14px}
-.navbar a:hover,.navbar a.active{background:rgba(255,255,255,0.15);color:#fff}
-.navbar .brand{font-size:16px;font-weight:bold;color:#4fc3f7;margin-right:10px}
-.container{max-width:960px;margin:20px auto;padding:0 16px}
-.status-bar{background:#fff;border-radius:8px;padding:12px 16px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,0.08);display:flex;align-items:center;gap:12px}
-.status-bar .path{color:#666;font-size:13px;flex:1}
-.status-bar .path code{background:#f5f5f5;padding:2px 6px;border-radius:3px;font-size:12px}
-.btn{padding:8px 18px;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;transition:all 0.2s}
-.btn-primary{background:#1976d2;color:#fff}.btn-primary:hover{background:#1565c0}
-.btn-success{background:#388e3c;color:#fff}.btn-success:hover{background:#2e7d32}
-.btn:disabled{opacity:0.5;cursor:not-allowed}
-.section{background:#fff;border-radius:8px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,0.08);overflow:hidden}
-.section-title{background:#263238;color:#fff;padding:10px 16px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px}
-.section-title .badge{background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:10px;font-size:11px}
-.section-body{padding:0}
-.field{display:flex;align-items:center;padding:10px 16px;border-bottom:1px solid #f0f0f0;gap:12px}
-.field:last-child{border-bottom:none}
-.field:hover{background:#fafafa}
-.field-label{min-width:200px;font-size:13px;color:#555;font-family:monospace}
-.field-input{flex:1}
-.field-input input,.field-input textarea{width:100%;padding:7px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;font-family:monospace;transition:border-color 0.2s}
-.field-input textarea{min-height:80px;resize:vertical;line-height:1.6}
-.field-input input:focus,.field-input textarea:focus{outline:none;border-color:#1976d2;box-shadow:0 0 0 2px rgba(25,118,210,0.1)}
-.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;color:#fff;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:9999;display:none}
-.toast-success{background:#388e3c}.toast-error{background:#d32f2f}
-.loading{text-align:center;padding:40px;color:#999}
-.spinning{display:inline-block;width:20px;height:20px;border:3px solid #ddd;border-top-color:#1976d2;border-radius:50%;animation:spin 0.8s linear infinite}
-@keyframes spin{to{transform:rotate(360deg)}}
-</style></head><body>
-<nav class="navbar">
-  <span class="brand">🚛 车牌识别系统</span>
-  <a href="/">监控首页</a>
-  <a href="/video">视频预览</a>
-  <a href="/config" class="active">系统配置</a>
-  <a href="/locks">锁定管理</a>
-</nav>
-<div class="container">
-  <div class="status-bar">
-    <div class="path">配置文件: <code id="cfgPath">加载中...</code></div>
-    <button class="btn btn-primary" id="saveBtn" onclick="saveConfig()" disabled>💾 保存配置</button>
-  </div>
-  <div id="configForm"><div class="loading"><div class="spinning"></div><p style="margin-top:10px">加载配置文件...</p></div></div>
-</div>
-<div class="toast" id="toast"></div>
-<script>
-const SECTION_LABELS = {
-  'global':'基本配置','config':'运行参数','MQTT':'MQTT上报',
-  'weight':'称重系统','feishu':'飞书告警','LED':'LED显示屏','PlateRec':'车牌白名单'
-};
-const KEY_LABELS = {
-  'project_name':'工程名称','point_number':'工地编号','throughway':'门号',
-  'app_key':'App Key','app_secret':'App Secret','app_api':'平台API',
-  'wType':'工地类型(1=工地/3=消纳)','TestFlag':'测试模式(0=正式/1=测试)',
-  'rtsp_url_front_in':'前方进站RTSP','rtsp_url_front_out':'前方出站RTSP',
-  'rtsp_url_side_in':'侧方进站RTSP','rtsp_url_side_out':'侧方出站RTSP',
-  'RUN_MODE':'运行模式','RECOGNIZE_INTERVAL':'识别间隔(秒)',
-  'PRECISION_INT8':'INT8精度','MAX_INFERENCE_FPS':'最大推理FPS',
-  'MATCH_THRESHOLD':'匹配阈值','MAX_QUEUE_SIZE':'最大队列','INFERENCE_THREADS':'推理线程数',
-  'DETECT_LEVEL':'检测级别','PLATE_CONFIDENCE_THRESHOLD':'车牌置信度阈值',
-  'PLATE_LOG_THRESHOLD':'车牌日志阈值','AlternatingMerge':'交替锁定',
-  'in_out_interval':'进出间隔(秒)','TIME_WINDOW':'时间窗口(分钟)',
-  'PhotoMaxCapacityMB':'照片存储上限(MB)','LogRetentionDays':'日志保留天数',
-  'DEBUG_LOG':'调试日志(0=关/1=开)',
-  'MQTT_HOST':'MQTT服务器','MQTT_PORT':'端口','MQTT_USER':'用户名','MQTT_PASS':'密码',
-  'MQTT_TOPIC':'Topic','MQTT_CLIENT_ID':'Client ID',
-  'PLATE_COLOR':'车牌颜色','VEHICLE_TYPE':'车辆类型',
-  'flagWeight':'启用称重(0/1)','weight_server_ip':'仪表IP','weight_server_port':'仪表端口',
-  'weight_threshold_in':'进站阈值(kg)','weight_threshold_out':'出站阈值(kg)',
-  'weight_detection_time':'检测时间(秒)','tcp_connect_timeout':'TCP超时(秒)',
-  'CANDIDATE_DATA_COUNT':'候选数据数量','STABLE_SAMPLE_COUNT':'稳定样本数',
-  'STABLE_THRESHOLD_KG':'稳定阈值(kg)','MAX_WEIGHT':'最大重量(kg)',
-  'WarningSigns':'告警开关(0/1)','APP_ID':'飞书AppID','APP_SECRET':'飞书AppSecret',
-  'CHAT_ID':'飞书ChatID','TIMEOUT_SECOND':'超时(秒)',
-  'IN_LED_IP':'进站LED IP','IN_LED_PORT':'进站LED端口',
-  'OUT_LED_IP':'出站LED IP','OUT_LED_PORT':'出站LED端口','Total':'白名单总数'
-};
-let configData = {};
-
-async function loadConfig() {
-  try {
-    const res = await fetch('/api/config');
-    if (!res.ok) throw new Error('HTTP ' + res.status);
-    configData = await res.json();
-    document.getElementById('cfgPath').textContent = location.host + '/config.ini';
-    renderForm();
-    document.getElementById('saveBtn').disabled = false;
-  } catch(e) {
-    document.getElementById('configForm').innerHTML = '<div class="loading" style="color:#d32f2f">加载失败: '+e.message+'</div>';
-  }
-}
-
-function renderForm() {
-  const form = document.getElementById('configForm');
-  let html = '';
-  for (const [section, kvs] of Object.entries(configData)) {
-    const label = SECTION_LABELS[section] || section;
-    const keys = Object.keys(kvs);
-    html += '<div class="section"><div class="section-title">📋 '+label+' <span class="badge">'+section+'</span></div><div class="section-body">';
-    // Collect car[*] entries for textarea rendering
-    const carEntries = [];
-    for (const key of keys) {
-      if (/^car\[\d+\]$/.test(key)) {
-        carEntries.push({index: parseInt(key.match(/\d+/)[0]), value: kvs[key]});
-      }
-    }
-    carEntries.sort((a,b) => a.index - b.index);
-    for (const key of keys) {
-      if (/^car\[\d+\]$/.test(key)) continue; // skip, handled below
-      const lbl = KEY_LABELS[key] || key;
-      const val = kvs[key];
-      const id = section+'__'+key;
-      html += '<div class="field"><div class="field-label" title="'+key+'">'+lbl+'</div>'
-            + '<div class="field-input"><input type="text" id="'+id+'" data-section="'+section+'" data-key="'+key+'" value="'+escAttr(val)+'"></div></div>';
-    }
-    // Render car entries as textarea
-    if (carEntries.length > 0) {
-      const platesText = carEntries.map(e => e.value).join('\n');
-      html += '<div class="field"><div class="field-label" title="car[*]">车牌白名单<br><small style="color:#999">每行一个车牌号</small></div>'
-            + '<div class="field-input"><textarea id="'+section+'____plates__" data-section="'+section+'" data-key="__plates__" placeholder="每行输入一个车牌号">'+escAttr(platesText)+'</textarea></div></div>';
-    }
-    html += '</div></div>';
-  }
-  form.innerHTML = html;
-}
-
-function escAttr(s) { return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
-
-async function saveConfig() {
-  const btn = document.getElementById('saveBtn');
-  btn.disabled = true; btn.textContent = '保存中...';
-  const payload = {};
-  // Collect regular input fields
-  document.querySelectorAll('#configForm input').forEach(inp => {
-    const sec = inp.dataset.section, key = inp.dataset.key;
-    if (!payload[sec]) payload[sec] = {};
-    payload[sec][key] = inp.value;
-  });
-  // Collect textarea fields (expand to car[0], car[1], ...)
-  document.querySelectorAll('#configForm textarea').forEach(ta => {
-    const sec = ta.dataset.section, key = ta.dataset.key;
-    if (!payload[sec]) payload[sec] = {};
-    if (key === '__plates__') {
-      // Parse textarea lines into car entries
-      payload[sec]['__plates__'] = ta.value;
-    }
-  });
-  try {
-    const res = await fetch('/api/config', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
-    const data = await res.json();
-    if (data.ok) { showToast('保存成功!更新了 '+data.updated+' 项配置','success'); }
-    else { showToast('保存失败: '+(data.error||'未知错误'),'error'); }
-  } catch(e) { showToast('保存失败: '+e.message,'error'); }
-  btn.disabled = false; btn.textContent = '💾 保存配置';
-}
-
-function showToast(msg, type) {
-  const t = document.getElementById('toast');
-  t.textContent = msg; t.className = 'toast toast-'+type; t.style.display = 'block';
-  setTimeout(() => t.style.display = 'none', 3000);
-}
-
-loadConfig();
-</script></body></html>)HTML";
-}
-
-// ✅ fix24: 锁定管理页面
-static std::string generate_locks_page() {
-	return R"HTML(<!DOCTYPE html>
-<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
-<title>交替锁定管理</title>
-<style>
-*{box-sizing:border-box;margin:0;padding:0}
-body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;color:#333}
-.navbar{background:#1a1a2e;color:#fff;padding:12px 20px;display:flex;align-items:center;gap:20px;box-shadow:0 2px 8px rgba(0,0,0,0.15)}
-.navbar a{color:#ccc;text-decoration:none;padding:6px 12px;border-radius:4px;font-size:14px}
-.navbar a:hover,.navbar a.active{background:rgba(255,255,255,0.15);color:#fff}
-.navbar .brand{font-size:16px;font-weight:bold;color:#4fc3f7;margin-right:10px}
-.container{max-width:960px;margin:20px auto;padding:0 16px}
-.card{background:#fff;border-radius:8px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,0.08);overflow:hidden}
-.card-title{background:#263238;color:#fff;padding:10px 16px;font-size:14px;font-weight:600;display:flex;align-items:center;justify-content:space-between}
-.badge{background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:10px;font-size:12px}
-table{width:100%;border-collapse:collapse}
-th{background:#f5f5f5;padding:10px 12px;text-align:left;font-size:13px;color:#555;border-bottom:2px solid #e0e0e0}
-td{padding:10px 12px;border-bottom:1px solid #f0f0f0;font-size:13px}
-tr:hover{background:#fafafa}
-.mode-in{color:#e65100;font-weight:600}
-.mode-out{color:#1565c0;font-weight:600}
-.btn{padding:6px 14px;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500;transition:all 0.2s}
-.btn-danger{background:#d32f2f;color:#fff}.btn-danger:hover{background:#b71c1c}
-.btn-sm{padding:4px 10px;font-size:11px}
-.empty{text-align:center;padding:40px;color:#999}
-.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;color:#fff;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:9999;display:none}
-.toast-success{background:#388e3c}.toast-error{background:#d32f2f}
-.loading{text-align:center;padding:40px;color:#999}
-</style></head><body>
-<nav class="navbar">
-  <span class="brand">🚛 车牌识别系统</span>
-  <a href="/">监控首页</a>
-  <a href="/video">视频预览</a>
-  <a href="/config">系统配置</a>
-  <a href="/locks" class="active">锁定管理</a>
-</nav>
-<div class="container">
-  <div class="card">
-    <div class="card-title">
-      <span>🔒 交替锁定记录 <span class="badge" id="lockCount">加载中...</span></span>
-      <span style="font-size:12px;opacity:0.7">超过2小时自动清零 | 手动清除用于解除异常阻塞</span>
-    </div>
-    <div id="lockList"><div class="loading">加载中...</div></div>
-  </div>
-</div>
-<div class="toast" id="toast"></div>
-<script>
-function fmtTime(ts) {
-  if (!ts || ts <= 0) return '-';
-  const d = new Date(ts * 1000);
-  return d.toLocaleString('zh-CN', {hour12:false});
-}
-function fmtDuration(sec) {
-  if (sec < 60) return sec + '秒';
-  if (sec < 3600) return Math.floor(sec/60) + '分' + (sec%60) + '秒';
-  return Math.floor(sec/3600) + '时' + Math.floor((sec%3600)/60) + '分';
-}
-async function loadLocks() {
-  try {
-    const res = await fetch('/api/station_locks');
-    const data = await res.json();
-    const locks = data.locks || [];
-    document.getElementById('lockCount').textContent = locks.length + '条记录';
-    if (locks.length === 0) {
-      document.getElementById('lockList').innerHTML = '<div class="empty">✅ 当前无锁定记录</div>';
-      return;
-    }
-    let html = '<table><tr><th>车牌号</th><th>当前状态</th><th>上次进站</th><th>上次出站</th><th>已等待</th><th>剩余时间</th><th>操作</th></tr>';
-    for (const lk of locks) {
-      const modeClass = lk.status === '只进不出' ? 'mode-in' : 'mode-out';
-      html += '<tr>';
-      html += '<td><strong>' + lk.plate + '</strong></td>';
-      html += '<td class="' + modeClass + '">' + lk.status + '</td>';
-      html += '<td>' + fmtTime(lk.last_in_time) + '</td>';
-      html += '<td>' + fmtTime(lk.last_out_time) + '</td>';
-      html += '<td>' + fmtDuration(lk.elapsed_seconds) + '</td>';
-      // ✅ fix24-v7: 显示剩余等待时间
-      if (lk.remaining_seconds === -2) {
-        html += '<td style="color:#d32f2f;font-weight:600">需对端先操作</td>';
-      } else if (lk.remaining_seconds === 0) {
-        html += '<td style="color:#388e3c;font-weight:600">可操作 ✅</td>';
-      } else if (lk.remaining_seconds > 0) {
-        html += '<td style="color:#e65100;font-weight:600">' + fmtDuration(lk.remaining_seconds) + '</td>';
-      } else {
-        html += '<td>-</td>';
-      }
-      html += '<td><button class="btn btn-danger btn-sm" onclick="clearLock(\'' + lk.plate + '\')">清除锁定</button></td>';
-      html += '</tr>';
-    }
-    html += '</table>';
-    document.getElementById('lockList').innerHTML = html;
-  } catch(e) {
-    document.getElementById('lockList').innerHTML = '<div class="empty" style="color:#d32f2f">加载失败: ' + e.message + '</div>';
-  }
-}
-async function clearLock(plate) {
-  if (!confirm('确定要清除 ' + plate + ' 的锁定记录吗?\n清除后该车牌可以立即进行进站/出站操作。')) return;
-  try {
-    const res = await fetch('/api/clear_lock', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({plate:plate})});
-    const data = await res.json();
-    if (data.ok) { showToast('已清除 ' + plate + ' 的锁定记录', 'success'); loadLocks(); }
-    else { showToast('清除失败: ' + (data.error||'未知错误'), 'error'); }
-  } catch(e) { showToast('请求失败: ' + e.message, 'error'); }
-}
-function showToast(msg, type) {
-  const t = document.getElementById('toast');
-  t.textContent = msg; t.className = 'toast toast-'+type; t.style.display = 'block';
-  setTimeout(() => t.style.display = 'none', 3000);
-}
-loadLocks();
-setInterval(loadLocks, 10000);
-</script></body></html>)HTML";
-}
-
-// ==================== 照片上传函数 ====================
-// ✅ 修复:db_id <= 0时都尝试插入,解决插入失败后无法重试的问题
-PhotoUploadResult upload_in_photos(std::shared_ptr<CarPlateInfo> plate_info,
-	const std::string& plate,
-	const std::string& tb_num,
-	const std::string& project_name,
-	const std::string& point_number,
-	const std::string& throughway,
-	bool is_special)
-{
-	PhotoUploadResult result;
-	std::cout << "[进站] " << plate << " 开始上传2组照片..." << std::endl;
-
-    // ✅ 修复:db_id <= 0时都尝试插入(包括db_id == -1的情况)
-    if (plate_info->db_id <= 0) {
-        // ✅ fix24-v18: 使用照片保存时固定的db_capture_time,防止识别线程竞态覆盖
-        time_t capture_ts = 0;
-        if (plate_info->db_capture_time > 0) {
-            capture_ts = plate_info->db_capture_time;
-        } else if (plate_info->cap_info_copy && plate_info->cap_info_copy->capture_time > 0) {
-            capture_ts = plate_info->cap_info_copy->capture_time;  // fallback
-        }
-        plate_info->db_id = db_insert_record_with_retry(plate, tb_num, 1,
-            plate_info->pic_path_front, plate_info->pic_path_side, 0, 0, 0, capture_ts);
-    } else {
-        std::cout << "[INFO] " << plate << " 复用已有数据库记录 ID=" << plate_info->db_id << std::endl;
-        g_metrics.record_db_duplicate_skip();
-    }
-
-	for (int group = 0; group < g_max_photo_groups && g_running; ++group) {
-		if (g_in_plate_status.is_blocked(plate)) {
-			std::cerr << "[进站] " << plate << " 已被临时阻止" << std::endl;
-			break;
-		}
-
-		bool group_success = false;
-		bool lo_success = false;
-		bool hi_success = false;
-		
-		for (int retry = 0; retry < PHOTO_RETRY_COUNT && !group_success && g_running; ++retry) {
-			if (retry > 0) {
-				int backoff = (1 << retry);  // 指数退让: 2, 4, 8秒
-				std::this_thread::sleep_for(std::chrono::seconds(backoff));
-			}
-
-			lo_success = lib_curl_image_upload_request(plate_info, POSITION_LO);
-			if (!lo_success) continue;
-			
-			std::this_thread::sleep_for(std::chrono::milliseconds(300));
-			
-			hi_success = lib_curl_image_upload_request(plate_info, POSITION_HI);
-			if (!hi_success) continue;
-
-			group_success = true;
-			result.success_groups++;
-            // 使用"或"操作,确保只要成功过一次就不会被覆盖
-			result.lo_success = result.lo_success || lo_success;
-			result.hi_success = result.hi_success || hi_success;
-			std::cout << "[进站] " << plate << " 第" << (group + 1) << "组成功" << std::endl;
-			
-			// ✅ P0修复:计数器已在create_or_get_bill_cache_for_photo中递增
-			// 此处不再重复递增,避免计数器超过实际照片组数
-			g_metrics.record_photo_upload(lo_success, hi_success, group_success);
-			break;
-		}
-
-        // ✅ P1修复: 只有组成功时才更新单张照片的成功状态,避免失败组污染全局状态
-        // 移除了 "即使组失败,也要更新单张照片的成功状态" 的逻辑
-
-		if (result.success_groups >= REQUIRED_SUCCESS_GROUPS) break;
-		if (g_running && group < g_max_photo_groups - 1) {
-			std::this_thread::sleep_for(std::chrono::milliseconds(300));
-		}
-	}
-
-	// 精确更新每张照片的独立状态
-	if (plate_info->db_id > 0) {
-		db_update_upload_status(plate_info->db_id,
-			result.lo_success ? 1 : 0,
-			result.hi_success ? 1 : 0);
-	}
-
-	{
-		std::lock_guard<std::mutex> lock(g_in_record_mtx);
-		auto it = g_in_upload_records.find(plate);
-		if (it != g_in_upload_records.end()) {
-			it->second.photo_success_count += result.success_groups;
-			it->second.trigger_time = time(NULL);
-			it->second.generation++;
-		}
-	}
-
-	if (result.success_groups >= REQUIRED_SUCCESS_GROUPS) {
-		bool should_send = false;
-		{
-			std::lock_guard<std::mutex> lock(g_in_record_mtx);
-			auto it = g_in_upload_records.find(plate);
-			if (it != g_in_upload_records.end() && !it->second.feishu_sent) {
-				should_send = true;
-				it->second.feishu_sent = true;
-			}
-		}
-
-		if (should_send) {  // ✅ FIX9-1: 常规飞书消息不受WarningSigns控制,始终发送
-			std::cout << "[进站] " << plate << " 发送飞书消息..." << std::endl;
-			std::string token = get_cached_tenant_token();
-			if (!token.empty()) {
-				std::string platenumcolor = is_special ? "黄色" : plate_info->type;
-				std::string vehicleType = is_special ? "渣土车" : "";
-				bool feishu_ok = send_feishu_msg(token,
-					project_name, plate, get_format_time(),
-					point_number, throughway,
-					"进站", platenumcolor, vehicleType,
-					tb_num);
-				if (plate_info->db_id > 0) {
-					db_update_feishu_status(plate_info->db_id, feishu_ok ? 1 : 2);
-				}
-			}
-		}
-
-		// ✅ fix10: 进站锁定已在 try_lock_in_station 中完成,此处仅确认
-		lock_in_station(plate);
-	}
-
-	return result;
-}
-
-// ✅ 修复:db_id <= 0时都尝试插入,解决插入失败后无法重试的问题
-PhotoUploadResult upload_out_photos(std::shared_ptr<CarPlateInfo> plate_info,
-	const std::string& plate,
-	const std::string& tb_num,
-	const std::string& project_name,
-	const std::string& point_number,
-	const std::string& throughway,
-	bool is_special)
-{
-	PhotoUploadResult result;
-	std::cout << "[出站] " << plate << " 开始上传2组照片..." << std::endl;
-
-    // ✅ 修复:db_id <= 0时都尝试插入(包括db_id == -1的情况)
-    if (plate_info->db_id <= 0) {
-        // ✅ fix24-v18: 使用照片保存时固定的db_capture_time,防止识别线程竞态覆盖
-        time_t capture_ts = 0;
-        if (plate_info->db_capture_time > 0) {
-            capture_ts = plate_info->db_capture_time;
-        } else if (plate_info->cap_info_copy && plate_info->cap_info_copy->capture_time > 0) {
-            capture_ts = plate_info->cap_info_copy->capture_time;  // fallback
-        }
-        plate_info->db_id = db_insert_record_with_retry(plate, tb_num, 2,
-            plate_info->pic_path_front, plate_info->pic_path_side, 0, 0, 0, capture_ts);
-    } else {
-        std::cout << "[INFO] " << plate << " 复用已有数据库记录 ID=" << plate_info->db_id << std::endl;
-        g_metrics.record_db_duplicate_skip();
-    }
-
-	for (int group = 0; group < g_max_photo_groups && g_running; ++group) {
-		if (g_out_plate_status.is_blocked(plate)) {
-			std::cerr << "[出站] " << plate << " 已被临时阻止" << std::endl;
-			break;
-		}
-
-		bool group_success = false;
-		bool lo_success = false;
-		bool hi_success = false;
-		
-		for (int retry = 0; retry < PHOTO_RETRY_COUNT && !group_success && g_running; ++retry) {
-			if (retry > 0) {
-				int backoff = (1 << retry);  // 指数退让: 2, 4, 8秒
-				std::this_thread::sleep_for(std::chrono::seconds(backoff));
-			}
-
-			lo_success = lib_curl_image_upload_request(plate_info, POSITION_LO);
-			if (!lo_success) continue;
-			
-			std::this_thread::sleep_for(std::chrono::milliseconds(300));
-			
-			hi_success = lib_curl_image_upload_request(plate_info, POSITION_HI);
-			if (!hi_success) continue;
-
-			group_success = true;
-			result.success_groups++;
-            // 使用"或"操作,确保只要成功过一次就不会被覆盖
-			result.lo_success = result.lo_success || lo_success;
-			result.hi_success = result.hi_success || hi_success;
-			std::cout << "[出站] " << plate << " 第" << (group + 1) << "组成功" << std::endl;
-			
-			// ✅ P0修复:计数器已在create_or_get_bill_cache_for_photo中递增
-			// 此处不再重复递增,避免计数器超过实际照片组数
-			g_metrics.record_photo_upload(lo_success, hi_success, group_success);
-			break;
-		}
-
-        // ✅ P1修复: 只有组成功时才更新单张照片的成功状态,避免失败组污染全局状态
-        // 移除了 "即使组失败,也要更新单张照片的成功状态" 的逻辑
-
-		if (result.success_groups >= REQUIRED_SUCCESS_GROUPS) break;
-		if (g_running && group < g_max_photo_groups - 1) {
-			std::this_thread::sleep_for(std::chrono::milliseconds(300));
-		}
-	}
-
-	// 精确更新每张照片的独立状态
-	if (plate_info->db_id > 0) {
-		db_update_upload_status(plate_info->db_id,
-			result.lo_success ? 1 : 0,
-			result.hi_success ? 1 : 0);
-	}
-
-	{
-		std::lock_guard<std::mutex> lock(g_out_record_mtx);
-		auto it = g_out_upload_records.find(plate);
-		if (it != g_out_upload_records.end()) {
-			it->second.photo_success_count += result.success_groups;
-			it->second.trigger_time = time(NULL);
-			it->second.generation++;
-		}
-	}
-
-	if (result.success_groups >= REQUIRED_SUCCESS_GROUPS) {
-		bool should_send = false;
-		{
-			std::lock_guard<std::mutex> lock(g_out_record_mtx);
-			auto it = g_out_upload_records.find(plate);
-			if (it != g_out_upload_records.end() && !it->second.feishu_sent) {
-				should_send = true;
-				it->second.feishu_sent = true;
-			}
-		}
-
-		if (should_send) {  // ✅ FIX9-1: 常规飞书消息不受WarningSigns控制,始终发送
-			std::cout << "[出站] " << plate << " 发送飞书消息..." << std::endl;
-			std::string token = get_cached_tenant_token();
-			if (!token.empty()) {
-				std::string platenumcolor = is_special ? "黄色" : plate_info->type;
-				std::string vehicleType = is_special ? "渣土车" : "";
-				bool feishu_ok = send_feishu_msg(token,
-					project_name, plate, get_format_time(),
-					point_number, throughway,
-					"出站", platenumcolor, vehicleType,
-					tb_num);
-				if (plate_info->db_id > 0) {
-					db_update_feishu_status(plate_info->db_id, feishu_ok ? 1 : 2);
-				}
-			}
-		}
-
-		// ✅ fix10: 出站锁定已在 try_lock_out_station 中完成,此处仅确认
-		lock_out_station(plate);
-	}
-
-	return result;
-}

+ 100 - 0
车牌识别系统_技术问答归档.md

@@ -0,0 +1,100 @@
+# 车牌识别系统 - 技术问答归档
+
+> 本文档记录车牌识别系统 PlateRecApp 开发过程中的所有技术问答  
+> 持续更新,按时间倒序排列  
+> 最后更新: 2026-10-10
+
+---
+
+## 归档索引
+
+| 编号 | 日期 | 分类 | 问题摘要 |
+|------|------|------|---------|
+| Q-001 | 2026-10-10 | 代码审查 | 共享摄像头模式方向标记错误 |
+| Q-002 | 2026-10-10 | 代码审查 | send_permanent_failure_alert 空指针解引用 |
+| Q-003 | 2026-10-10 | 代码审查 | 版本号不一致 |
+| Q-004 | 2026-10-10 | 代码审查 | 生产环境日志污染 |
+| Q-005 | 2026-10-10 | 代码审查 | SSH API JSON解析脆弱 |
+| Q-006 | 2026-10-10 | 配置 | C++标准版本不一致 |
+
+---
+
+## Q-001: 共享摄像头模式方向标记错误
+
+**日期**: 2026-10-10 | **分类**: 代码审查 | **严重性**: P0
+
+**问题**: `plate_recognizer.cpp:380-381` 中 `is_inbound`/`is_outbound` 使用原始 `cap_info->dtype` 而非 `effective_cap_info->dtype`
+
+**代码片段**:
+```cpp
+// 错误:使用 cap_info 而非 effective_cap_info
+plate_info->is_inbound = (cap_info->dtype == STATION_IN);
+plate_info->is_outbound = (cap_info->dtype == STATION_OUT);
+```
+
+**修复方案**:
+```cpp
+plate_info->is_inbound = (effective_cap_info->dtype == STATION_IN);
+plate_info->is_outbound = (effective_cap_info->dtype == STATION_OUT);
+```
+
+**影响**: 共享摄像头模式下进出站方向标记错误,上传数据异常
+
+---
+
+## Q-002: send_permanent_failure_alert 空指针解引用
+
+**日期**: 2026-10-10 | **分类**: 代码审查 | **严重性**: P1
+
+**问题**: `main.cpp:354,615,626` 直接访问 `cap_info_copy->dtype` 未检查 null
+
+**修复方案**:
+```cpp
+(plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN)
+```
+
+**影响**: 极端情况下程序崩溃
+
+---
+
+## Q-003: 版本号不一致
+
+**日期**: 2026-10-10 | **分类**: 代码审查 | **严重性**: P2
+
+**问题**: `main.cpp:305` 启动消息仍为 "v43.2 fix19",应为 "fix27 v44"
+
+**修复**: 更新版本字符串
+
+---
+
+## Q-004: 生产环境日志污染
+
+**日期**: 2026-10-10 | **分类**: 代码审查 | **严重性**: P2
+
+**问题**: `network_client.cpp:30` 每次打印完整 API 响应到 stdout
+
+**修复**: 改为 `if (DEBUG_LOG) { ... }`
+
+---
+
+## Q-005: SSH API JSON 解析脆弱
+
+**日期**: 2026-10-10 | **分类**: 代码审查 | **严重性**: P3
+
+**问题**: `web_server.cpp:461` 使用 `rfind('"')` 解析 JSON,可能匹配错误位置
+
+**修复**: 改用 `cJSON_Parse` 安全解析
+
+---
+
+## Q-006: C++ 标准版本不一致
+
+**日期**: 2026-10-10 | **分类**: 文档 | **严重性**: P3
+
+**问题**: README 标注 C++23,CMakeLists.txt 实际设置 C++17
+
+**修复**: README 更正为 C++17
+
+---
+
+> **归档规则**:每次 Code Review 发现的问题和用户技术问答必须归档,按编号递增,持续更新