2 次代碼提交 25a74cf281 ... 276deacd29

作者 SHA1 備註 提交日期
  steven_roc 276deacd29 fix27 v44: comprehensive bug fixes (19 bugs found in code review) 22 小時之前
  steven_roc 4d608333cc chore: remove old backup files and add Q&A archive 22 小時之前
共有 11 個文件被更改,包括 2781 次插入 和 3249 次删除
  1. 1655 0
      INSTALL_RPI5.md
  2. 1005 0
      TECH_DOC.md
  3. 0 311
      src/frpc_manager1.cpp
  4. 8 4
      src/log_manager.cpp
  5. 6 5
      src/main.cpp
  6. 0 202
      src/mqtt_client1.cpp
  7. 0 742
      src/network_client1.cpp
  8. 0 247
      src/system_monitor1.cpp
  9. 7 6
      src/web_server.cpp
  10. 0 1732
      src/web_server1.cpp
  11. 100 0
      车牌识别系统_技术问答归档.md

+ 1655 - 0
INSTALL_RPI5.md

@@ -0,0 +1,1655 @@
+# PlateRecApp 树莓派5安装编译运行完整指南
+
+> **适用平台**: 树莓派 5 8G (BCM2712, Cortex-A76, aarch64)  
+> **操作系统**: Ubuntu 24.04 LTS Server (aarch64)  
+> **项目版本**: fix27 v44 | 2026-08-30  
+> **C++标准**: C++17 | **编译器**: GCC 13+  
+> **本文档持续更新**
+
+---
+
+## 目录
+
+1. [系统准备](#1-系统准备)
+2. [安装所有依赖](#2-安装所有依赖)
+3. [编译安装 MNN](#3-编译安装-mnn)
+4. [安装 HyperLPR3 SDK](#4-安装-hyperlpr3-sdk)
+5. [编译 PlateRecApp](#5-编译-plateRecApp)
+6. [部署运行](#6-部署运行)
+7. [常见问题排查](#7-常见问题排查)
+8. [性能优化建议](#8-性能优化建议)
+
+---
+
+## 1. 系统准备
+
+### 1.1 安装 Ubuntu 24.04
+
+#### 1.1.1 下载 Raspberry Pi Imager
+
+从官方下载最新版本的 Raspberry Pi Imager:
+
+| 平台 | 下载地址 |
+|------|---------|
+| Windows | https://downloads.raspberrypi.org/imager/imager_latest.exe |
+| macOS | https://downloads.raspberrypi.org/imager/imager_latest.dmg |
+| Linux (AppImage) | https://downloads.raspberrypi.org/imager/imager_latest.appimage |
+| Ubuntu (snap) | `sudo snap install rpi-imager` |
+
+#### 1.1.2 烧录 Ubuntu 24.04 Server
+
+1. 插入 microSD 卡(建议 64GB 以上,A2 等级,推荐 SanDisk Extreme)
+2. 打开 Raspberry Pi Imager
+3. 点击 **CHOOSE DEVICE** → 选择 **Raspberry Pi 5**
+4. 点击 **CHOOSE OS** → **Other general-purpose OS** → **Ubuntu** → **Ubuntu Server 24.04 LTS (64-bit)**
+5. 点击 **CHOOSE STORAGE** → 选择你的 microSD 卡
+
+#### 1.1.3 配置高级选项(重要)
+
+点击右下角齿轮图标(或按 `Ctrl+Shift+X`)进入高级选项:
+
+```yaml
+# 主机名
+hostname: plate-rec
+
+# 启用 SSH(必须!)
+Enable SSH: ✅ Use password authentication
+
+# 设置用户名和密码
+Username: pi
+Password: <your-password>
+
+# WiFi 配置(如果需要无线连接)
+Configure wireless LAN:
+  SSID: YourWiFiName
+  Password: YourWiFiPassword
+  Wireless LAN country: CN
+
+# 区域设置
+Set locale settings:
+  Time zone: Asia/Shanghai
+  Keyboard layout: cn
+```
+
+#### 1.1.4 首次启动配置
+
+烧录完成后插入树莓派,接通电源启动。等待 1-2 分钟后通过 SSH 连接:
+
+```bash
+# 连接方式1: 通过主机名
+ssh pi@plate-rec.local
+
+# 连接方式2: 通过 IP 地址(在路由器管理页面查看)
+ssh pi@192.168.x.x
+
+# 首次登录,更新系统
+sudo apt update && sudo apt upgrade -y
+sudo reboot
+```
+
+> **⚠️ 注意**: 树莓派5 的 Wi-Fi 支持 Wi-Fi 6 (802.11ac/ax),但强烈建议使用有线以太网连接以获得稳定的视频流传输。
+
+#### 1.1.5 扩展文件系统(如果使用较小容量的卡)
+
+```bash
+# Ubuntu 24.04 通常会自动扩展,如果没有:
+sudo raspi-config
+# 选择 Advanced Options → Expand Filesystem → 重启
+```
+
+### 1.2 系统更新与基础工具
+
+```bash
+# 更新系统到最新
+sudo apt update && sudo apt upgrade -y
+
+# 安装基础编译工具链
+sudo apt install -y \
+    build-essential \
+    cmake \
+    git \
+    wget \
+    curl \
+    pkg-config \
+    unzip \
+    tree \
+    htop \
+    nano \
+    net-tools \
+    lsb-release
+
+# 验证系统信息
+uname -a
+# 预期输出: Linux plate-rec 6.x.x xxx aarch64 GNU/Linux
+
+cat /etc/os-release
+# 预期输出: Ubuntu 24.04.x LTS (Noble Numbat)
+
+gcc --version
+# 预期输出: gcc (Ubuntu 13.x.x) 13.x.x
+
+cmake --version
+# 预期输出: cmake version 3.28.x
+
+# 验证内存
+free -h
+# 预期输出: 约 7.8GB 可用
+
+# 查看 CPU 信息
+lscpu
+# 预期: ARMv8 Processor rev 0 (v8l), 4 cores
+```
+
+### 1.3 配置静态IP(可选)
+
+#### 1.3.1 使用 Netplan 配置静态IP
+
+Ubuntu 24.04 使用 Netplan 管理网络配置。
+
+```bash
+# 查看当前网络接口
+ip link show
+# 通常有线网卡为 eth0 或 end0
+
+# 查看当前 Netplan 配置
+cat /etc/netplan/50-cloud-init.yaml
+# 或
+ls /etc/netplan/
+```
+
+创建或编辑配置文件:
+
+```bash
+sudo nano /etc/netplan/01-static.yaml
+```
+
+写入以下内容(根据实际网卡名修改):
+
+```yaml
+# /etc/netplan/01-static.yaml
+network:
+  version: 2
+  renderer: networkd
+  ethernets:
+    end0:  # 树莓派5有线网卡名通常是 end0,用 ip link show 确认
+      dhcp4: no
+      addresses:
+        - 192.168.1.100/24    # 修改为你需要的静态IP
+      routes:
+        - to: default
+          via: 192.168.1.1    # 网关地址
+      nameservers:
+        addresses:
+          - 223.5.5.5         # 阿里DNS
+          - 8.8.8.8           # Google DNS
+```
+
+应用配置:
+
+```bash
+# 验证配置语法
+sudo netplan try
+
+# 确认无误后应用
+sudo netplan apply
+
+# 验证IP
+ip addr show end0
+```
+
+#### 1.3.2 配置 mDNS(可选)
+
+如果需要通过 `plate-rec.local` 访问:
+
+```bash
+sudo apt install -y avahi-daemon
+sudo systemctl enable avahi-daemon
+sudo systemctl start avahi-daemon
+```
+
+---
+
+## 2. 安装所有依赖
+
+按以下顺序安装所有编译和运行依赖。所有命令均为精确的 `apt` 命令。
+
+### 2.1 一键安装全部依赖(推荐)
+
+```bash
+sudo apt install -y \
+    libopencv-dev \
+    libavformat-dev \
+    libavcodec-dev \
+    libavutil-dev \
+    libswscale-dev \
+    libsqlite3-dev \
+    libcurl4-openssl-dev \
+    libssl-dev \
+    libfreetype-dev \
+    libcjson-dev \
+    libmosquitto-dev \
+    libmosquittopp-dev \
+    libprotobuf-dev \
+    protobuf-compiler \
+    libcrypt-dev
+```
+
+### 2.2 分步安装(便于排查问题)
+
+#### 2.2.1 OpenCV 4.x(计算机视觉库)
+
+```bash
+sudo apt install -y libopencv-dev
+
+# 验证
+pkg-config --modversion opencv4
+# 预期输出: 4.x.x(Ubuntu 24.04 默认提供 OpenCV 4.x)
+
+# 检查头文件
+pkg-config --cflags opencv4
+# 应包含 -I/usr/include/opencv4
+
+# 检查库
+pkg-config --libs opencv4
+```
+
+> **说明**: OpenCV 用于图像处理、ROI 裁剪、格式转换等。系统包约占用 200-300MB 磁盘空间。
+
+#### 2.2.2 FFmpeg(视频解码库,v43+ 核心依赖)
+
+```bash
+sudo apt install -y \
+    libavformat-dev \
+    libavcodec-dev \
+    libavutil-dev \
+    libswscale-dev
+
+# 验证
+pkg-config --modversion libavformat
+pkg-config --modversion libavcodec
+pkg-config --modversion libavutil
+pkg-config --modversion libswscale
+
+# 可选:安装 ffmpeg 命令行工具用于测试 RTSP 流
+sudo apt install -y ffmpeg
+```
+
+> **说明**: FFmpeg 用于 RTSP 视频流解码,支持 HEVC DRM 硬件解码和 H.264 软解。这是 v43 版本的核心新增依赖。
+
+#### 2.2.3 SQLite3(轻量级数据库)
+
+```bash
+sudo apt install -y libsqlite3-dev
+
+# 验证
+pkg-config --modversion sqlite3
+# 预期输出: 3.x.x
+
+# 可选:安装命令行工具
+sudo apt install -y sqlite3
+```
+
+#### 2.2.4 cURL + OpenSSL(网络/加密库)
+
+```bash
+sudo apt install -y libcurl4-openssl-dev libssl-dev
+
+# 验证
+pkg-config --modversion libcurl
+pkg-config --modversion openssl
+
+# 检查 cURL 支持 HTTPS
+curl --version | grep -i https
+```
+
+#### 2.2.5 FreeType2(字体渲染库)
+
+```bash
+sudo apt install -y libfreetype-dev
+
+# 验证
+pkg-config --modversion freetype2
+# 预期输出: 2x.x.x
+```
+
+> **说明**: FreeType2 用于在图像上绘制中文字符(通过 cvxFont 模块)。
+
+#### 2.2.6 cJSON(JSON 解析库)
+
+```bash
+sudo apt install -y libcjson-dev
+
+# 验证
+pkg-config --modversion cjson
+# 预期输出: 1.7.x
+
+# 检查头文件位置
+dpkg -L libcjson-dev | grep cJSON.h
+# 通常在 /usr/include/cjson/cJSON.h
+```
+
+#### 2.2.7 MQTT(消息队列库)
+
+```bash
+sudo apt install -y libmosquitto-dev libmosquittopp-dev
+
+# 验证
+ls /usr/lib/aarch64-linux-gnu/libmosquitto*
+ls /usr/lib/aarch64-linux-gnu/libmosquittopp*
+
+# 检查头文件
+ls /usr/include/mosquitto.h
+ls /usr/include/mosquittopp.h
+```
+
+> **说明**: mosquitto 是 C 库,mosquittopp 是 C++ 封装库。两者都需要安装。
+
+#### 2.2.8 Protobuf(协议缓冲区)
+
+```bash
+sudo apt install -y libprotobuf-dev protobuf-compiler
+
+# 验证
+protoc --version
+# 预期输出: libprotoc 3.x.x
+
+pkg-config --modversion protobuf
+```
+
+#### 2.2.9 libcrypt(密码哈希库,fix24 认证系统)
+
+```bash
+sudo apt install -y libcrypt-dev
+
+# 验证
+ls /usr/lib/aarch64-linux-gnu/libcrypt.so
+ls /usr/include/crypt.h
+```
+
+> **说明**: libcrypt 提供 `crypt_r()` 函数用于 bcrypt 密码哈希。这是 fix24 认证系统的必要依赖。
+
+### 2.3 验证所有依赖安装完成
+
+```bash
+# 一键验证脚本
+echo "=== 依赖验证 ==="
+pkg-config --modversion opencv4 && echo "✅ OpenCV" || echo "❌ OpenCV"
+pkg-config --modversion libavformat && echo "✅ FFmpeg-libavformat" || echo "❌ FFmpeg-libavformat"
+pkg-config --modversion libavcodec && echo "✅ FFmpeg-libavcodec" || echo "❌ FFmpeg-libavcodec"
+pkg-config --modversion libavutil && echo "✅ FFmpeg-libavutil" || echo "❌ FFmpeg-libavutil"
+pkg-config --modversion libswscale && echo "✅ FFmpeg-libswscale" || echo "❌ FFmpeg-libswscale"
+pkg-config --modversion sqlite3 && echo "✅ SQLite3" || echo "❌ SQLite3"
+pkg-config --modversion libcurl && echo "✅ cURL" || echo "❌ cURL"
+pkg-config --modversion openssl && echo "✅ OpenSSL" || echo "❌ OpenSSL"
+pkg-config --modversion freetype2 && echo "✅ FreeType2" || echo "❌ FreeType2"
+pkg-config --modversion cjson && echo "✅ cJSON" || echo "❌ cJSON"
+ls /usr/lib/aarch64-linux-gnu/libmosquitto.so && echo "✅ Mosquitto" || echo "❌ Mosquitto"
+ls /usr/lib/aarch64-linux-gnu/libmosquittopp.so && echo "✅ Mosquittopp" || echo "❌ Mosquittopp"
+protoc --version && echo "✅ Protobuf" || echo "❌ Protobuf"
+ls /usr/lib/aarch64-linux-gnu/libcrypt.so && echo "✅ libcrypt" || echo "❌ libcrypt"
+echo "=== 验证完成 ==="
+```
+
+---
+
+## 3. 编译安装 MNN
+
+MNN(Mobile Neural Network)是阿里巴巴开源的轻量级深度学习推理引擎,HyperLPR3 依赖 MNN 进行车牌检测与识别模型的推理。
+
+### 3.1 克隆源码
+
+```bash
+cd ~
+git clone https://github.com/alibaba/MNN.git
+cd MNN
+
+# 切换到稳定版本(推荐使用 2.9.x 或更高)
+git checkout 2.9.6
+# 或使用 master 分支
+# git checkout master
+```
+
+### 3.2 编译(aarch64 专用参数)
+
+```bash
+cd ~/MNN
+mkdir -p build && cd build
+```
+
+#### 完整 CMake 配置命令及参数说明
+
+```bash
+cmake .. \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DMNN_BUILD_SHARED_LIBS=ON \
+    -DMNN_BUILD_CONVERTER=OFF \
+    -DMNN_BUILD_DEMO=OFF \
+    -DMNN_BUILD_TOOLS=OFF \
+    -DMNN_BUILD_TRAIN=OFF \
+    -DMNN_BUILD_QUANTOOLS=OFF \
+    -DMNN_BUILD_MINIMAL=OFF \
+    -DMNN_ARM82=ON \
+    -DMNN_FP16=ON \
+    -DMNN_OPENCL=OFF \
+    -DMNN_VULKAN=OFF \
+    -DMNN_METAL=OFF \
+    -DMNN_USE_THREAD_POOL=ON \
+    -DMNN_NUM_THREADS=4 \
+    -DCMAKE_INSTALL_PREFIX=/usr/local
+```
+
+**参数详细说明**:
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| `CMAKE_BUILD_TYPE` | Release | 发布模式,启用 -O3 优化 |
+| `MNN_BUILD_SHARED_LIBS` | ON | 编译为动态库 (.so),减小程序体积 |
+| `MNN_BUILD_CONVERTER` | OFF | 不编译模型转换工具(节省编译时间和空间) |
+| `MNN_BUILD_DEMO` | OFF | 不编译示例程序 |
+| `MNN_BUILD_TOOLS` | OFF | 不编译额外工具 |
+| `MNN_BUILD_TRAIN` | OFF | 不编译训练模块 |
+| `MNN_BUILD_QUANTOOLS` | OFF | 不编译量化工具 |
+| `MNN_BUILD_MINIMAL` | OFF | 不启用最小化构建 |
+| **`MNN_ARM82`** | **ON** | **关键参数!启用 ARMv8.2 fp16 指令集加速,树莓派5 Cortex-A76 支持** |
+| **`MNN_FP16`** | **ON** | **启用半精度浮点运算,配合 ARM82 大幅提升推理速度** |
+| `MNN_OPENCL` | OFF | 不使用 OpenCL GPU 加速(树莓派5 支持有限) |
+| `MNN_VULKAN` | OFF | 不使用 Vulkan GPU 加速 |
+| `MNN_METAL` | OFF | Apple Metal 专用,Linux 不需要 |
+| `MNN_USE_THREAD_POOL` | ON | 启用线程池,提升多核利用率 |
+| `MNN_NUM_THREADS` | 4 | 线程数设为 4,匹配树莓派5的4个核心 |
+| `CMAKE_INSTALL_PREFIX` | /usr/local | 安装到 /usr/local 目录 |
+
+#### 开始编译
+
+```bash
+# 使用 4 核并行编译(树莓派5 有 4 个核心)
+make -j4
+
+# 编译过程约需 5-15 分钟,取决于 microSD 卡速度
+# 使用 SSD/USB 会更快
+
+# 安装
+sudo make install
+
+# 更新动态链接库缓存
+sudo ldconfig
+```
+
+### 3.3 验证安装
+
+```bash
+# 检查头文件
+ls /usr/local/include/MNN/
+# 应包含: Interpreter.hpp, MNNDefine.h, MNNForwardType.h 等
+
+# 检查库文件
+ls /usr/local/lib/libMNN*
+# 应包含: libMNN.so, libMNN*.so 等
+
+# 使用 pkg-config 验证(如果 MNN 提供了 .pc 文件)
+pkg-config --libs MNN 2>/dev/null || echo "MNN 无 pkg-config 文件,使用手动路径"
+
+# 验证库是否可加载
+ldconfig -p | grep MNN
+```
+
+---
+
+## 4. 安装 HyperLPR3 SDK
+
+HyperLPR3 是开源车牌识别库,提供车牌检测、字符识别的预训练模型和推理接口。
+
+### 4.1 预编译 SDK 安装(推荐)
+
+#### 4.1.1 下载 SDK
+
+```bash
+cd /root/PlateRecApp
+mkdir -p hyperlpr3
+cd hyperlpr3
+
+# 下载预编译的 aarch64 Linux SDK
+# 方式1: 从 GitHub Releases 下载
+wget https://github.com/nicklasxyz/hyperlpr3/releases/download/v0.1.0/hyperlpr3-linux-aarch64.tar.gz
+
+# 方式2: 如果上述链接不可用,从 HyperLPR 官方获取
+# https://github.com/nicklasxyz/HyperLPR3
+# 注意:需要根据实际发布版本调整 URL
+
+# 方式3: 从自有服务器下载(如果有预编译版本)
+# wget http://your-server/hyperlpr3-linux-aarch64.tar.gz
+```
+
+#### 4.1.2 解压并安装
+
+```bash
+cd /root/PlateRecApp/hyperlpr3
+
+# 解压
+tar -xzf hyperlpr3-linux-aarch64.tar.gz
+
+# 预期目录结构
+tree /root/PlateRecApp/hyperlpr3/
+```
+
+**标准目录结构**:
+
+```
+hyperlpr3/
+├── include/                 # 头文件目录
+│   ├── hyperlpr3.h          # C API 头文件
+│   └── hyper_lpr_sdk.h     # SDK 接口头文件
+├── lib/                     # 库文件目录
+│   ├── libhyperlpr3.so      # 主库(动态链接)
+│   └── libMNN.so            # MNN 运行时库(如果未单独安装 MNN)
+├── models/                  # 模型文件目录
+│   ├── det/                 # 车牌检测模型
+│   │   └── det.mnn         # MNN 格式检测模型
+│   ├── rec/                 # 字符识别模型
+│   │   └── rec.mnn         # MNN 格式识别模型
+│   └── seg/                 # 分割模型(如果有)
+│       └── seg.mnn
+└── README.md
+```
+
+#### 4.1.3 运行时库路径配置
+
+```bash
+# 添加 HyperLPR3 库路径到系统动态链接器
+echo "/root/PlateRecApp/hyperlpr3/lib" | sudo tee /etc/ld.so.conf.d/hyperlpr3.conf
+
+# 更新动态链接库缓存
+sudo ldconfig
+
+# 验证库是否被识别
+ldconfig -p | grep hyperlpr3
+```
+
+### 4.2 源码编译安装(备选)
+
+如果预编译 SDK 不可用,可自行编译:
+
+```bash
+cd ~
+git clone https://github.com/nicklasxyz/HyperLPR3.git
+cd HyperLPR3
+
+# 编译(需要已安装 MNN)
+mkdir build && cd build
+cmake .. \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DMNN_DIR=/usr/local \
+    -DCMAKE_INSTALL_PREFIX=/root/PlateRecApp/hyperlpr3
+
+make -j4
+make install
+```
+
+> **⚠️ 注意**: 源码编译需要确保 MNN 已正确安装,且需要下载模型文件放到 models/ 目录。
+
+---
+
+## 5. 编译 PlateRecApp
+
+### 5.1 克隆代码
+
+```bash
+cd /root
+git clone https://git.zhonjin.com:40717/steven_roc/PlateRecApp.git
+cd PlateRecApp
+
+# 如果使用已有代码,直接进入目录
+# cd /root/PlateRecApp
+
+# 查看当前版本
+head -5 README.md
+```
+
+### 5.2 检查项目结构
+
+确认以下文件和目录存在:
+
+```bash
+# 检查源码目录
+ls src/
+# 应包含: main.cpp, config.cpp, plate_recognizer.cpp, web_server.cpp 等
+
+# 检查第三方依赖
+ls third_party/
+# 应包含: cvxFont.cpp, cvxFont.h, httplib.h, ini.c, ini.h, md5ex1.c, md5ex1.h
+
+# 检查 HyperLPR3 SDK
+ls hyperlpr3/include/ hyperlpr3/lib/
+# 应包含头文件和库文件
+
+# 检查 CMakeLists.txt
+ls CMakeLists.txt
+```
+
+### 5.3 CMake 配置
+
+```bash
+cd /root/PlateRecApp
+mkdir -p build && cd build
+
+cmake .. \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DUSE_STATIC_LIBS=OFF \
+    -DCMAKE_CXX_FLAGS="-march=armv8.2-a+fp16" \
+    -DCMAKE_C_FLAGS="-march=armv8.2-a+fp16"
+```
+
+**CMake 参数说明**:
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| `CMAKE_BUILD_TYPE` | Release | 发布模式,启用 -O3 优化 + strip |
+| `USE_STATIC_LIBS` | OFF | 使用动态链接,减小程序体积 |
+| `CMAKE_CXX_FLAGS` | `-march=armv8.2-a+fp16` | 启用 ARMv8.2 fp16 指令集 |
+| `CMAKE_C_FLAGS` | `-march=armv8.2-a+fp16` | 同上,用于 C 文件 |
+
+> **注意**: CMake 配置阶段会检查所有依赖。如果某个依赖缺失,会报 `FATAL_ERROR`。请根据错误提示安装缺失的库。
+
+**CMake 预期输出**(关键信息):
+
+```
+-- ========================================
+-- 项目配置信息:
+--   项目名称: HyperLPR3-LinuxAPP
+--   项目版本: 1.0.0
+--   C++ 标准: C++17
+--   构建类型: Release
+--   使用静态库: OFF
+-- ----------------------------------------
+-- 库路径信息:
+--   OpenCV: /usr/include/opencv4
+--   HyperLPR3: /root/PlateRecApp/hyperlpr3/include
+--   cJSON 库: /usr/lib/aarch64-linux-gnu/libcjson.so
+--   libcrypt: /usr/lib/aarch64-linux-gnu/libcrypt.so
+-- ========================================
+```
+
+### 5.4 编译
+
+```bash
+cd /root/PlateRecApp/build
+
+# 使用 4 核并行编译
+make -j4
+
+# 编译过程约需 3-10 分钟
+# Release 模式下会自动 strip 符号表
+
+# 如果编译出错,可查看详细错误
+make VERBOSE=1
+```
+
+### 5.5 验证编译产物
+
+```bash
+# 检查可执行文件
+ls -lh /root/PlateRecApp/build/PlateRecApp
+# 应为一个可执行文件
+
+# 查看文件信息
+file /root/PlateRecApp/build/PlateRecApp
+# 预期: ELF 64-bit LSB executable, ARM aarch64
+
+# 查看动态链接依赖
+ldd /root/PlateRecApp/build/PlateRecApp
+# 检查所有依赖库都能找到(无 "not found")
+
+# 快速测试运行(会因缺少 config.ini 报错,但不应报库缺失)
+./PlateRecApp --help 2>&1 || true
+```
+
+**关键验证**: `ldd` 输出中不应出现 `not found`,否则需要安装缺失的库。
+
+---
+
+## 6. 部署运行
+
+### 6.1 目录结构
+
+生产环境推荐以下目录结构:
+
+```
+/root/PlateRecApp/              # 项目根目录
+├── PlateRecApp                 # 编译产物(可执行文件)或 build/PlateRecApp
+├── config.ini                  # 主配置文件
+├── hyperlpr3/                  # HyperLPR3 SDK
+│   ├── include/
+│   ├── lib/
+│   └── models/
+├── data/                       # 运行时数据目录
+│   ├── plate_rec.db            # SQLite 数据库
+│   └── auth.db                 # 认证数据库
+├── fonts/                      # 字体文件目录
+│   └── font.ttf               # 中文字体文件
+├── html/                       # Web 前端文件(如果有)
+├── logs/                       # 日志目录
+│   └── plate_rec.log
+├── scripts/                    # 脚本目录
+└── src/                        # 源码目录
+```
+
+创建必要目录:
+
+```bash
+cd /root/PlateRecApp
+mkdir -p data fonts html logs scripts
+
+# 复制编译产物到项目根目录(可选)
+cp build/PlateRecApp ./PlateRecApp
+chmod +x ./PlateRecApp
+```
+
+### 6.2 config.ini 配置
+
+创建配置文件:
+
+```bash
+cat > /root/PlateRecApp/config.ini << 'EOF'
+[system]
+# 设备名称
+device_name=plate-rec-01
+# 日志级别: DEBUG, INFO, WARN, ERROR
+log_level=INFO
+# 日志文件路径
+log_path=/root/PlateRecApp/logs/plate_rec.log
+# 日志最大大小(MB)
+log_max_size=50
+# 日志保留份数
+log_backup_count=3
+
+[rtsp]
+# RTSP 视频流地址(支持最多4路)
+# 格式: rtsp://username:password@ip:port/stream
+url1=rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101
+url2=
+url3=
+url4=
+# 每路流的 ROI 区域 (x,y,w,h) 逗号分隔
+roi1=0,0,1920,1080
+roi2=0,0,1920,1080
+roi3=0,0,1920,1080
+roi4=0,0,1920,1080
+# 解码模式: auto, drm(HEVC硬解), cpu(H264软解)
+decode_mode=auto
+# 帧跳过间隔(每N帧处理1帧)
+frame_skip=3
+
+[plate_recognition]
+# 置信度阈值 (0.0-1.0)
+confidence_threshold=0.5
+# 模型路径
+model_dir=/root/PlateRecApp/hyperlpr3/models
+
+[database]
+# SQLite 数据库路径
+db_path=/root/PlateRecApp/data/plate_rec.db
+
+[web_server]
+# Web 管理后台端口
+port=8080
+# 静态文件目录
+html_dir=/root/PlateRecApp/html
+# 超级管理员账号(首次启动会自动创建)
+admin_user=admin
+admin_password=zhonjin188A
+
+[feishu]
+# 飞书推送开关
+enabled=false
+# 飞书 Webhook URL
+webhook_url=https://open.feishu.cn/open-apis/bot/v2/hook/your-hook-id
+# 推送消息模板
+entry_msg=🟢 进站 | 车牌: {plate} | 时间: {time}
+exit_msg=🔴 出站 | 车牌: {plate} | 时间: {time}
+
+[mqtt]
+# MQTT 上报开关
+enabled=false
+# Broker 地址
+broker=tcp://192.168.1.200:1883
+# 客户端 ID
+client_id=plate_rec_01
+# 主题
+topic=plate_recognition/result
+# 用户名密码(如果有)
+username=
+password=
+
+[station_lock]
+# 站点锁定超时(秒)
+lock_timeout=120
+
+[frpc]
+# frpc 可执行文件路径
+frpc_path=/usr/local/bin/frpc
+# frpc 配置文件路径
+frpc_config_path=/root/PlateRecApp/frpc.toml
+# frpc admin API 端口(仅允许 127.0.0.1 访问)
+admin_port=7400
+EOF
+```
+
+> **⚠️ 重要**: 请根据实际环境修改 RTSP 地址、飞书 Webhook、MQTT Broker 等配置。
+
+### 6.3 systemd service 配置
+
+创建系统服务文件:
+
+```bash
+sudo tee /etc/systemd/system/plate-rec.service << 'EOF'
+[Unit]
+Description=PlateRecApp - License Plate Recognition Service
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=root
+Group=root
+WorkingDirectory=/root/PlateRecApp
+ExecStart=/root/PlateRecApp/PlateRecApp
+Restart=always
+RestartSec=5
+StartLimitIntervalSec=60
+StartLimitBurst=3
+
+# 环境变量
+Environment=LD_LIBRARY_PATH=/root/PlateRecApp/hyperlpr3/lib:/usr/local/lib
+Environment=MNN_NUM_THREADS=4
+
+# 资源限制
+LimitNOFILE=65536
+LimitNPROC=4096
+MemoryMax=2G
+CPUQuota=300%
+
+# 安全加固
+NoNewPrivileges=yes
+ProtectSystem=strict
+ReadWritePaths=/root/PlateRecApp
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+
+# 日志
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=plate-rec
+
+[Install]
+WantedBy=multi-user.target
+EOF
+```
+
+**服务文件关键配置说明**:
+
+| 配置项 | 值 | 说明 |
+|--------|-----|------|
+| `LD_LIBRARY_PATH` | hyperlpr3/lib + /usr/local/lib | 确保能找到 MNN 和 HyperLPR3 动态库 |
+| `MNN_NUM_THREADS` | 4 | MNN 推理线程数,匹配树莓派5核心数 |
+| `LimitNOFILE` | 65536 | 最大打开文件数(4路 RTSP 流需要较多 fd) |
+| `MemoryMax` | 2G | 内存上限,防止 OOM |
+| `CPUQuota` | 300% | CPU 使用上限(4 核 = 400%,留 1 核给系统) |
+| `RestartSec` | 5 | 崩溃后 5 秒重启 |
+| `StartLimitBurst` | 3 | 60秒内最多重启 3 次,防止频繁崩溃 |
+
+### 6.4 启动和验证
+
+```bash
+# 重新加载 systemd 配置
+sudo systemctl daemon-reload
+
+# 启用开机自启
+sudo systemctl enable plate-rec
+
+# 启动服务
+sudo systemctl start plate-rec
+
+# 检查服务状态
+sudo systemctl status plate-rec
+
+# 预期输出:
+# ● plate-rec.service - PlateRecApp - License Plate Recognition Service
+#      Loaded: loaded (/etc/systemd/system/plate-rec.service; enabled)
+#      Active: active (running) since ...
+```
+
+**验证运行**:
+
+```bash
+# 检查进程
+ps aux | grep PlateRecApp
+
+# 检查端口监听
+ss -tlnp | grep 8080
+# 或
+netstat -tlnp | grep 8080
+
+# 检查数据库文件是否创建
+ls -la /root/PlateRecApp/data/
+
+# 测试 Web 管理后台
+curl -s http://localhost:8080/ | head -20
+
+# 检查日志输出
+sudo journalctl -u plate-rec -f --no-pager -n 50
+```
+
+### 6.5 日志查看
+
+```bash
+# 实时查看服务日志
+sudo journalctl -u plate-rec -f
+
+# 查看最近 100 行日志
+sudo journalctl -u plate-rec -n 100 --no-pager
+
+# 查看今天的日志
+sudo journalctl -u plate-rec --since today --no-pager
+
+# 查看应用自身日志文件
+tail -f /root/PlateRecApp/logs/plate_rec.log
+
+# 查看应用日志(带颜色)
+tail -f /root/PlateRecApp/logs/plate_rec.log | grep -E "ERROR|WARN" --color=always
+
+# 配置 logrotate 自动清理日志(如果应用未自带)
+sudo tee /etc/logrotate.d/plate-rec << 'LOGEOF'
+/root/PlateRecApp/logs/*.log {
+    daily
+    rotate 7
+    compress
+    delaycompress
+    missingok
+    notifempty
+    create 0644 root root
+    postrotate
+        systemctl reload plate-rec 2>/dev/null || true
+    endscript
+}
+LOGEOF
+```
+
+**常用运维命令**:
+
+```bash
+# 重启服务
+sudo systemctl restart plate-rec
+
+# 停止服务
+sudo systemctl stop plate-rec
+
+# 禁用开机自启
+sudo systemctl disable plate-rec
+
+# 查看服务资源使用
+systemctl status plate-rec
+systemd-cgtop
+
+# 查看 crash 日志
+sudo journalctl -u plate-rec --since "1 hour ago" -p err --no-pager
+```
+
+---
+
+## 7. 常见问题排查
+
+### 编译类问题
+
+#### Q1: CMake 报错 "Could NOT find OpenCV"
+
+**原因**: OpenCV 开发包未安装。
+
+**解决**:
+```bash
+sudo apt install -y libopencv-dev
+# 如果仍有问题,检查 pkg-config
+pkg-config --modversion opencv4
+```
+
+#### Q2: CMake 报错 "Could NOT find CURL" 或 "Failed to find curl.h"
+
+**原因**: cURL 开发包未安装。
+
+**解决**:
+```bash
+sudo apt install -y libcurl4-openssl-dev
+```
+
+#### Q3: 编译报错 "hyper_lpr_sdk.h: No such file or directory"
+
+**原因**: HyperLPR3 SDK 未正确安装到 hyperlpr3/ 目录。
+
+**解决**:
+```bash
+ls /root/PlateRecApp/hyperlpr3/include/hyper_lpr_sdk.h
+# 如果不存在,重新安装 HyperLPR3 SDK(参见第4节)
+```
+
+#### Q4: 链接报错 "undefined reference to `MNN::Interpreter::createFromFile'"
+
+**原因**: MNN 未安装或链接器找不到 MNN 库。
+
+**解决**:
+```bash
+# 检查 MNN 是否安装
+ls /usr/local/lib/libMNN.so
+
+# 如果不存在,重新编译安装 MNN(参见第3节)
+# 如果存在但找不到,更新 ldconfig
+echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/mnn.conf
+sudo ldconfig
+```
+
+#### Q5: 链接报错 "cannot find -lmosquittopp" 或 "cannot find -lcjson"
+
+**原因**: 对应的开发包未安装。
+
+**解决**:
+```bash
+sudo apt install -y libmosquittopp-dev libcjson-dev
+```
+
+#### Q6: 编译报错 "'crypt_r' was not declared in this scope"
+
+**原因**: libcrypt-dev 未安装。
+
+**解决**:
+```bash
+sudo apt install -y libcrypt-dev
+```
+
+#### Q7: 编译报错 "libavformat/libavcodec 相关头文件找不到"
+
+**原因**: FFmpeg 开发包未安装。
+
+**解决**:
+```bash
+sudo apt install -y libavformat-dev libavcodec-dev libavutil-dev libswscale-dev
+pkg-config --modversion libavformat
+```
+
+#### Q8: 链接阶段 OOM (Out of Memory)
+
+**原因**: 树莓派5 8G 内存在链接大型项目时可能不够(特别是 Debug 模式)。
+
+**解决**:
+```bash
+# 方法1: 增加 swap
+sudo fallocate -l 2G /swapfile
+sudo chmod 600 /swapfile
+sudo mkswap /swapfile
+sudo swapon /swapfile
+echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
+
+# 方法2: 使用单线程编译
+make -j1
+
+# 方法3: 使用 Release 模式(体积更小,内存占用更少)
+cmake -DCMAKE_BUILD_TYPE=Release ..
+```
+
+### 运行时问题
+
+#### Q9: 启动报错 "error while loading shared libraries: libMNN.so: cannot open shared object file"
+
+**原因**: 动态链接器找不到 MNN 库。
+
+**解决**:
+```bash
+# 确认库存在
+ls /usr/local/lib/libMNN.so
+
+# 添加路径
+echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/mnn.conf
+sudo ldconfig
+
+# 验证
+ldconfig -p | grep MNN
+```
+
+#### Q10: 启动报错 "error while loading shared libraries: libhyperlpr3.so"
+
+**原因**: HyperLPR3 库路径未配置。
+
+**解决**:
+```bash
+echo "/root/PlateRecApp/hyperlpr3/lib" | sudo tee /etc/ld.so.conf.d/hyperlpr3.conf
+sudo ldconfig
+ldconfig -p | grep hyperlpr3
+```
+
+#### Q11: RTSP 流无法连接,日志报 "Failed to open RTSP stream"
+
+**原因**: 网络不通、RTSP 地址错误或摄像头未开启。
+
+**解决**:
+```bash
+# 测试网络连通性
+ping 192.168.1.100
+
+# 使用 ffmpeg 测试 RTSP 流
+ffmpeg -rtsp_transport tcp -i "rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101" -frames:v 1 test.jpg
+
+# 检查 config.ini 中 RTSP 地址是否正确
+grep "url1=" /root/PlateRecApp/config.ini
+```
+
+#### Q12: Web 管理后台无法访问(浏览器打不开 8080 端口)
+
+**原因**: 服务未启动、端口被占用或防火墙阻止。
+
+**解决**:
+```bash
+# 检查服务状态
+sudo systemctl status plate-rec
+
+# 检查端口
+ss -tlnp | grep 8080
+
+# 检查防火墙
+sudo ufw status
+sudo ufw allow 8080/tcp
+
+# 检查是否有端口冲突
+sudo lsof -i :8080
+```
+
+#### Q13: 飞书推送不工作
+
+**原因**: 配置错误或网络问题。
+
+**解决**:
+```bash
+# 检查配置
+grep -A5 "\[feishu\]" /root/PlateRecApp/config.ini
+
+# 手动测试 Webhook
+curl -X POST "https://open.feishu.cn/open-apis/bot/v2/hook/your-hook-id" \
+  -H "Content-Type: application/json" \
+  -d '{"msg_type":"text","content":{"text":"test"}}'
+
+# 查看日志中飞书相关错误
+journalctl -u plate-rec | grep -i feishu
+```
+
+#### Q14: MQTT 连接失败
+
+**原因**: Broker 地址错误、网络不通或认证失败。
+
+**解决**:
+```bash
+# 检查 Broker 连通性
+mosquitto_sub -h 192.168.1.200 -p 1883 -t "test" -d
+
+# 检查配置
+grep -A10 "\[mqtt\]" /root/PlateRecApp/config.ini
+
+# 查看 MQTT 相关日志
+journalctl -u plate-rec | grep -i mqtt
+```
+
+#### Q15: 程序运行一段时间后崩溃 (Segmentation fault)
+
+**原因**: 可能是内存泄漏、RTSP 断流重连问题或模型加载异常。
+
+**解决**:
+```bash
+# 查看崩溃前的日志
+journalctl -u plate-rec -p err --since "10 min ago"
+
+# 启用 core dump
+sudo sysctl -w kernel.core_pattern=/root/PlateRecApp/logs/core.%e.%p
+
+# 使用 Debug 模式编译以获取更多信息
+cd /root/PlateRecApp/build
+cmake -DCMAKE_BUILD_TYPE=Debug ..
+make -j4
+
+# 检查内存使用
+htop
+free -h
+
+# systemd 会自动重启服务(Restart=always)
+```
+
+#### Q16: 车牌识别率低
+
+**原因**: 摄像头角度、光线、ROI 设置或置信度阈值问题。
+
+**解决**:
+```bash
+# 调整 ROI 区域(通过 Web 管理后台或修改 config.ini)
+# roi1=x,y,width,height
+
+# 降低置信度阈值(可能增加误识别)
+# confidence_threshold=0.3
+
+# 检查摄像头画面质量
+ffmpeg -rtsp_transport tcp -i "rtsp://..." -frames:v 1 snapshot.jpg
+
+# 检查模型文件是否完整
+ls -la /root/PlateRecApp/hyperlpr3/models/
+```
+
+#### Q17: 数据库文件权限问题
+
+**原因**: 服务以 root 用户运行时创建的数据库文件,其他用户无法访问。
+
+**解决**:
+```bash
+# 确保 data 目录权限正确
+ls -la /root/PlateRecApp/data/
+
+# 修复权限
+chmod 644 /root/PlateRecApp/data/*.db
+chown root:root /root/PlateRecApp/data/*.db
+```
+
+#### Q18: config.ini 解析失败
+
+**原因**: 配置文件格式错误(如包含多余空格、编码问题)。
+
+**解决**:
+```bash
+# 检查配置文件编码
+file /root/PlateRecApp/config.ini
+# 应为: ASCII text 或 UTF-8 text
+
+# 检查语法(简单验证)
+cat /root/PlateRecApp/config.ini | grep -v "^#" | grep -v "^$"
+
+# 重新创建配置文件(参考 6.2 节)
+```
+
+#### Q19: frpc 管理功能报错 "Connection refused"
+
+**原因**: frpc 未安装或 admin API 端口未开放。
+
+**解决**:
+```bash
+# 检查 frpc 是否安装
+which frpc
+ls -la /usr/local/bin/frpc
+
+# 检查 frpc 配置文件
+cat /root/PlateRecApp/frpc.toml
+
+# 测试 admin API(仅允许 127.0.0.1)
+curl http://127.0.0.1:7400/api/status
+
+# 安全提示: frpc admin API 禁止通过 frp 隧道暴露到公网!
+```
+
+#### Q20: 登录认证系统无法登录
+
+**原因**: 认证数据库未初始化或密码错误。
+
+**解决**:
+```bash
+# 检查认证数据库
+ls -la /root/PlateRecApp/data/auth.db
+
+# 删除认证数据库让系统重新创建(会丢失用户数据)
+# rm /root/PlateRecApp/data/auth.db
+# sudo systemctl restart plate-rec
+
+# 默认超级管理员账号
+# 用户名: admin
+# 密码: zhonjin188A(在 config.ini 中配置)
+```
+
+#### Q21: 温度过高导致降频
+
+**原因**: 树莓派5 在高负载下温度可达 80°C+,触发降频保护。
+
+**解决**:
+```bash
+# 查看当前温度
+vcgencmd measure_temp
+# 或
+cat /sys/class/thermal/thermal_zone0/temp
+
+# 查看是否降频
+vcgencmd get_throttled
+# 0x0: 正常, 非0: 有降频/ throttling
+
+# 安装散热片/风扇
+# 推荐使用主动散热(风扇)方案
+```
+
+#### Q22: systemd 服务启动失败 "Exit code: 127"
+
+**原因**: 可执行文件路径错误或动态库找不到。
+
+**解决**:
+```bash
+# 检查可执行文件路径
+ls -la /root/PlateRecApp/PlateRecApp
+
+# 手动运行查看错误
+cd /root/PlateRecApp && ./PlateRecApp
+
+# 检查 LD_LIBRARY_PATH 配置
+grep LD_LIBRARY_PATH /etc/systemd/system/plate-rec.service
+
+# 确认所有动态库都能找到
+ldd /root/PlateRecApp/PlateRecApp | grep "not found"
+```
+
+#### Q23: 系统时间不准确
+
+**原因**: NTP 同步未配置或网络不通。
+
+**解决**:
+```bash
+# 检查时间同步状态
+timedatectl status
+
+# 启用 NTP
+sudo timedatectl set-ntp true
+
+# 设置时区
+sudo timedatectl set-timezone Asia/Shanghai
+
+# 手动同步时间
+sudo apt install -y ntpdate
+sudo ntpdate pool.ntp.org
+```
+
+---
+
+## 8. 性能优化建议
+
+### 8.1 CPU 调频优化
+
+树莓派5 的 Cortex-A76 核心最高可达 2.4GHz,默认使用 ondemand 调频策略。对于实时车牌识别场景,建议设置为 performance 模式。
+
+```bash
+# 查看当前 CPU 调频策略
+cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
+
+# 设置为 performance 模式(全速运行)
+echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
+
+# 设置为 powersave 模式(省电)
+echo powersave | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
+
+# 持久化配置(每次启动自动设置)
+sudo apt install -y cpufrequtils
+sudo tee /etc/default/cpufrequtils << 'EOF'
+GOVERNOR=performance
+EOF
+sudo systemctl enable cpufrequtils
+```
+
+> **注意**: performance 模式会持续运行在最高频率,功耗和温度会增加。建议配合主动散热使用。
+
+### 8.2 内存管理优化
+
+```bash
+# 查看内存使用
+free -h
+
+# 设置 swappiness(降低 swap 使用倾向,优先使用物理内存)
+echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf
+sudo sysctl -p
+
+# 预分配大页内存(可选,MNN 推理可能受益)
+# echo "vm.nr_hugepages=16" | sudo tee -a /etc/sysctl.conf
+
+# 禁用不必要的 systemd 服务释放内存
+sudo systemctl disable bluetooth.service 2>/dev/null
+sudo systemctl disable cups.service 2>/dev/null
+sudo systemctl disable ModemManager.service 2>/dev/null
+```
+
+### 8.3 DRM 硬解优化
+
+对于 HEVC (H.265) 编码的 RTSP 流,树莓派5 支持 DRM 硬件解码,可大幅降低 CPU 占用。
+
+```bash
+# 检查 DRM 设备
+ls /dev/dri/
+
+# 检查 V4L2 硬件解码支持
+v4l2-ctl --list-devices
+
+# 在 config.ini 中启用 DRM 硬解
+# decode_mode=drm
+
+# 测试 DRM 硬解
+ffmpeg -hwaccel drm -hwaccel_output_format drm_prime \
+  -rtsp_transport tcp -i "rtsp://..." \
+  -frames:v 10 -f null -
+```
+
+**DRM 硬解 vs 软解对比**:
+
+| 模式 | CPU 占用 | 延迟 | 兼容性 | 推荐场景 |
+|------|---------|------|--------|---------|
+| `auto` | 中 | 低 | 广 | 默认推荐 |
+| `drm` | 极低 (~5%) | 最低 | HEVC 流 | 4路 HEVC 摄像头 |
+| `cpu` | 高 (~60%每路) | 中 | 最广 | H.264 流或兼容性要求 |
+
+### 8.4 RTSP 流优化
+
+```bash
+# 优化网络缓冲区(提高 RTSP 流稳定性)
+sudo sysctl -w net.core.rmem_max=16777216
+sudo sysctl -w net.core.wmem_max=16777216
+
+# 持久化
+cat >> /etc/sysctl.conf << 'EOF'
+net.core.rmem_max=16777216
+net.core.wmem_max=16777216
+net.ipv4.tcp_rmem=4096 87380 16777216
+net.ipv4.tcp_wmem=4096 65536 16777216
+EOF
+sudo sysctl -p
+```
+
+### 8.5 文件系统优化
+
+```bash
+# 使用 tmpfs 存放临时文件(减少 SD 卡写入)
+sudo tee -a /etc/fstab << 'EOF'
+tmpfs /tmp tmpfs defaults,noatime,size=256M 0 0
+tmpfs /var/log tmpfs defaults,noatime,size=128M 0 0
+EOF
+
+# SD 卡优化:禁用 atime 减少写入
+# 在 /etc/fstab 的挂载选项中添加 noatime
+# 例如: /dev/mmcblk0p2 / ext4 defaults,noatime 0 1
+
+# 强烈建议使用 SSD/USB 3.0 存储替代 microSD 卡
+# 通过 USB 3.0 接口连接 SSD,性能提升 5-10 倍
+```
+
+### 8.6 MNN 推理优化
+
+```bash
+# 在 config.ini 或环境变量中配置 MNN 推理参数
+# MNN_NUM_THREADS=4     # 推理线程数,匹配核心数
+# MNN_ARM82=1           # 启用 fp16 加速(编译时已启用)
+
+# 监控推理耗时
+# 查看日志中的推理耗时统计
+journalctl -u plate-rec | grep "inference" | tail -20
+```
+
+### 8.7 散热方案
+
+| 方案 | 散热能力 | 噪音 | 推荐度 |
+|------|---------|------|--------|
+| 被动散热片(铝制) | 中 | 无 | ⭐⭐⭐ |
+| 主动风扇(30mm) | 高 | 低 | ⭐⭐⭐⭐ |
+| 冰塔散热器 | 极高 | 无 | ⭐⭐⭐⭐⭐ |
+| 金属外壳散热 | 高 | 无 | ⭐⭐⭐⭐ |
+
+> **建议**: 4路 RTSP 流 + 车牌识别场景下,强烈推荐使用主动散热方案,确保 CPU 温度低于 70°C。
+
+### 8.8 系统监控
+
+```bash
+# 安装监控工具
+sudo apt install -y htop iotop iftop
+
+# 实时监控
+htop
+
+# 查看温度
+watch -n 1 "cat /sys/class/thermal/thermal_zone0/temp | awk '{print \$1/1000}'"
+
+# 查看 CPU 频率
+watch -n 1 "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq | awk '{print \$1/1000 \" MHz\"}'"
+
+# 查看磁盘 IO
+iotop -o
+
+# 查看网络流量
+iftop -i end0
+```
+
+---
+
+## 附录 A: 完整安装脚本(一键执行)
+
+将以下步骤合并为单个脚本,适合全新安装:
+
+```bash
+#!/bin/bash
+# PlateRecApp 一键安装脚本 (树莓派5 + Ubuntu 24.04)
+# 使用方法: chmod +x install.sh && sudo ./install.sh
+
+set -e
+
+echo "========================================="
+echo " PlateRecApp 安装脚本"
+echo " 平台: 树莓派5 8G + Ubuntu 24.04"
+echo "========================================="
+
+# 1. 系统更新
+echo "[1/6] 更新系统..."
+apt update && apt upgrade -y
+
+# 2. 安装依赖
+echo "[2/6] 安装编译依赖..."
+apt install -y \
+    build-essential cmake git wget curl pkg-config unzip \
+    libopencv-dev \
+    libavformat-dev libavcodec-dev libavutil-dev libswscale-dev \
+    libsqlite3-dev \
+    libcurl4-openssl-dev libssl-dev \
+    libfreetype-dev \
+    libcjson-dev \
+    libmosquitto-dev libmosquittopp-dev \
+    libprotobuf-dev protobuf-compiler \
+    libcrypt-dev
+
+# 3. 编译安装 MNN
+echo "[3/6] 编译安装 MNN..."
+if [ ! -d "/usr/local/include/MNN" ]; then
+    cd /root
+    git clone --depth 1 --branch 2.9.6 https://github.com/alibaba/MNN.git
+    cd MNN && mkdir build && cd build
+    cmake .. \
+        -DCMAKE_BUILD_TYPE=Release \
+        -DMNN_BUILD_SHARED_LIBS=ON \
+        -DMNN_BUILD_CONVERTER=OFF \
+        -DMNN_BUILD_DEMO=OFF \
+        -DMNN_BUILD_TOOLS=OFF \
+        -DMNN_BUILD_TRAIN=OFF \
+        -DMNN_ARM82=ON \
+        -DMNN_FP16=ON \
+        -DMNN_USE_THREAD_POOL=ON \
+        -DMNN_NUM_THREADS=4 \
+        -DCMAKE_INSTALL_PREFIX=/usr/local
+    make -j4
+    make install
+    ldconfig
+    echo "✅ MNN 安装完成"
+else
+    echo "✅ MNN 已存在,跳过"
+fi
+
+# 4. 安装 HyperLPR3 SDK
+echo "[4/6] 安装 HyperLPR3 SDK..."
+if [ ! -d "/root/PlateRecApp/hyperlpr3/include" ]; then
+    echo "⚠️ 请手动安装 HyperLPR3 SDK 到 /root/PlateRecApp/hyperlpr3/"
+    echo "   参考本文档第4节"
+else
+    echo "/root/PlateRecApp/hyperlpr3/lib" > /etc/ld.so.conf.d/hyperlpr3.conf
+    ldconfig
+    echo "✅ HyperLPR3 SDK 配置完成"
+fi
+
+# 5. 编译 PlateRecApp
+echo "[5/6] 编译 PlateRecApp..."
+cd /root/PlateRecApp
+mkdir -p build && cd build
+cmake .. -DCMAKE_BUILD_TYPE=Release
+make -j4
+echo "✅ 编译完成: $(ls -lh PlateRecApp)"
+
+# 6. 配置 systemd 服务
+echo "[6/6] 配置系统服务..."
+mkdir -p /root/PlateRecApp/{data,fonts,html,logs}
+
+# 性能优化
+echo "performance" > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null || true
+
+echo ""
+echo "========================================="
+echo " ✅ 安装完成!"
+echo ""
+echo " 后续步骤:"
+echo " 1. 编辑 /root/PlateRecApp/config.ini"
+echo " 2. 安装 HyperLPR3 SDK 模型文件"
+echo " 3. sudo systemctl daemon-reload"
+echo " 4. sudo systemctl enable --now plate-rec"
+echo "========================================="
+```
+
+---
+
+## 附录 B: 系统信息收集(用于问题反馈)
+
+遇到问题时,运行以下命令收集系统信息:
+
+```bash
+echo "=== 系统信息 ==="
+uname -a
+cat /etc/os-release | head -3
+echo "=== CPU ==="
+lscpu | grep -E "Model name|Architecture|CPU\(s\)|Thread"
+echo "=== 内存 ==="
+free -h
+echo "=== 温度 ==="
+cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1/1000 "°C"}'
+echo "=== 磁盘 ==="
+df -h / /root
+echo "=== 依赖版本 ==="
+pkg-config --modversion opencv4 2>/dev/null && echo "OpenCV: ✅" || echo "OpenCV: ❌"
+pkg-config --modversion libavformat 2>/dev/null && echo "FFmpeg: ✅" || echo "FFmpeg: ❌"
+pkg-config --modversion sqlite3 2>/dev/null && echo "SQLite3: ✅" || echo "SQLite3: ❌"
+echo "=== MNN ==="
+ls /usr/local/lib/libMNN.so 2>/dev/null && echo "MNN: ✅" || echo "MNN: ❌"
+echo "=== HyperLPR3 ==="
+ls /root/PlateRecApp/hyperlpr3/lib/libhyperlpr3.so 2>/dev/null && echo "HyperLPR3: ✅" || echo "HyperLPR3: ❌"
+echo "=== PlateRecApp ==="
+file /root/PlateRecApp/PlateRecApp 2>/dev/null || echo "未编译"
+echo "=== 动态库检查 ==="
+ldd /root/PlateRecApp/PlateRecApp 2>/dev/null | grep "not found" || echo "所有库已找到 ✅"
+```
+
+---
+
+> **📝 本文档持续更新**  
+> 最后更新: 2026-08-30 | 版本: fix27 v44  
+> 如有问题或建议,请联系项目维护者。

+ 1005 - 0
TECH_DOC.md

@@ -0,0 +1,1005 @@
+# PlateRecApp 技术方案文档
+
+> **版本**: v43.2 fix24  
+> **作者**: steven_roc  
+> **仓库**: https://git.zhonjin.com:40717/steven_roc/PlateRecApp  
+> **最后更新**: 2026-07-06  
+
+---
+
+## 1. 项目概述
+
+### 1.1 项目名称
+**PlateRecApp**(HyperLPR3-LinuxAPP)
+
+### 1.2 系统定位
+面向渣土车/工程车辆管理的**车牌识别+称重+联单上传**一体化边缘计算系统。部署在工地出入口的树莓派5上,通过4路RTSP摄像头实时捕获车牌,结合称重仪表数据,自动完成:
+
+- 车牌识别(HyperLPR3 + MNN推理引擎)
+- 进站/出站交替锁定管理
+- 照片水印叠加与平台上传
+- 飞书实时通知推送
+- MQTT消息发布
+- 耀华称重仪表数据采集与上报
+- Web管理后台(监控/视频/配置/锁定管理)
+- frpc内网穿透管理
+- SSH密钥管理
+- 系统资源监控
+
+### 1.3 目标场景
+工地/码头/回填点出入口,单点双向(进站+出站),支持4个摄像头(前低位×2 + 侧高位×2),支持共享摄像头模式。
+
+---
+
+## 2. 软件环境
+
+### 2.1 目标硬件
+| 参数 | 规格 |
+|------|------|
+| 主板 | 树莓派5 8GB |
+| CPU | BCM2712, Quad-core Arm Cortex-A76 |
+| GPU | VideoCore VII |
+| 内存 | 8GB LPDDR4X |
+
+### 2.2 操作系统
+| 参数 | 值 |
+|------|-----|
+| OS | Ubuntu 24.04 LTS |
+| 架构 | aarch64 (ARM64) |
+| 内核 | Linux 6.x |
+
+### 2.3 编译器与构建工具
+| 工具 | 版本/标准 |
+|------|-----------|
+| GCC | 11+ (aarch64-linux-gnu) |
+| C++ 标准 | C++17 (CMAKE_CXX_STANDARD 17) |
+| CMake | 3.25.1+ |
+| 构建类型 | Release (默认) / Debug / MinSizeRel / RelWithDebInfo |
+
+### 2.4 依赖库清单
+
+| 库名 | 用途 | 查找方式 |
+|------|------|----------|
+| OpenCV | 图像处理、JPEG编解码、水印渲染 | `find_package(OpenCV REQUIRED)` |
+| HyperLPR3 SDK | 车牌检测+识别(MNN推理) | 头文件 `hyper_lpr_sdk.h`,库 `libhyperlpr3` |
+| SQLite3 | 本地数据库(记录存储、认证、监控) | `pkg_check_modules(SQLITE3 REQUIRED sqlite3)` |
+| cURL | HTTP客户端(API上传、飞书消息) | `find_package(CURL REQUIRED)` |
+| OpenSSL | HTTPS/SSL、MD5计算 | `find_package(OpenSSL REQUIRED)` |
+| FreeType2 | 中文字体渲染(水印文字) | `pkg_check_modules(FREETYPE REQUIRED freetype2)` |
+| FFmpeg (libavformat/libavcodec/libavutil/libswscale) | RTSP流解码、DRM硬件加速 | `pkg_check_modules` |
+| cJSON | JSON解析与生成 | 手动查找 + pkg-config 回退 |
+| mosquitto / mosquittopp | MQTT客户端通信 | 直接链接 |
+| libcrypt | bcrypt密码哈希(`crypt_r`) | `find_library(CRYPT_LIBRARY crypt)` |
+| inih | INI配置文件解析 | 第三方头文件 `ini.h` |
+| httplib (cpp-httplib) | 轻量级HTTP/HTTPS服务器 | 第三方头文件 `httplib.h` |
+| cvxFont | OpenCV中文字体绘制 | 第三方库 `cvxFont.h` |
+
+---
+
+## 3. 系统架构
+
+### 3.1 模块划分图
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│                         main.cpp (主调度)                            │
+│   配置加载 → 数据库初始化 → HyperLPR初始化 → 摄像头启动 → 主循环       │
+└──────┬──────────┬──────────┬──────────┬──────────┬─────────────────┘
+       │          │          │          │          │
+  ┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼────┐ ┌───▼──────┐
+  │ config │ │database│ │web   │ │feishu  │ │ MQTT +   │
+  │  .cpp  │ │  .cpp  │ │server│ │client  │ │ Weight   │
+  └────────┘ └────────┘ └──────┘ └────────┘ │ Scale    │
+                                             └──────────┘
+  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
+  │  RTSP    │ │  Plate   │ │ Station  │ │   Network    │
+  │ Capture  │ │Recognize │ │  Lock    │ │   Client     │
+  │  (×4)    │ │   r.cpp  │ │  .cpp    │ │   .cpp       │
+  └──────────┘ └──────────┘ └──────────┘ └──────────────┘
+  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
+  │  Auth    │ │  frpc    │ │  SSH     │ │  System      │
+  │ System   │ │ Manager  │ │ Manager  │ │  Monitor     │
+  │(*.cpp)   │ │  .cpp    │ │  .cpp    │ │  .cpp        │
+  └──────────┘ └──────────┘ └──────────┘ └──────────────┘
+  ┌──────────┐ ┌──────────────────────────────────────────┐
+  │   Log    │ │     common.h (全局类型/结构体/指标)       │
+  │ Manager  │ │     MonitorMetrics / ROIConfig / ...     │
+  └──────────┘ └──────────────────────────────────────────┘
+```
+
+### 3.2 模块间依赖关系
+
+```
+common.h ← 所有模块(全局类型定义)
+config.cpp ← main.cpp(配置加载)
+rtsp_capture.cpp ← plate_recognizer.cpp(帧采集)
+plate_recognizer.cpp ← main.cpp(识别主逻辑)
+station_lock.cpp ← plate_recognizer.cpp(交替锁定)
+database.cpp ← plate_recognizer.cpp, web_server.cpp(数据持久化)
+network_client.cpp ← plate_recognizer.cpp, weight_scale.cpp(HTTP API)
+feishu_client.cpp ← plate_recognizer.cpp(飞书通知)
+mqtt_client.cpp ← weight_scale.cpp(MQTT发布)
+weight_scale.cpp ← main.cpp(称重数据采集)
+web_server.cpp ← main.cpp(Web服务线程)
+auth_*.cpp ← web_server.cpp(认证中间件)
+frpc_manager.cpp ← web_server.cpp(frpc管理API)
+ssh_manager.cpp ← web_server.cpp(SSH密钥管理)
+log_manager.cpp ← main.cpp(日志缓冲)
+system_monitor.cpp ← web_server.cpp(系统监控)
+```
+
+### 3.3 数据流图
+
+```
+RTSP摄像头(×4)
+    │
+    ▼ FFmpeg解码(软解/DRM硬解)
+帧队列(deque<cv::Mat>, 最大10帧)
+    │
+    ▼ getFrame() 取出最新帧
+ROI裁剪(可选, 按配置区域)
+    │
+    ▼ HyperLPR3推理(HLPR_UpdateImage)
+车牌检测结果(车牌号+置信度+类型)
+    │
+    ▼ 帧级去重(2秒内同车牌跳过)
+    │
+    ├─── 置信度 ≥ LOG_THRESHOLD? ──→ 保存照片(加水印) → 插入SQLite
+    │
+    ▼ 置信度 ≥ CONFIDENCE_THRESHOLD?
+    │
+    ├─── 交替锁定检查(try_lock_in/out_station)
+    │       │ BLOCKED → 跳过
+    │       ▼ ALLOWED
+    │
+    ├─── 创建联单(lib_curl_create_bill_request) → 获取tb_num
+    │
+    ├─── 上传照片(低位+高位×2组) → 平台API
+    │
+    ├─── 飞书通知(send_feishu_msg) → 飞书开放API
+    │
+    └─── 称重+MQTT(weight_scale → mqtt_publish) → MQTT Broker
+```
+
+---
+
+## 4. 核心模块技术方案
+
+### 4.1 车牌识别引擎(HyperLPR3 + MNN)
+
+#### 4.1.1 初始化流程
+```cpp
+// hyperlpr_lib_init() — plate_recognizer.cpp
+HLPR_ContextConfiguration configuration = { 0 };
+configuration.models_path = (char*)model.c_str();  // hyperlpr3/resource/models/r2_mobile
+configuration.max_num = 2;                    // 每帧最多检测2个车牌
+configuration.det_level = DETECT_LEVEL_HIGH;  // 高精度检测模式
+configuration.use_half = false;               // 不使用FP16半精度
+configuration.nms_threshold = 0.5f;           // NMS非极大值抑制阈值
+configuration.rec_confidence_threshold = 0.6f; // 识别置信度阈值
+configuration.box_conf_threshold = 0.30f;     // 检测框置信度阈值
+configuration.threads = 1;                    // 推理线程数
+plate_rec_app.hlpr_ctx = HLPR_CreateContext(&configuration);
+```
+
+#### 4.1.2 推理流程
+1. `capture_do_work_thread()` 主循环每500ms执行一次
+2. 从4路RTSPCapture获取最新帧
+3. ROI裁剪(如启用)→ `HLPR_CreateDataBuffer` → `HLPR_UpdateImage`
+4. 遍历检测结果,按置信度更新 `CarPlateInfo`
+5. 置信度 ≥ `PLATE_LOG_THRESHOLD` → 保存照片到磁盘
+6. 置信度 ≥ `PLATE_CONFIDENCE_THRESHOLD` → 触发上传流程
+
+#### 4.1.3 ROI裁剪策略
+```cpp
+struct ROIConfig {
+    int x, y, width, height;  // 裁剪区域
+    bool enabled;              // 是否启用
+    cv::Rect getRect(int img_w, int img_h);  // 安全边界裁剪
+};
+// 进站ROI: g_roi_in, 出站ROI: g_roi_out
+// 支持通过Web界面手动绘制调整,写回config.ini
+```
+
+#### 4.1.4 车牌类型映射
+```cpp
+static const std::vector<std::string> TYPES = {
+    "蓝牌", "黄牌", "白牌单层", "绿牌", "黑牌港澳", "香港单层",
+    "香港双层", "澳门单层", "澳门双层", "黄牌双层"
+};
+```
+
+#### 4.1.5 关键结构体
+```cpp
+struct CarPlateInfo {
+    std::string code;          // 车牌号
+    float confidence;          // 识别置信度
+    std::string type;          // 车牌类型(蓝牌/黄牌等)
+    cv::Mat image_front;       // 前向照片
+    cv::Mat image_side;        // 侧向照片
+    int db_id;                 // 数据库记录ID
+    bool is_inbound/outbound;  // 进站/出站标记
+    time_t db_capture_time;    // 照片保存时固定的capture_time
+    std::atomic<bool> upload_in_progress{false};  // P0: 上传竞态保护
+};
+```
+
+### 4.2 RTSP视频采集
+
+#### 4.2.1 类设计
+```cpp
+class RTSPCapture {
+    std::string rtspUrl_;
+    DecodeMode decodeMode_;      // AUTO / SOFT / DRM
+    std::string drmDevice_;      // 默认 /dev/dri/renderD128
+    std::string transport_;      // tcp / udp
+    AVFormatContext* fmtCtx_;
+    AVCodecContext* codecCtx_;
+    AVBufferRef* hwDeviceCtx_;   // DRM硬件设备上下文
+    SwsContext* swsCtx_;         // 像素格式转换上下文
+    std::deque<cv::Mat> frameQueue_;  // 帧队列(最大10帧)
+    std::thread captureThread_;
+};
+```
+
+#### 4.2.2 FFmpeg解码流程
+1. `avformat_open_input()` — 打开RTSP流
+2. `avformat_find_stream_info()` — 获取流信息
+3. `av_find_best_stream()` — 查找视频流
+4. 根据codec判断:HEVC → 尝试DRM硬解,否则软解
+5. `avcodec_open2()` — 打开解码器(thread_count=4)
+6. `captureLoop()` 循环读取帧 → `decodeFrameToBGR()` → 入队
+
+#### 4.2.3 DRM硬件加速配置
+```cpp
+bool initDRMHwAccel() {
+    AVHWDeviceType type = av_hwdevice_find_type_by_name("drm");
+    av_hwdevice_ctx_create(&hwDeviceCtx_, type, "/dev/dri/renderD128", nullptr, 0);
+    codecCtx_->hw_device_ctx = av_buffer_ref(hwDeviceCtx_);
+}
+// 解码后检测 AV_PIX_FMT_DRM_PRIME 格式 → av_hwframe_transfer_data 转CPU
+```
+
+#### 4.2.4 帧队列管理
+- 队列容量:最大10帧,超出时丢弃最旧帧
+- `getFrame()` — 取出并移除最新帧(供识别线程使用)
+- `peekFrame()` — 只读取不移除(供MJPEG视频流使用,避免与识别线程竞争)
+- 解码线程与消费线程通过 `std::mutex + condition_variable` 同步
+
+#### 4.2.5 重连策略
+- 读取失败(`av_read_frame` 返回错误)→ `cleanupFFmpeg()` → 等待3秒 → 重新 `initFFmpeg()`
+- 连接超时:5秒(`timeout=5000000`微秒)
+- RTSP传输:默认TCP(可配置UDP)
+- 低延迟优化:`fflags=nobuffer+genpts`, `flags=low_delay`
+
+#### 4.2.6 FFmpeg选项
+```cpp
+av_dict_set(&opts, "rtsp_transport", "tcp", 0);
+av_dict_set(&opts, "timeout", "5000000", 0);      // 5秒超时
+av_dict_set(&opts, "fflags", "nobuffer+genpts", 0);
+av_dict_set(&opts, "flags", "low_delay", 0);
+av_dict_set(&opts, "avoid_negative_ts", "make_zero", 0);
+av_dict_set(&opts, "err_detect", "ignore_err", 0);
+```
+
+### 4.3 交替锁定机制
+
+#### 4.3.1 核心规则(fix17版)
+| 状态转换 | 等待时间 | 超时行为 |
+|----------|----------|----------|
+| 进站完成 → 允许出站 | `in_out_interval` (默认300秒) | 等待≤2小时,超时清零 |
+| 出站完成 → 允许进站 | `in_out_interval + TIME_WINDOW` (默认600秒) | 等待≤2小时,超时清零 |
+| 出站无进站记录 | 永远拦截 | 必须先进站才能出站 |
+| 同方向连续操作 | 永远拦截 | 必须先完成对端操作 |
+
+#### 4.3.2 状态机
+```
+INBOUND_ALLOWED  → 上次出站完成,等待 in_out_interval+TIME_WINDOW 后允许进站
+OUTBOUND_ALLOWED → 上次进站完成,等待 in_out_interval 后允许出站
+无记录出站       → 永远拦截(必须先进站)
+锁定超2小时     → 自动清零释放(视为车辆已离开)
+```
+
+#### 4.3.3 关键函数
+```cpp
+bool try_lock_in_station(const std::string& plate);   // 原子锁定进站
+bool try_lock_out_station(const std::string&plate);   // 原子锁定出站
+void lock_in_station(const std::string& plate);        // 确认进站锁定
+void lock_out_station(const std::string& plate);       // 确认出站锁定
+void unlock_rollback_station(const std::string& plate, bool is_in);  // 失败回滚
+void cleanup_station_cache();                          // 2小时超时清理
+void load_station_cache_from_db();                     // 重启恢复
+```
+
+#### 4.3.4 持久化
+- 数据库表:`station_lock_cache`
+- 字段:`plate_number`, `last_in_time`, `last_out_time`, `current_mode`
+- 重启后自动从数据库恢复锁定状态,超时记录直接删除
+
+### 4.4 称重调度器
+
+#### 4.4.1 耀华仪表通信协议
+```
+帧格式: 0x02 + sign(1字节) + digits(8字节) + status(1字节) + 0x03
+帧长度: 12字节
+数据解析: weight_kg = digits_value / 100.0  (单位: 分→公斤)
+有效状态: 排除 status='G'/'H' 的帧
+重量范围: g_weight_min_kg ~ g_weight_max_kg
+```
+
+#### 4.4.2 TCP连接管理
+- 连接超时:`tcp_connect_timeout`(可配置)
+- 读取间隔:200ms
+- 断线重连:5秒延迟自动重连
+- 接收缓冲区:1024字节
+
+#### 4.4.3 稳定读数检测
+```cpp
+// 稳定性判定:连续 g_weight_stable_samples 个样本波动 ≤ g_weight_stable_threshold
+g_stability_samples.push_back(weight_kg);
+if (size > g_weight_stable_samples) g_stability_samples.erase(begin);
+// 候选数据:收集 g_weight_candidate_count 个有效读数
+```
+
+#### 4.4.4 称重+MQTT异步任务队列
+```cpp
+struct WeightMqttTask {
+    std::string plate;
+    int station_type;   // 1=进站, 2=出站
+    std::string tb_num;
+};
+// 去重机制:30秒内同车牌只发送一次MQTT消息
+static std::unordered_map<std::string, time_t> g_mqtt_dedup_map;
+static const int MQTT_DEDUP_INTERVAL_SEC = 30;
+```
+
+### 4.5 Web管理后台
+
+#### 4.5.1 框架与配置
+- **HTTP框架**: cpp-httplib(header-only)
+- **SSL支持**: OpenSSL,通过 `httplib::SSLServer` 创建HTTPS服务器
+- **默认端口**: 8080(从config.ini `[server] ip` 字段解析)
+- **HTTP重定向**: 可选HTTP→HTTPS自动跳转(fix24-v19)
+
+#### 4.5.2 SSL性能优化
+```cpp
+// SSL会话缓存:10分钟有效期
+SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER);
+SSL_CTX_set_timeout(ssl_ctx, 600);
+// 优先使用ECDHE+AESGCM/ChaCha20密码套件
+SSL_CTX_set_cipher_list(ssl_ctx,
+    "ECDHE-ECDSA-AES128-GCM-SHA256:"
+    "ECDHE-RSA-AES128-GCM-SHA256:"
+    "ECDHE-ECDSA-CHACHA20-POLY1305:...");
+SSL_CTX_set_ecdh_auto(ssl_ctx, 1);  // 优先X25519
+```
+
+#### 4.5.3 路由表
+
+| 路径 | 方法 | 功能 | 认证要求 |
+|------|------|------|----------|
+| `/` | GET | 监控首页 | 公开/基础 |
+| `/video` | GET | 4路视频预览 | 公开/基础 |
+| `/login` | GET | 登录页面 | 公开 |
+| `/config` | GET | 系统配置 | 管理员 |
+| `/locks` | GET | 锁定管理 | 公开/基础 |
+| `/system` | GET | 系统监控 | 公开/基础 |
+| `/api/monitor` | GET | 监控指标JSON | 公开 |
+| `/api/messages` | GET | 记录列表JSON | 公开 |
+| `/api/system/status` | GET | 系统资源状态 | 公开 |
+| `/api/frpc/status` | GET | frpc隧道状态 | 管理员 |
+| `/api/frpc/config` | GET/POST | frpc配置读写 | 超级管理员 |
+| `/api/auth/login` | POST | 登录认证 | 公开 |
+| `/api/auth/logout` | POST | 登出 | 已登录 |
+| `/api/auth/status` | GET | 会话状态 | 公开 |
+| `/api/station_locks` | GET | 锁定记录列表 | 公开 |
+| `/api/clear_lock` | POST | 清除锁定 | 管理员 |
+| `/api/video/{stream}` | GET | MJPEG视频流 | 公开 |
+| `/api/photo/{path}` | GET | 照片访问 | 公开 |
+
+#### 4.5.4 认证系统
+
+**架构**: bcrypt密码哈希 + 内存+SQLite双层Session + CSRF Token + IP速率限制
+
+```cpp
+// auth_db.h — 数据库层
+struct User {
+    int id; std::string username; std::string password_hash;
+    int role;       // 0=user, 1=admin, 2=superadmin
+    int enabled;
+    int failed_attempts; time_t locked_until;
+};
+struct Session {
+    std::string session_id;  // 64字符hex
+    std::string csrf_token;  // 64字符hex
+    int user_id; std::string username;
+    time_t created_at, last_active, expires_at;
+    int remember;   // 1=延长会话(7天)
+    int user_role;
+};
+
+// 数据库表: users, sessions, audit_log
+// WAL模式 + busy_timeout=5000ms
+```
+
+**Session管理**:
+- 空闲超时: 30分钟 (1800秒)
+- 绝对超时: 8小时 (28800秒)
+- "记住我": 7天 (604800秒)
+- 内存缓存 + SQLite持久化双层架构
+
+**速率限制**:
+- 滑动窗口: 5分钟 (300秒)
+- 最大尝试: 5次/IP
+- 超限返回 HTTP 429
+
+**CSRF保护**:
+- 每个Session生成64字符hex CSRF Token
+- 状态变更请求(POST/PUT/DELETE)必须携带 `X-CSRF-Token` 头
+
+**安全响应头**:
+- `X-Content-Type-Options: nosniff`
+- `X-Frame-Options: DENY`
+- `X-XSS-Protection: 1; mode=block`
+- `Content-Security-Policy: default-src 'self'`
+
+### 4.6 飞书/MQTT推送
+
+#### 4.6.1 飞书消息
+
+**API**: `POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id`
+
+**消息格式**:
+```json
+{
+    "receive_id": "{chat_id}",
+    "msg_type": "text",
+    "content": "{\"text\":\"工程名: xxx\\n车牌号: xxx\\n时间: xxx\\n...\"}"
+}
+```
+
+**重试策略**:
+- 最大重试: 4次(初始请求 + 3次重试)
+- 退让策略: 指数退让 2^n 秒 (2s, 4s, 8s)
+- 网络超时/连接失败 → 重试;业务错误 → 直接终止
+
+**Token管理**:
+- `g_tenant_token`: 飞书租户访问令牌
+- 过期时间: `g_token_expire_time`
+- 刷新缓冲: 300秒 (`TOKEN_REFRESH_BUFFER_SECONDS`)
+
+#### 4.6.2 MQTT客户端
+
+**架构**: 基于 `mosquittopp` 类,继承回调机制
+
+```cpp
+class MqttClient : public mosqpp::mosquittopp {
+    std::atomic<bool> connected{false};
+    std::atomic<MqttState> current_state{DISCONNECTED};
+    static const int MAX_RECONNECT_ATTEMPTS = 30;
+};
+```
+
+**连接管理**:
+- 异步连接: `connect_async(host, port, keepalive=60)`
+- 状态机: DISCONNECTED → CONNECTING → CONNECTED → STABLE (5秒后)
+- 最大重连: 30次
+- ClientID格式: `{client_id}_{pid}_{timestamp}`
+
+**消息发布**:
+- QoS: 1 (至少一次)
+- 发布后等待ACK确认 (`acked` 原子变量)
+- 连接断开后自动重连
+
+**消息内容**: 车牌号 + 车牌颜色 + 车型 + 联单编号 + 进出站类型
+
+### 4.7 数据库操作
+
+#### 4.7.1 SQLite表结构
+
+**主表: `upload_records`**
+```sql
+CREATE TABLE upload_records (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    create_time DATETIME DEFAULT (datetime('now', 'localtime')),
+    capture_time INTEGER,           -- fix24-v13: 抓拍时刻Unix时间戳
+    plate_number TEXT NOT NULL,
+    tb_num TEXT NOT NULL,
+    station_type INTEGER,           -- 1=进站, 2=出站
+    lo_photo_path TEXT,
+    hi_photo_path TEXT,
+    lo_upload_status INTEGER DEFAULT 0,
+    hi_upload_status INTEGER DEFAULT 0,
+    feishu_status INTEGER DEFAULT 0,
+    retry_count INTEGER DEFAULT 0
+);
+-- 索引
+CREATE INDEX idx_plate_number ON upload_records(plate_number);
+CREATE INDEX idx_tb_num ON upload_records(tb_num);
+CREATE INDEX idx_create_time ON upload_records(create_time);
+CREATE INDEX idx_plate_station_time_status ON upload_records(
+    plate_number, station_type, create_time, lo_upload_status, hi_upload_status);
+```
+
+**交替锁定表: `station_lock_cache`**
+```sql
+CREATE TABLE station_lock_cache (
+    plate_number TEXT PRIMARY KEY,
+    last_in_time INTEGER,
+    last_out_time INTEGER,
+    current_mode INTEGER    -- 0=INBOUND_ALLOWED, 1=OUTBOUND_ALLOWED
+);
+```
+
+**TIME_WINDOW缓存表: `time_window_cache`**
+```sql
+-- 存储进站/出站工单配额缓存
+CREATE TABLE time_window_cache (...);
+```
+
+**认证数据库表** (独立SQLite文件):
+```sql
+CREATE TABLE users (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    username TEXT UNIQUE NOT NULL,
+    password_hash TEXT NOT NULL,     -- bcrypt hash
+    role INTEGER DEFAULT 0,
+    enabled INTEGER DEFAULT 1,
+    failed_attempts INTEGER DEFAULT 0,
+    locked_until INTEGER DEFAULT 0,
+    created_at INTEGER NOT NULL,
+    updated_at INTEGER NOT NULL
+);
+CREATE TABLE sessions (...);
+CREATE TABLE audit_log (...);
+```
+
+**系统监控数据库** (独立SQLite文件):
+- 路径: `data/system_metrics.db`
+- 用途: CPU/内存/温度/磁盘历史数据
+
+#### 4.7.2 数据库配置
+```cpp
+sqlite3_busy_timeout(g_db, 100);       // busy超时100ms
+SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
+// WAL模式(认证数据库)
+// 认证数据库: busy_timeout=5000ms
+```
+
+#### 4.7.3 缓存策略
+- TIME_WINDOW缓存: 内存 + SQLite 双层
+- 交替锁定缓存: 内存(`g_plate_station_cache`) + SQLite 双层
+- 联单缓存: `g_in_bill_cache` / `g_out_bill_cache` (内存)
+- 数据库自动清理: 每日23:10执行 `db_cleanup_old_records` + `db_vacuum`
+- 数据库健康检查: 每60秒 `SELECT 1` 探活,失败自动重连
+- DB容量阈值: 500MB触发清理 (`DB_CLEANUP_THRESHOLD_MB`)
+
+### 4.8 日志管理
+
+#### 4.8.1 内存日志缓冲架构(fix24-v36)
+```
+stdout/stderr → pipe() → 读取线程 → 内存buffer(vector<string>)
+                                         │
+                                         ├── Web API读取(零磁盘I/O)
+                                         └── 定时刷盘(默认23:20) → 磁盘文件
+```
+
+#### 4.8.2 关键配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `enabled` | true | 1=内存缓冲模式, 0=直写文件 |
+| `buffer_size_mb` | 20MB | 内存缓冲区上限 |
+| `flush_time` | 23:20 | 每日刷盘时间 |
+| `retention_days` | 30天 | 日志保留天数 |
+| `redirect_stdout` | true | 是否重定向stdout/stderr |
+| `auto_flush_on_exit` | true | 程序退出时自动刷盘 |
+
+#### 4.8.3 刷盘策略
+- 定时刷盘: 到达 `flush_time` 时将内存buffer全量写入磁盘
+- 退出刷盘: 程序优雅退出时执行最后一次刷盘
+- 容量控制: 超过 `buffer_size_mb` 时丢弃最旧日志
+- 文件清理: 凌晨2:00按 `retention_days` 清理过期日志文件
+
+### 4.9 frpc内网穿透管理
+
+#### 4.9.1 状态获取
+通过 frpc admin HTTP API 获取隧道状态:
+```cpp
+std::string call_admin_api(const std::string &endpoint) {
+    // URL: http://{admin_addr}:{admin_port}/api/serverinfo
+    // Basic Auth: g_frpc_admin_user : g_frpc_admin_password
+    // 超时: connect=1s, total=2s
+}
+```
+
+#### 4.9.2 进程检测(三重验证)
+1. `systemctl is-active frpc` — systemd状态
+2. `pgrep -x frpc` — 进程存在性
+3. frpc admin API可达性
+
+#### 4.9.3 配置热更新
+- 读取: 直接读取 `frpc.toml` 文件内容
+- 保存: 自动备份旧文件到 `backup_path` → 写入新配置
+- 重启: `systemctl restart frpc` → 等待 `restart_check_timeout` 秒
+- 自动回滚: 重启后检测进程存活,失败则恢复备份配置
+
+#### 4.9.4 安全限制
+- **admin API仅允许本机127.0.0.1调用**
+- 禁止通过frp隧道暴露到公网
+- 受保护隧道列表: `g_frpc_protected_tunnels`(不可删除)
+
+### 4.10 SSH密钥管理
+
+#### 4.10.1 功能
+- 读取/添加/删除 `authorized_keys` 文件中的SSH公钥
+- 支持密钥类型验证: ssh-rsa, ssh-dss, ssh-ed25519, ecdsa-sha2-*
+
+#### 4.10.2 密钥格式验证
+```cpp
+bool validate_key_format(const std::string &key_line) {
+    // 格式: type base64_data [comment]
+    // type必须是已知SSH密钥类型
+    // base64数据至少20字符
+}
+```
+
+#### 4.10.3 配置
+- 密钥文件路径: `g_ssh_keys_path` (默认 `~/.ssh/authorized_keys`)
+- 通过Web管理界面操作
+
+---
+
+## 5. 配置文件详解
+
+### 5.1 config.ini 完整配置段
+
+#### [config] — 核心业务配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `project_name` | (必填) | 工程名 |
+| `point_number` | (必填) | 点位编号 |
+| `throughway` | (必填) | 出入口编号 |
+| `rtsp_url_front_in` | (必填) | 进站前向摄像头RTSP URL |
+| `rtsp_url_front_out` | (必填) | 出站前向摄像头RTSP URL |
+| `rtsp_url_side_in` | (必填) | 进站侧向摄像头RTSP URL |
+| `rtsp_url_side_out` | (必填) | 出站侧向摄像头RTSP URL |
+| `app_api` | (必填) | 平台API地址 |
+| `app_key` | (必填) | 平台API Key |
+| `app_secret` | (必填) | 平台API Secret |
+| `DEBUG_LOG` | 0 | 调试日志(1=开启, 0=正式) |
+| `TestFlag` | 0 | 测试环境标记 |
+| `wType` | 1 | 工地类型(1=工地, 2=码头, 3=回填) |
+| `TIME_WINDOW` | 10 | 时间窗口(分钟),控制进站/出站配额 |
+| `MAX_PHOTO_GROUPS` | 2 | 每侧最大照片组数 |
+| `in_out_interval` | 5 | 进出站间隔(分钟),转换为秒: ×60 |
+| `PLATE_CONFIDENCE_THRESHOLD` | 0.8 | 上传触发置信度阈值 |
+| `PLATE_LOG_THRESHOLD` | 0.5 | 日志记录置信度阈值 |
+| `AlternatingMerge` | 0 | 交替锁定开关(1=启用) |
+| `roi_in_x/y/w/h` | 0 | 进站ROI区域坐标 |
+| `roi_in_enabled` | 0 | 进站ROI启用(1=启用) |
+| `roi_out_x/y/w/h` | 0 | 出站ROI区域坐标 |
+| `roi_out_enabled` | 0 | 出站ROI启用 |
+| `roi_debug_enabled` | 0 | ROI调试模式 |
+| `hw_decode_mode` | auto | 解码模式(auto/soft/drm) |
+| `hw_decode_device` | /dev/dri/renderD128 | DRM设备路径 |
+| `rtsp_transport` | tcp | RTSP传输协议(tcp/udp) |
+| `PhotoMaxCapacityMB` | 1024 | 照片存储容量阈值(MB) |
+| `LogRetentionDays` | 30 | 日志保留天数 |
+
+#### [server] — Web服务配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `ip` | http://0.0.0.0:8080 | 服务地址(端口从此字段解析) |
+
+#### [ssl] — HTTPS/SSL配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `enabled` | 0 | SSL启用(1=启用) |
+| `cert_path` | (空) | SSL证书文件路径 |
+| `key_path` | (空) | SSL私钥文件路径 |
+
+#### [auth] — 认证系统配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `enabled` | 0 | 认证启用(1=启用) |
+| `db_path` | data/auth.db | 认证数据库路径(相对pathComm) |
+
+#### [feishu] — 飞书通知配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `APP_ID` | (必填) | 飞书应用ID |
+| `APP_SECRET` | (必填) | 飞书应用密钥 |
+| `CHAT_ID` | (必填) | 飞书群聊ID |
+| `TIMEOUT_SECOND` | 10 | HTTP超时(秒) |
+| `WarningSigns` | 0 | 抑制紧急告警(1=抑制) |
+
+#### [weight] — 称重系统配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `flagWeight` | 0 | 称重启用(1=启用) |
+| `weight_threshold_in` | (必填) | 进站重量阈值(公斤) |
+| `weight_threshold_out` | (必填) | 出站重量阈值(公斤) |
+| `default_entry_weight` | 0 | 默认进站重量 |
+| `default_exit_weight` | 0 | 默认出站重量 |
+| `weight_detection_time` | 10 | 称重检测时间(秒) |
+| `tcp_connect_timeout` | 5 | TCP连接超时(秒) |
+| `weight_server_ip` | (必填) | 仪表IP地址 |
+| `weight_server_port` | (必填) | 仪表端口 |
+| `MAX_WEIGHT` | 100000 | 最大有效重量(公斤) |
+| `STABLE_SAMPLE_COUNT` | 5 | 稳定判定样本数 |
+| `STABLE_THRESHOLD_KG` | 50 | 稳定波动阈值(公斤) |
+| `CANDIDATE_DATA_COUNT` | 10 | 候选数据数量 |
+| `MAX_UPLOAD_RETRIES` | 3 | 最大重试次数 |
+| `RETRY_DELAY_MS` | 1000 | 重试延迟(毫秒) |
+
+#### [MQTT] — MQTT配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `MQTT_HOST` | (空) | MQTT Broker地址 |
+| `MQTT_PORT` | 1883 | MQTT端口 |
+| `MQTT_USER` | (空) | 用户名 |
+| `MQTT_PASS` | (空) | 密码 |
+| `MQTT_TOPIC` | (必填) | 发布主题 |
+| `MQTT_CLIENT_ID` | PlateRecApp | 客户端ID |
+| `PLATE_COLOR` | 黄色 | 默认车牌颜色 |
+| `VEHICLE_TYPE` | 渣土车 | 默认车型 |
+
+#### [system] — 系统监控配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `monitor_interval` | 30 | 采样间隔(秒) |
+| `temp_alert_threshold` | 80 | 温度告警阈值(°C) |
+| `cpu_alert_threshold` | 90 | CPU告警阈值(%) |
+| `db_path` | data/system_metrics.db | 监控数据库路径 |
+| `history_retention_days` | 30 | 全量数据保留天数 |
+| `aggregation_retention_days` | 365 | 聚合数据保留天数 |
+| `flush_time` | 23:20 | 每日刷盘时间 |
+
+#### [frpc] — frpc管理配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `config_path` | /etc/frp/frpc.toml | frpc配置文件路径 |
+| `backup_path` | (自动) | 备份文件路径 |
+| `protected_tunnels` | (空) | 受保护隧道列表(逗号分隔) |
+| `restart_check_timeout` | 10 | 重启后等待检测时间(秒) |
+| `auto_rollback` | 1 | 失败自动回滚(1=启用) |
+| `web_enabled` | 1 | Web管理页面开关 |
+| `admin_addr` | 127.0.0.1 | frpc admin API地址 |
+| `admin_port` | 7400 | frpc admin API端口 |
+| `admin_user` | (空) | admin用户名 |
+| `admin_password` | (空) | admin密码 |
+
+#### [terminal] — 终端/SSH配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `ttyd_port` | 7681 | ttyd Web终端端口 |
+| `ttyd_credential` | (空) | ttyd认证凭证(user:pass) |
+| `ttyd_max_clients` | 5 | 最大客户端数 |
+| `ssh_keys_path` | ~/.ssh/authorized_keys | SSH公钥文件路径 |
+
+#### [log] — 日志管理配置
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `enabled` | 1 | 内存日志模式(1=内存, 0=直写) |
+| `log_file` | PlateRecApp.log | 日志文件名 |
+| `buffer_size_mb` | 20 | 缓冲区大小(MB) |
+| `flush_time` | 23:20 | 刷盘时间(HH:MM) |
+| `retention_days` | 30 | 保留天数 |
+| `redirect_stdout` | 1 | 重定向stdout/stderr |
+| `auto_flush_on_exit` | 1 | 退出时自动刷盘 |
+
+---
+
+## 6. 构建系统
+
+### 6.1 CMakeLists.txt 详细解析
+
+```cmake
+cmake_minimum_required(VERSION 3.25.1)
+project(HyperLPR3-LinuxAPP VERSION 1.0.0 LANGUAGES CXX C)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+```
+
+**构建类型默认**: Release
+
+### 6.2 编译优化标志
+
+| 标志 | 作用 |
+|------|------|
+| `-O3` | 最高级别速度优化 |
+| `-s` | 去除符号表(strip) |
+| `--as-needed` | 只链接实际使用的符号 |
+| `--gc-sections` | 删除未使用的段 |
+| POST_BUILD strip | 编译后strip进一步减小体积 |
+
+**可选优化**(注释状态):
+- `-Os`: 体积优先优化
+- `-flto`: 链接时优化(减小10-30%体积)
+- `-fvisibility=hidden`: 隐藏符号
+
+### 6.3 依赖库查找策略
+1. **pkg-config优先**: FreeType2, SQLite3, FFmpeg各模块
+2. **CMake find_package**: OpenCV, CURL, OpenSSL
+3. **手动查找回退**: cJSON(先pkg-config,再find_library,最后硬编码路径)
+4. **动态链接优先**: `BUILD_SHARED_LIBS ON`,可通过 `-DUSE_STATIC_LIBS=ON` 切换静态
+
+### 6.4 源码文件列表
+
+```
+src/
+├── main.cpp              # 主入口 + 调度
+├── common.cpp/h          # 全局类型定义
+├── config.cpp/h          # 配置解析
+├── plate_recognizer.cpp/h # 车牌识别引擎
+├── rtsp_capture.cpp/h    # RTSP视频采集
+├── web_server.cpp/h      # Web管理后台
+├── database.cpp/h        # SQLite数据库
+├── network_client.cpp/h  # HTTP API通信
+├── feishu_client.cpp/h   # 飞书推送
+├── mqtt_client.cpp/h     # MQTT客户端
+├── weight_scale.cpp/h    # 称重系统
+├── station_lock.cpp/h    # 交替锁定
+├── log_manager.cpp/h     # 日志管理
+├── frpc_manager.cpp/h    # frpc管理
+├── ssh_manager.cpp/h     # SSH管理
+├── system_monitor.cpp/h  # 系统监控
+├── system_metrics_manager.cpp/h # 监控历史数据
+├── monitor.cpp/h         # 指标打印
+├── utils.cpp/h           # 工具函数
+├── auth_api.cpp/h        # 认证API
+├── auth_crypto.cpp/h     # bcrypt + 安全随机
+├── auth_db.cpp/h         # 认证数据库
+├── auth_session.cpp/h    # 会话管理
+├── auth_middleware.cpp/h  # 认证中间件
+├── auth_rate_limiter.h   # 速率限制(header-only)
+└── bcrypt/               # bcrypt实现
+```
+
+---
+
+## 7. 部署方案
+
+### 7.1 目录结构
+```
+/opt/openAI/project/003.PlateRecAPP/   # pathComm (可执行文件所在目录)
+├── PlateRecApp                        # 可执行文件
+├── config.ini                         # 主配置文件
+├── config.txt                         # MD5校验配置
+├── simsun.ttc                         # 中文字体文件
+├── hyperlpr3/
+│   └── resource/models/r2_mobile/     # HyperLPR3模型文件
+├── assets/web/                        # Web前端HTML页面
+│   ├── login.html
+│   ├── system.html
+│   └── ...
+├── data/                              # 数据库目录
+│   ├── upload_records.db              # 主数据库
+│   ├── auth.db                        # 认证数据库
+│   └── system_metrics.db              # 监控数据库
+├── PlateJPG/                          # 照片存储目录
+├── third_party/                       # 第三方依赖
+└── sync_to_gogs.sh                    # Gogs同步脚本
+```
+
+### 7.2 systemd service 配置
+```ini
+[Unit]
+Description=PlateRecApp License Plate Recognition
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=/opt/openAI/project/003.PlateRecAPP
+ExecStart=/opt/openAI/project/003.PlateRecAPP/PlateRecApp config.ini
+Restart=always
+RestartSec=5
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### 7.3 文件权限
+- 可执行文件: `755`
+- 配置文件: `644`(含敏感信息,不可公开)
+- 数据库目录: `755`
+- 照片目录: `755`
+
+### 7.4 启动流程
+```
+1. 解析可执行文件路径 → 确定 pathComm
+2. 读取CPU序列号 → MD5三重校验 → 验证config.txt
+3. 解析config.ini → 加载所有配置
+4. 计算时区偏移 → 初始化cURL
+5. 初始化SQLite数据库 → 加载缓存
+6. 初始化HyperLPR3模型
+7. 创建4路RTSPCapture → 启动采集线程
+8. 启动识别工作线程
+9. 初始化认证系统 → 启动Web服务器
+10. 启动称重系统 → 启动MQTT客户端
+11. 进入主循环(100ms间隔)
+```
+
+---
+
+## 8. 安全策略
+
+### 8.1 认证机制
+- **bcrypt密码哈希**: 使用系统libcrypt的`crypt_r`,成本因子自动调整
+- **Session管理**: 64字符hex随机session_id,内存+SQLite双层
+- **CSRF防护**: 每个Session独立CSRF Token,状态变更请求必须验证
+- **IP速率限制**: 滑动窗口5分钟/5次,超限返回429
+- **角色权限**: user(0) / admin(1) / superadmin(2),不同页面/API按角色控制
+- **账户锁定**: 连续失败登录自动锁定(`failed_attempts` + `locked_until`)
+- **审计日志**: 所有登录/登出/失败/超时事件记录到 `audit_log` 表
+
+### 8.2 密码存储
+- 算法: bcrypt(通过 `crypt_r` 系统调用)
+- 自动salt生成,每次哈希结果不同
+- 验证使用 `secure_compare` 时间安全比较(防时序攻击)
+
+### 8.3 HTTPS/SSL
+- 可选启用(config.ini `[ssl] enabled=1`)
+- 证书+私钥 PEM格式
+- SSL会话缓存(10分钟)
+- 优先ECDHE+AESGCM/ChaCha20密码套件
+- HTTP→HTTPS自动重定向(可选)
+
+### 8.4 frpc admin API 限制
+- **仅允许本机127.0.0.1调用**
+- 禁止通过frp隧道暴露到公网
+- Basic Auth认证保护
+- 超时严格限制(connect=1s, total=2s)
+
+### 8.5 路径安全
+- Web文件访问: `is_safe_file_path()` 防路径穿越
+- 禁止 `..` / `/` / `\` 字符
+- `realpath()` 验证绝对路径在基础目录内
+
+### 8.6 配置文件校验
+- CPU序列号 + MD5三重校验 → 防止未授权设备运行
+- config.ini 含敏感信息,.gitignore 已排除
+
+---
+
+## 9. 版本变更记录
+
+详见 [CHANGELOG.md](CHANGELOG.md)
+
+主要版本里程碑:
+- **v43**: RTSP ROI裁剪、DRM硬件解码、4路独立视频流
+- **v43.2 fix11-fix17**: 交替锁定机制完善(非对称等待、2小时超时清零)
+- **fix18**: 称重+MQTT主进程合并、日志logrotate
+- **fix19**: 文件清理重构(照片按容量、日志按天数)
+- **fix20**: 推理耗时追踪、MQTT完整迁移
+- **fix21-fix23**: P0时间对齐bug修复、抓拍时间全链路一致
+- **fix24**: 登录认证系统、Web管理功能(系统监控+frpc+SSH+日志)、HTTPS/SSL
+
+---
+
+## 10. 待改进项
+
+### 10.1 技术债务
+1. **配置热更新不完整**: ROI可热写回config.ini,但其他配置修改后需重启
+2. **认证系统JSON解析**: 使用手写简单解析而非cJSON,鲁棒性待提升
+3. **全局变量过多**: 大量 `extern` 全局变量,耦合度高,应逐步迁移到单例/依赖注入
+4. **Web前端内嵌**: HTML直接写在C++字符串中,维护困难,应迁移到独立模板
+5. **缺少单元测试**: 无自动化测试框架覆盖核心逻辑
+6. **数据库迁移**: 旧数据库需手动cp迁移,缺少自动迁移工具
+7. **日志内存缓冲**: 超过buffer_size时丢弃旧日志,可能丢失关键信息
+
+### 10.2 改进方向
+1. **gRPC替代HTTP**: 平台API通信改用gRPC提升效率
+2. **TensorRT替代MNN**: 利用NPU/GPU进一步加速推理
+3. **WebSocket替代轮询**: Web监控数据改为实时推送
+4. **Docker化部署**: 简化依赖安装和环境配置
+5. **OTA远程更新**: 结合frpc实现远程固件升级
+6. **多站点支持**: 单进程管理多个出入口点位
+
+---
+
+> **本文档持续更新**  
+> 最后更新: 2026-07-06 | 基于 PlateRecApp v43.2 fix24 源码

+ 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

+ 8 - 4
src/log_manager.cpp

@@ -278,7 +278,8 @@ bool MemoryLogBuffer::flush_to_file(std::string& error) {
         std::lock_guard<std::mutex> lock(stats_mutex_);
         time_t now = time(NULL);
         char time_buf[64];
-        struct tm *tm = localtime(&now);
+        struct tm tm_buf;
+        struct tm* tm = localtime_r(&now, &tm_buf);
         strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm);
         last_flush_time_ = time_buf;
     }
@@ -369,7 +370,8 @@ void MemoryLogBuffer::flush_thread_func() {
 
         // 检查是否到达刷盘时间
         time_t now = time(NULL);
-        struct tm* tm_now = localtime(&now);
+        struct tm tm_now_buf;
+        struct tm* tm_now = localtime_r(&now, &tm_now_buf);
 
         if (tm_now->tm_hour == target_hour && tm_now->tm_min == target_minute) {
             std::string error;
@@ -462,7 +464,8 @@ bool MemoryLogBuffer::parse_flush_time(const std::string& time_str, int& hour, i
 
 std::string MemoryLogBuffer::generate_archive_filename() {
     time_t now = time(NULL);
-    struct tm* tm_now = localtime(&now);
+    struct tm tm_now_buf;
+    struct tm* tm_now = localtime_r(&now, &tm_now_buf);
     char buf[64];
     // 生成 PlateRecApp_20260705.log 格式
     snprintf(buf, sizeof(buf), "PlateRecApp_%04d%02d%02d.log",
@@ -472,7 +475,8 @@ std::string MemoryLogBuffer::generate_archive_filename() {
 
 std::string MemoryLogBuffer::current_timestamp() {
     time_t now = time(NULL);
-    struct tm* tm_now = localtime(&now);
+    struct tm tm_now_buf;
+    struct tm* tm_now = localtime_r(&now, &tm_now_buf);
     char buf[32];
     snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
              tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday,

+ 6 - 5
src/main.cpp

@@ -111,7 +111,7 @@ int main(int argc, char** argv)
 	// 需要校正时区偏移才能使ORDER BY正确排序
 	{
 		time_t now_utc = time(NULL);
-		struct tm* utc_tm = gmtime(&now_utc);
+		struct tm utc_tm_buf; struct tm* utc_tm = gmtime_r(&now_utc, &utc_tm_buf);
 		time_t utc_as_local = mktime(utc_tm);  // 把UTC时刻当作本地时间解释
 		g_tz_offset_seconds = (int)difftime(now_utc, utc_as_local);
 		std::cout << "[时区] 本地时区偏移: " << g_tz_offset_seconds << " 秒 ("
@@ -791,10 +791,11 @@ int main(int argc, char** argv)
 		capture_thread.join();
 	}
 
-	delete plate_rec_app.capture_front_in;
-	delete plate_rec_app.capture_front_out;
-	delete plate_rec_app.capture_side_in;
-	delete plate_rec_app.capture_side_out;
+	// fix27: 空指针保护
+	if (plate_rec_app.capture_front_in) { delete plate_rec_app.capture_front_in; plate_rec_app.capture_front_in = nullptr; }
+	if (plate_rec_app.capture_front_out) { delete plate_rec_app.capture_front_out; plate_rec_app.capture_front_out = nullptr; }
+	if (plate_rec_app.capture_side_in) { delete plate_rec_app.capture_side_in; plate_rec_app.capture_side_in = nullptr; }
+	if (plate_rec_app.capture_side_out) { delete plate_rec_app.capture_side_out; plate_rec_app.capture_side_out = nullptr; }
 
 	hyperlpr_lib_deinit();
 	close_database();

+ 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

+ 7 - 6
src/web_server.cpp

@@ -971,12 +971,13 @@ void start_web_server() {
 		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);
+		// fix27: 添加 cJSON 类型检查,防止类型不匹配导致崩溃
+		if (j_side && cJSON_IsString(j_side) && j_side->valuestring) side = j_side->valuestring;
+		if (j_x && cJSON_IsNumber(j_x)) x = j_x->valueint;
+		if (j_y && cJSON_IsNumber(j_y)) y = j_y->valueint;
+		if (j_w && cJSON_IsNumber(j_w)) w = j_w->valueint;
+		if (j_h && cJSON_IsNumber(j_h)) h = j_h->valueint;
+		if (j_enabled) enabled = (j_enabled->type == cJSON_True || (cJSON_IsNumber(j_enabled) && j_enabled->valueint == 1));
 
 		// 坐标校验
 		if (x < 0 || y < 0 || w < 0 || h < 0) {

+ 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 发现的问题和用户技术问答必须归档,按编号递增,持续更新