瀏覽代碼

initial: uploaded source code

steven_roc 17 小時之前
當前提交
b71a3c4a01
共有 67 個文件被更改,包括 43378 次插入 和 0 次删除
  1. 430 0
      CMakeLists.txt
  2. 372 0
      src/auth_api.cpp
  3. 13 0
      src/auth_api.h
  4. 66 0
      src/auth_crypto.cpp
  5. 25 0
      src/auth_crypto.h
  6. 407 0
      src/auth_db.cpp
  7. 92 0
      src/auth_db.h
  8. 265 0
      src/auth_middleware.cpp
  9. 51 0
      src/auth_middleware.h
  10. 95 0
      src/auth_rate_limiter.h
  11. 187 0
      src/auth_session.cpp
  12. 62 0
      src/auth_session.h
  13. 122 0
      src/bcrypt/bcrypt.c
  14. 38 0
      src/bcrypt/bcrypt.h
  15. 154 0
      src/bcrypt/bf_sbox.h
  16. 240 0
      src/common.cpp
  17. 784 0
      src/common.h
  18. 250 0
      src/config.cpp
  19. 6 0
      src/config.h
  20. 966 0
      src/database.cpp
  21. 37 0
      src/database.h
  22. 193 0
      src/feishu_client.cpp
  23. 15 0
      src/feishu_client.h
  24. 368 0
      src/frpc_manager.cpp
  25. 50 0
      src/frpc_manager.h
  26. 311 0
      src/frpc_manager1.cpp
  27. 523 0
      src/log_manager.cpp
  28. 150 0
      src/log_manager.h
  29. 806 0
      src/main.cpp
  30. 4 0
      src/monitor.cpp
  31. 4 0
      src/monitor.h
  32. 217 0
      src/mqtt_client.cpp
  33. 36 0
      src/mqtt_client.h
  34. 202 0
      src/mqtt_client1.cpp
  35. 758 0
      src/network_client.cpp
  36. 24 0
      src/network_client.h
  37. 742 0
      src/network_client1.cpp
  38. 785 0
      src/plate_recognizer.cpp
  39. 23 0
      src/plate_recognizer.h
  40. 250 0
      src/rtsp_capture.cpp
  41. 45 0
      src/rtsp_capture.h
  42. 184 0
      src/ssh_manager.cpp
  43. 35 0
      src/ssh_manager.h
  44. 717 0
      src/station_lock.cpp
  45. 47 0
      src/station_lock.h
  46. 756 0
      src/system_metrics_manager.cpp
  47. 167 0
      src/system_metrics_manager.h
  48. 274 0
      src/system_monitor.cpp
  49. 79 0
      src/system_monitor.h
  50. 247 0
      src/system_monitor1.cpp
  51. 1264 0
      src/utils.cpp
  52. 37 0
      src/utils.h
  53. 2174 0
      src/web_server.cpp
  54. 20 0
      src/web_server.h
  55. 1732 0
      src/web_server1.cpp
  56. 584 0
      src/weight_scale.cpp
  57. 56 0
      src/weight_scale.h
  58. 3191 0
      third_party/cJSON.c
  59. 306 0
      third_party/cJSON.h
  60. 180 0
      third_party/cvxFont.cpp
  61. 71 0
      third_party/cvxFont.h
  62. 20091 0
      third_party/httplib.h
  63. 255 0
      third_party/hyper_lpr_sdk.h
  64. 333 0
      third_party/ini.c
  65. 189 0
      third_party/ini.h
  66. 193 0
      third_party/md5ex1.c
  67. 28 0
      third_party/md5ex1.h

+ 430 - 0
CMakeLists.txt

@@ -0,0 +1,430 @@
+# =============================================================================
+# 项目名称: HyperLPR3-LinuxAPP
+# 功能: 车牌识别应用程序
+# 优化目标: 减小可执行文件体积、提升编译效率、增强可移植性
+# =============================================================================
+
+# -----------------------------------------------------------------------------
+# 1. CMake 最低版本要求
+# 说明: 3.10.2 是 Ubuntu 18.04 默认版本,如需使用新特性可提高到 3.16+
+# -----------------------------------------------------------------------------
+#cmake_minimum_required(VERSION 3.10.2)
+#cmake_minimum_required(VERSION 3.16.2)
+#cmake_minimum_required(VERSION 3.28)
+#当前CMake 3.25.1
+
+cmake_minimum_required(VERSION 3.25.1)
+
+# -----------------------------------------------------------------------------
+# 2. 项目定义
+# -----------------------------------------------------------------------------
+project(HyperLPR3-LinuxAPP
+    VERSION 1.0.0
+    DESCRIPTION "HyperLPR3 车牌识别 Linux 应用"
+    LANGUAGES CXX C
+)
+
+# -----------------------------------------------------------------------------
+# 3. C++ 标准设置
+# 说明: 使用 C++17 标准,启用标准强制检查
+# -----------------------------------------------------------------------------
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)  # 强制要求编译器支持 C++17,不支持则报错
+set(CMAKE_CXX_EXTENSIONS OFF)        # 禁用编译器扩展(如 GNU++17),确保跨平台兼容
+
+# -----------------------------------------------------------------------------
+# 4. 构建类型设置
+# 说明: 
+#   - Release: 发布模式,启用优化,去除调试信息,体积最小
+#   - Debug:   调试模式,保留调试信息,便于开发调试
+#   - RelWithDebInfo: 带调试信息的发布模式
+#   - MinSizeRel: 最小体积发布模式(比 Release 更激进优化体积)
+# -----------------------------------------------------------------------------
+if(NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
+endif()
+
+# 设置可选的构建类型(供 cmake-gui 或 ccmake 使用)
+set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 
+    "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
+
+# -----------------------------------------------------------------------------
+# 5. 编译器优化标志(Release 模式专用)
+# 说明: 这些标志专门用于减小编译后的可执行文件体积
+# -----------------------------------------------------------------------------
+if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
+    # -O3: 最高级别优化(速度优先)
+    # -Os: 优化代码大小(体积优先,推荐用于嵌入式)
+    # -s:  去除符号表信息(strip)
+    # -fvisibility=hidden: 隐藏符号,减少导出表大小
+    # -fvisibility-inlines-hidden: 隐藏内联函数符号
+    # -ffunction-sections -fdata-sections: 为每个函数/数据创建独立段
+    # -Wl,--gc-sections: 链接时删除未使用的段
+    # -Wl,--strip-all: 链接时去除所有符号
+    # -flto: 链接时优化(Link Time Optimization),可显著减小体积
+
+    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -s")
+    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -s")
+
+    # 可选: 启用 LTO(链接时优化)- 需要编译器支持,可减小 10-30% 体积
+    # 注意: 会增加编译时间,某些编译器可能有兼容性问题
+    # set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
+
+    # 可选: 最小体积模式(比 Release 更激进)
+    # set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -s -fvisibility=hidden -fvisibility-inlines-hidden")
+endif()
+
+# -----------------------------------------------------------------------------
+# 6. 优先使用动态链接库(减小可执行文件体积的关键)
+# 说明: 
+#   - ON:  优先查找动态库(.so),体积最小,但部署时需确保目标机有对应库
+#   - OFF: 优先查找静态库(.a),体积大但独立性强
+# 建议: 开发/测试用动态库,发布时根据部署环境决定
+# -----------------------------------------------------------------------------
+option(USE_STATIC_LIBS "使用静态库链接(体积大但独立性强)" OFF)
+
+if(NOT USE_STATIC_LIBS)
+    set(BUILD_SHARED_LIBS ON)
+    # 设置动态库查找优先级
+    set(CMAKE_FIND_LIBRARY_SUFFIXES ".so" ".so.1" ".so.0" ".a")
+else()
+    set(BUILD_SHARED_LIBS OFF)
+    set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".so")
+endif()
+
+# -----------------------------------------------------------------------------
+# 7. 查找 PkgConfig 模块
+# 说明: pkg-config 用于自动查找系统安装的库的头文件和链接参数
+# -----------------------------------------------------------------------------
+find_package(PkgConfig REQUIRED)
+
+# -----------------------------------------------------------------------------
+# 8. 查找第三方库
+# -----------------------------------------------------------------------------
+
+# 8.1 FreeType2 - 字体渲染库
+# 用途: 在图像上绘制中文字符
+pkg_check_modules(FREETYPE REQUIRED freetype2)
+
+# 8.2 SQLite3 - 轻量级数据库
+# 用途: 存储车牌识别记录
+pkg_check_modules(SQLITE3 REQUIRED sqlite3)
+
+# 8.3 cURL - HTTP 客户端库
+# 用途: 网络请求(如上传识别结果到服务器)
+find_package(CURL REQUIRED)
+
+# 8.4 OpenCV - 计算机视觉库(体积大户!)
+# 说明: 如果只需要核心模块,可以指定 COMPONENTS 减少依赖
+# 例如: find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs highgui)
+find_package(OpenCV REQUIRED)
+
+# 8.5 OpenSSL - 加密/SSL 库
+# 用途: HTTPS 请求、数据加密
+find_package(OpenSSL REQUIRED)
+
+# 8.6 FFmpeg - 视频解码库(v43新增)
+# 用途: RTSP流解码,HEVC DRM硬件解码,H.264软解
+pkg_check_modules(AVFORMAT REQUIRED libavformat)
+pkg_check_modules(AVCODEC REQUIRED libavcodec)
+pkg_check_modules(AVUTIL REQUIRED libavutil)
+pkg_check_modules(SWSCALE REQUIRED libswscale)
+
+# 8.7 libcrypt - 密码哈希库(fix24 auth系统新增)
+# 用途: bcrypt 密码哈希(通过系统 libcrypt 的 crypt_r)
+find_library(CRYPT_LIBRARY crypt
+    PATHS
+        /usr/lib
+        /usr/lib/aarch64-linux-gnu
+        /usr/lib/x86_64-linux-gnu
+        /usr/local/lib
+    DOC "libcrypt path"
+)
+if(NOT CRYPT_LIBRARY)
+    message(WARNING "libcrypt not found, auth system will not work. Install libcrypt-dev.")
+else()
+    message(STATUS "Found libcrypt: ${CRYPT_LIBRARY}")
+endif()
+
+# -----------------------------------------------------------------------------
+# 9. 查找 cJSON 库
+# 说明: cJSON 可能没有 .pc 文件,使用多种方式查找
+# -----------------------------------------------------------------------------
+
+# 方法1: 尝试使用 pkg-config 查找(某些系统可能安装了 cjson.pc)
+pkg_check_modules(CJSON cjson)
+
+if(CJSON_FOUND)
+    # 使用 pkg-config 找到的结果
+    message(STATUS "通过 pkg-config 找到 cJSON")
+    set(CJSON_LIBRARY ${CJSON_LIBRARIES})
+    set(CJSON_INCLUDE_DIR ${CJSON_INCLUDE_DIRS})
+else()
+    # 方法2: 手动查找库文件
+    message(STATUS "pkg-config 未找到 cJSON,尝试手动查找...")
+
+    # 查找库文件(优先动态库)
+    find_library(CJSON_LIBRARY 
+        NAMES cjson
+        PATHS 
+            /usr/lib 
+            /usr/lib/aarch64-linux-gnu 
+            /usr/lib/x86_64-linux-gnu
+            /usr/local/lib
+            /opt/cjson/lib
+        DOC "cJSON library path"
+    )
+
+    # 查找头文件
+    find_path(CJSON_INCLUDE_DIR 
+        NAMES cJSON.h
+        PATHS 
+            /usr/include 
+            /usr/include/cjson
+            /usr/local/include
+            /usr/local/include/cjson
+            /opt/cjson/include
+        DOC "cJSON header path"
+    )
+endif()
+
+# 检查是否找到 cJSON
+if(NOT CJSON_LIBRARY)
+    message(WARNING "未找到 cJSON 库,尝试使用默认路径...")
+    # 尝试常见默认路径
+    if(EXISTS "/usr/lib/aarch64-linux-gnu/libcjson.so")
+        set(CJSON_LIBRARY "/usr/lib/aarch64-linux-gnu/libcjson.so")
+    elseif(EXISTS "/usr/lib/x86_64-linux-gnu/libcjson.so")
+        set(CJSON_LIBRARY "/usr/lib/x86_64-linux-gnu/libcjson.so")
+    elseif(EXISTS "/usr/lib/libcjson.so")
+        set(CJSON_LIBRARY "/usr/lib/libcjson.so")
+    elseif(EXISTS "/usr/local/lib/libcjson.so")
+        set(CJSON_LIBRARY "/usr/local/lib/libcjson.so")
+    else()
+        message(FATAL_ERROR "无法找到 cJSON 库!请安装: sudo apt install libcjson-dev")
+    endif()
+endif()
+
+if(NOT CJSON_INCLUDE_DIR)
+    # 如果找不到头文件目录,使用系统默认包含路径
+    # 并假设头文件直接在 /usr/include 下
+    if(EXISTS "/usr/include/cJSON.h")
+        set(CJSON_INCLUDE_DIR "/usr/include")
+    elseif(EXISTS "/usr/include/cjson/cJSON.h")
+        set(CJSON_INCLUDE_DIR "/usr/include/cjson")
+    else()
+        message(WARNING "未找到 cJSON 头文件目录,使用系统默认路径")
+        set(CJSON_INCLUDE_DIR "")  # 空表示使用系统默认路径
+    endif()
+endif()
+
+# 验证找到的文件
+message(STATUS "cJSON 库路径: ${CJSON_LIBRARY}")
+message(STATUS "cJSON 头文件路径: ${CJSON_INCLUDE_DIR}")
+
+if(NOT EXISTS ${CJSON_LIBRARY})
+    message(FATAL_ERROR "cJSON 库文件不存在: ${CJSON_LIBRARY}")
+endif()
+
+# -----------------------------------------------------------------------------
+# 10. HyperLPR3 SDK 设置
+# 说明: 车牌识别核心库,可能包含大型模型数据
+# -----------------------------------------------------------------------------
+set(HYPERLPR3_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/hyperlpr3")
+set(HYPERLPR3_INCLUDE_DIR "${HYPERLPR3_ROOT}/include")
+set(HYPERLPR3_LIB_DIR "${HYPERLPR3_ROOT}/lib")
+
+# 检查 SDK 是否存在
+if(NOT EXISTS ${HYPERLPR3_INCLUDE_DIR})
+    message(FATAL_ERROR "HyperLPR3 SDK 头文件目录不存在: ${HYPERLPR3_INCLUDE_DIR}")
+endif()
+
+if(NOT EXISTS ${HYPERLPR3_LIB_DIR})
+    message(FATAL_ERROR "HyperLPR3 SDK 库目录不存在: ${HYPERLPR3_LIB_DIR}")
+endif()
+
+# 添加 SDK 库目录到链接器搜索路径
+link_directories(${HYPERLPR3_LIB_DIR})
+
+# -----------------------------------------------------------------------------
+# 11. 包含目录设置
+# 说明: 使用 target_include_directories 替代全局 include_directories(现代 CMake 推荐)
+# -----------------------------------------------------------------------------
+
+# 源文件列表
+set(SOURCES
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/common.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/config.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/rtsp_capture.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/plate_recognizer.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/station_lock.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/database.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/network_client.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/feishu_client.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/web_server.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/monitor.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/utils.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/weight_scale.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/mqtt_client.cpp
+    # ✅ fix24 功能一:登录认证系统
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/bcrypt/bcrypt.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/auth_crypto.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/auth_db.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/auth_session.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/auth_middleware.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/auth_api.cpp
+    # fix24 v24: Web管理功能
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/system_monitor.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/frpc_manager.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/ssh_manager.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/log_manager.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/system_metrics_manager.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cvxFont.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/third_party/md5ex1.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ini.c
+)
+
+# 创建可执行文件
+add_executable(PlateRecApp ${SOURCES})
+
+# 为目标设置包含目录(PRIVATE 表示仅本目标使用)
+set(INCLUDE_DIRS
+    ${PROJECT_SOURCE_DIR}/src                 # 模块头文件
+    ${PROJECT_SOURCE_DIR}/third_party         # 第三方头文件
+    ${PROJECT_SOURCE_DIR}                    # 项目根目录
+    ${HYPERLPR3_INCLUDE_DIR}                   # HyperLPR3 SDK 头文件
+    ${OpenCV_INCLUDE_DIRS}                     # OpenCV 头文件
+    ${FREETYPE_INCLUDE_DIRS}                   # FreeType 头文件
+    ${SQLITE3_INCLUDE_DIRS}                    # SQLite3 头文件
+    ${AVFORMAT_INCLUDE_DIRS}                   # FFmpeg libavformat 头文件(v43新增)
+    ${AVCODEC_INCLUDE_DIRS}                    # FFmpeg libavcodec 头文件(v43新增)
+    ${AVUTIL_INCLUDE_DIRS}                     # FFmpeg libavutil 头文件(v43新增)
+    ${SWSCALE_INCLUDE_DIRS}                    # FFmpeg libswscale 头文件(v43新增)
+)
+
+# 只有当找到有效的 cJSON 头文件目录时才添加
+if(CJSON_INCLUDE_DIR AND NOT CJSON_INCLUDE_DIR STREQUAL "")
+    list(APPEND INCLUDE_DIRS ${CJSON_INCLUDE_DIR})
+endif()
+
+target_include_directories(PlateRecApp PRIVATE ${INCLUDE_DIRS})
+
+# -----------------------------------------------------------------------------
+# 12. 链接库设置
+# 说明: 链接顺序很重要!被依赖的库放在后面
+# 顺序原则: 目标 <- 直接依赖 <- 间接依赖 <- 系统库
+# -----------------------------------------------------------------------------
+target_link_libraries(PlateRecApp PRIVATE
+    # 1. HyperLPR3 SDK(车牌识别核心,可能包含模型数据)
+    hyperlpr3
+
+    # 2. 数据库和配置库
+    ${SQLITE3_LIBRARIES}    # SQLite3
+
+    # 3. 图像/字体处理库
+    ${FREETYPE_LIBRARIES}   # FreeType2(字体渲染)
+    ${OpenCV_LIBS}          # OpenCV(计算机视觉,体积较大)
+
+    # 4. 网络/加密库
+    CURL::libcurl           # cURL(HTTP 客户端)
+    ${OPENSSL_LIBRARIES}    # OpenSSL(加密/HTTPS)
+    mosquittopp             # MQTT C++客户端(fix20新增)
+    mosquitto               # MQTT C库(fix20新增)
+    ${CRYPT_LIBRARY}        # libcrypt(密码哈希,fix24 auth系统新增)
+
+    # 5. 数据格式库
+    ${CJSON_LIBRARY}        # cJSON(JSON 解析)
+
+    # 6. FFmpeg 视频解码库(v43新增)
+    ${AVFORMAT_LIBRARIES}   # libavformat(容器解封装)
+    ${AVCODEC_LIBRARIES}    # libavcodec(视频解码,含DRM硬解)
+    ${AVUTIL_LIBRARIES}     # libavutil(工具函数)
+    ${SWSCALE_LIBRARIES}    # libswscale(像素格式转换)
+
+    # 7. 系统线程库(通常自动链接,显式指定更安全)
+    pthread
+
+    # 8. 数学库(某些平台需要)
+    m
+
+    # 9. 实时库(某些平台需要)
+    rt
+)
+
+# -----------------------------------------------------------------------------
+# 13. 链接选项设置(体积优化)
+# -----------------------------------------------------------------------------
+if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
+    # --as-needed: 只链接实际使用的符号(避免过度链接)
+    # --gc-sections: 删除未使用的段
+    # -O1: 链接时优化级别
+    target_link_options(PlateRecApp PRIVATE
+        "LINKER:--as-needed"
+        "LINKER:--gc-sections"
+    )
+endif()
+
+# -----------------------------------------------------------------------------
+# 14. 编译后处理(Release 模式)
+# 说明: 使用 strip 去除符号表,进一步减小体积
+# -----------------------------------------------------------------------------
+if(CMAKE_BUILD_TYPE STREQUAL "Release")
+    add_custom_command(TARGET PlateRecApp POST_BUILD
+        COMMAND ${CMAKE_STRIP} $<TARGET_FILE:PlateRecApp>
+        COMMENT "去除符号表以减小程序体积..."
+    )
+endif()
+
+# -----------------------------------------------------------------------------
+# 15. 安装规则(可选)
+# 说明: 定义 make install 时的安装路径
+# -----------------------------------------------------------------------------
+install(TARGETS PlateRecApp
+    RUNTIME DESTINATION bin          # 可执行文件
+    LIBRARY DESTINATION lib            # 动态库
+    ARCHIVE DESTINATION lib/static     # 静态库
+)
+
+# -----------------------------------------------------------------------------
+# 16. 输出配置信息(便于调试)
+# -----------------------------------------------------------------------------
+message(STATUS "========================================")
+message(STATUS "项目配置信息:")
+message(STATUS "  项目名称: ${PROJECT_NAME}")
+message(STATUS "  项目版本: ${PROJECT_VERSION}")
+message(STATUS "  C++ 标准: C++${CMAKE_CXX_STANDARD}")
+message(STATUS "  构建类型: ${CMAKE_BUILD_TYPE}")
+message(STATUS "  使用静态库: ${USE_STATIC_LIBS}")
+message(STATUS "----------------------------------------")
+message(STATUS "库路径信息:")
+message(STATUS "  OpenCV: ${OpenCV_INCLUDE_DIRS}")
+message(STATUS "  OpenCV 库: ${OpenCV_LIBS}")
+message(STATUS "  HyperLPR3: ${HYPERLPR3_INCLUDE_DIR}")
+message(STATUS "  cJSON 库: ${CJSON_LIBRARY}")
+message(STATUS "  cJSON 头文件: ${CJSON_INCLUDE_DIR}")
+message(STATUS "  libcrypt: ${CRYPT_LIBRARY}")
+message(STATUS "========================================")
+
+# -----------------------------------------------------------------------------
+# 17. 体积优化建议(构建时提示)
+# -----------------------------------------------------------------------------
+if(CMAKE_BUILD_TYPE STREQUAL "Release" AND NOT USE_STATIC_LIBS)
+    message(STATUS "")
+    message(STATUS "【体积优化提示】")
+    message(STATUS "  1. 当前使用动态链接,确保目标机安装以下库:")
+    message(STATUS "     - OpenCV: libopencv-core, libopencv-imgproc, ...")
+    message(STATUS "     - OpenSSL: libssl, libcrypto")
+    message(STATUS "     - SQLite3: libsqlite3")
+    message(STATUS "     - cJSON: libcjson")
+    message(STATUS "     - FreeType2: libfreetype")
+    message(STATUS "     - cURL: libcurl")
+    message(STATUS "     - libcrypt: libcrypt (密码哈希)")
+    message(STATUS "")
+    message(STATUS "  2. 如需静态链接(独立运行但体积大):")
+    message(STATUS "     cmake -DUSE_STATIC_LIBS=ON ..")
+    message(STATUS "")
+    message(STATUS "  3. 如需进一步减小体积,启用 LTO:")
+    message(STATUS "     取消注释第 56 行的 set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)")
+    message(STATUS "")
+endif()

+ 372 - 0
src/auth_api.cpp

@@ -0,0 +1,372 @@
+/*
+ * auth_api.cpp - Login/Logout/Status API endpoint implementation
+ */
+
+#include "auth_api.h"
+#include "auth_crypto.h"
+#include "auth_db.h"
+#include "auth_session.h"
+#include "auth_rate_limiter.h"
+#include "auth_middleware.h"
+#include "common.h"
+#include <cstdio>
+#include <cstring>
+#include <ctime>
+#include <string>
+
+// These are declared in common.h/cpp as globals
+extern auth::AuthDB *g_auth_db;
+extern auth::SessionManager *g_session_mgr;
+extern auth::RateLimiter *g_rate_limiter;
+extern auth::AuthMiddleware *g_auth_middleware;
+
+namespace auth {
+
+/* POST /api/auth/login */
+static void handle_login(const httplib::Request &req, httplib::Response &res) {
+    if (!g_auth_db || !g_session_mgr || !g_rate_limiter) {
+        res.status = 500;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"success\":false,\"message\":\"Auth system not initialized\"}";
+        return;
+    }
+
+    std::string client_ip = AuthMiddleware::get_client_ip(req);
+
+    // Rate limiting check
+    if (!g_rate_limiter->check(client_ip)) {
+        res.status = 429;
+        res.set_header("Content-Type", "application/json");
+        int remaining_sec = 300; // 5 minute window
+        char msg[128];
+        snprintf(msg, sizeof(msg),
+                 "{\"success\":false,\"message\":\"Too many login attempts. Try again in %d seconds\"}",
+                 remaining_sec);
+        res.body = msg;
+
+        AuditEntry audit;
+        audit.timestamp = time(nullptr);
+        audit.event_type = "rate_limited";
+        audit.remote_ip = client_ip;
+        audit.details = "Login rate limit exceeded";
+        audit.success = 0;
+        g_auth_db->add_audit_log(audit);
+        return;
+    }
+
+    // Parse JSON body
+    std::string username, password;
+    bool remember = false;
+
+    // Try to parse as JSON
+    // Simple JSON parsing for {username, password, remember}
+    auto parse_json_string = [](const std::string &json, const std::string &key) -> std::string {
+        std::string search = "\"" + key + "\"";
+        size_t pos = json.find(search);
+        if (pos == std::string::npos) return "";
+        pos = json.find(':', pos);
+        if (pos == std::string::npos) return "";
+        pos++;
+        // Skip whitespace
+        while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++;
+        if (pos >= json.size()) return "";
+        if (json[pos] == '"') {
+            pos++;
+            size_t end = json.find('"', pos);
+            if (end == std::string::npos) return "";
+            return json.substr(pos, end - pos);
+        }
+        return "";
+    };
+
+    auto parse_json_bool = [](const std::string &json, const std::string &key) -> bool {
+        std::string search = "\"" + key + "\"";
+        size_t pos = json.find(search);
+        if (pos == std::string::npos) return false;
+        pos = json.find(':', pos);
+        if (pos == std::string::npos) return false;
+        pos++;
+        while (pos < json.size() && json[pos] == ' ') pos++;
+        return (json.substr(pos, 4) == "true");
+    };
+
+    if (req.body.find('{') != std::string::npos) {
+        username = parse_json_string(req.body, "username");
+        password = parse_json_string(req.body, "password");
+        remember = parse_json_bool(req.body, "remember");
+    } else {
+        username = req.get_param_value("username");
+        password = req.get_param_value("password");
+        remember = (req.get_param_value("remember") == "true" ||
+                    req.get_param_value("remember") == "1");
+    }
+
+    // Validate input
+    if (username.empty() || password.empty()) {
+        res.status = 400;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"success\":false,\"message\":\"Username and password required\"}";
+        return;
+    }
+
+    // Look up user
+    User user;
+    if (!g_auth_db->get_user_by_name(username, user)) {
+        g_rate_limiter->record_failure(client_ip);
+        res.status = 401;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"success\":false,\"message\":\"Invalid username or password\"}";
+
+        AuditEntry audit;
+        audit.timestamp = time(nullptr);
+        audit.event_type = "failed_login";
+        audit.username = username;
+        audit.remote_ip = client_ip;
+        audit.details = "User not found";
+        audit.success = 0;
+        g_auth_db->add_audit_log(audit);
+        return;
+    }
+
+    // Check if user is enabled
+    if (!user.enabled) {
+        res.status = 403;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"success\":false,\"message\":\"Account is disabled\"}";
+        return;
+    }
+
+    // Check if account is locked
+    time_t now = time(nullptr);
+    if (user.locked_until > now) {
+        int remaining = (int)(user.locked_until - now);
+        char msg[128];
+        snprintf(msg, sizeof(msg),
+                 "{\"success\":false,\"message\":\"Account locked. Try again in %d seconds\"}",
+                 remaining);
+        res.status = 423;
+        res.set_header("Content-Type", "application/json");
+        res.body = msg;
+        return;
+    }
+
+    // Verify password
+    if (!bcrypt_verify(password, user.password_hash)) {
+        // Increment failed attempts
+        int failures = user.failed_attempts + 1;
+        time_t lock_until = 0;
+        if (failures >= 5) {
+            lock_until = now + 900; // Lock for 15 minutes
+            failures = 0; // Reset after lock
+        }
+        g_auth_db->update_failed_attempts(user.id, failures, lock_until);
+
+        g_rate_limiter->record_failure(client_ip);
+        res.status = 401;
+        res.set_header("Content-Type", "application/json");
+
+        if (lock_until > 0) {
+            res.body = "{\"success\":false,\"message\":\"Too many failed attempts. Account locked for 15 minutes\"}";
+        } else {
+            int remaining = g_rate_limiter->remaining(client_ip);
+            char msg[128];
+            snprintf(msg, sizeof(msg),
+                     "{\"success\":false,\"message\":\"Invalid username or password\",\"remaining_attempts\":%d}",
+                     remaining);
+            res.body = msg;
+        }
+
+        AuditEntry audit;
+        audit.timestamp = now;
+        audit.event_type = "failed_login";
+        audit.username = username;
+        audit.remote_ip = client_ip;
+        audit.details = "Wrong password (attempt " + std::to_string(failures) + ")";
+        audit.success = 0;
+        g_auth_db->add_audit_log(audit);
+        return;
+    }
+
+    // Success! Create session
+    std::string session_id = g_session_mgr->create_session(
+        user.id, user.username, client_ip, remember, user.role);
+
+    if (session_id.empty()) {
+        res.status = 500;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"success\":false,\"message\":\"Failed to create session\"}";
+        return;
+    }
+
+    // Reset failed attempts on successful login
+    g_auth_db->reset_failed_attempts(user.id);
+    g_rate_limiter->clear(client_ip);
+
+    // Get CSRF token
+    Session sess;
+    g_session_mgr->validate_session(session_id, sess);
+
+    // Set cookies
+    int max_age = remember ? 604800 : 28800; // 7 days or 8 hours
+    char cookie_buf[256];
+    snprintf(cookie_buf, sizeof(cookie_buf),
+             "session_id=%s; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=%d",
+             session_id.c_str(), max_age);
+    res.set_header("Set-Cookie", cookie_buf);
+
+    // CSRF token cookie (not HttpOnly so JS can read it for X-CSRF-Token header)
+    snprintf(cookie_buf, sizeof(cookie_buf),
+             "csrf_token=%s; Path=/; SameSite=Strict; Max-Age=%d",
+             sess.csrf_token.c_str(), max_age);
+    res.set_header("Set-Cookie", cookie_buf);
+
+    // Clear login page cookies
+    snprintf(cookie_buf, sizeof(cookie_buf),
+             "remember_username=; Path=/; Max-Age=0");
+
+    res.status = 200;
+    res.set_header("Content-Type", "application/json");
+    res.body = "{\"success\":true,\"message\":\"Login successful\"}";
+
+    printf("[auth] User '%s' logged in from %s (remember=%d)\n",
+           username.c_str(), client_ip.c_str(), remember);
+
+    AuditEntry audit;
+    audit.timestamp = now;
+    audit.event_type = "login";
+    audit.username = username;
+    audit.remote_ip = client_ip;
+    audit.success = 1;
+    g_auth_db->add_audit_log(audit);
+}
+
+/* POST /api/auth/logout */
+static void handle_logout(const httplib::Request &req, httplib::Response &res) {
+    if (!g_session_mgr) {
+        res.status = 500;
+        res.body = "{\"success\":false}";
+        return;
+    }
+
+    // Extract session_id from cookie
+    std::string session_id;
+    auto it = req.headers.find("Cookie");
+    if (it != req.headers.end()) {
+        std::string cookies = it->second;
+        std::string search = "session_id=";
+        size_t pos = cookies.find(search);
+        if (pos != std::string::npos) {
+            pos += search.length();
+            size_t end = cookies.find(';', pos);
+            if (end == std::string::npos) end = cookies.length();
+            session_id = cookies.substr(pos, end - pos);
+        }
+    }
+
+    if (!session_id.empty()) {
+        g_session_mgr->destroy_session(session_id);
+        printf("[auth] Session destroyed (logout)\n");
+    }
+
+    // Clear cookies
+    res.set_header("Set-Cookie",
+                   "session_id=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0");
+    res.set_header("Set-Cookie",
+                   "csrf_token=; Path=/; SameSite=Strict; Max-Age=0");
+
+    res.status = 200;
+    res.set_header("Content-Type", "application/json");
+    res.body = "{\"success\":true,\"message\":\"Logged out\"}";
+}
+
+/* GET /api/auth/status */
+static void handle_status(const httplib::Request &req, httplib::Response &res) {
+    if (!g_session_mgr || !g_auth_middleware) {
+        res.status = 200;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"authenticated\":false,\"auth_enabled\":false}";
+        return;
+    }
+
+    Session sess;
+    std::string session_id = g_auth_middleware->check_auth(req, sess);
+
+    if (!session_id.empty()) {
+        res.status = 200;
+        res.set_header("Content-Type", "application/json");
+        char buf[256];
+        snprintf(buf, sizeof(buf),
+                 "{\"authenticated\":true,\"auth_enabled\":true,\"username\":\"%s\","
+                 "\"role\":%d,\"csrf_token\":\"%s\"}",
+                 sess.username.c_str(), sess.user_role, sess.csrf_token.c_str());
+        res.body = buf;
+    } else {
+        res.status = 200;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"authenticated\":false,\"auth_enabled\":true}";
+    }
+}
+
+/* GET /api/auth/audit - get audit logs (admin only) */
+static void handle_audit(const httplib::Request &req, httplib::Response &res) {
+    if (!g_auth_db) {
+        res.status = 500;
+        res.body = "{\"error\":\"Auth DB not initialized\"}";
+        return;
+    }
+
+    // Check auth (should be admin)
+    Session sess;
+    std::string session_id = g_auth_middleware ?
+        g_auth_middleware->check_auth(req, sess) : "";
+
+    if (session_id.empty()) {
+        res.status = 401;
+        res.set_header("Content-Type", "application/json");
+        res.body = "{\"error\":\"Unauthorized\"}";
+        return;
+    }
+
+    int limit = 100, offset = 0;
+    std::string limit_str = req.get_param_value("limit");
+    std::string offset_str = req.get_param_value("offset");
+    if (!limit_str.empty()) limit = std::stoi(limit_str);
+    if (!offset_str.empty()) offset = std::stoi(offset_str);
+    if (limit > 500) limit = 500;
+
+    auto logs = g_auth_db->get_audit_logs(limit, offset);
+    int total = g_auth_db->get_audit_log_count();
+
+    // Build JSON response
+    std::string json = "{\"total\":" + std::to_string(total) + ",\"logs\":[";
+    for (size_t i = 0; i < logs.size(); i++) {
+        if (i > 0) json += ",";
+        char buf[512];
+        snprintf(buf, sizeof(buf),
+                 "{\"id\":%d,\"timestamp\":%ld,\"event_type\":\"%s\","
+                 "\"username\":\"%s\",\"remote_ip\":\"%s\","
+                 "\"details\":\"%s\",\"success\":%d}",
+                 logs[i].id, (long)logs[i].timestamp,
+                 logs[i].event_type.c_str(),
+                 logs[i].username.c_str(),
+                 logs[i].remote_ip.c_str(),
+                 logs[i].details.c_str(),
+                 logs[i].success);
+        json += buf;
+    }
+    json += "]}";
+
+    res.status = 200;
+    res.set_header("Content-Type", "application/json");
+    res.body = json;
+}
+
+void register_auth_routes(httplib::Server &svr) {
+    svr.Post("/api/auth/login", handle_login);
+    svr.Post("/api/auth/logout", handle_logout);
+    svr.Get("/api/auth/status", handle_status);
+    svr.Get("/api/auth/audit", handle_audit);
+    printf("[auth] Auth API routes registered\n");
+}
+
+} // namespace auth

+ 13 - 0
src/auth_api.h

@@ -0,0 +1,13 @@
+#pragma once
+/*
+ * auth_api.h - Login/Logout/Status API endpoint handlers
+ */
+
+#include <httplib.h>
+
+namespace auth {
+
+/* Register all auth-related API routes */
+void register_auth_routes(httplib::Server &svr);
+
+} // namespace auth

+ 66 - 0
src/auth_crypto.cpp

@@ -0,0 +1,66 @@
+/*
+ * auth_crypto.cpp - bcrypt C++ wrapper implementation
+ */
+
+#include "auth_crypto.h"
+#include "bcrypt/bcrypt.h"
+#include <cstring>
+#include <cstdio>
+#include <fcntl.h>
+#include <unistd.h>
+
+namespace auth {
+
+static const int BCRYPT_COST = 12;
+
+std::string bcrypt_hash(const std::string &password) {
+    char salt[64];
+    char hash[128];
+    if (bcrypt_gensalt(BCRYPT_COST, salt) != 0) return "";
+    if (bcrypt_hashpw(password.c_str(), salt, hash) != 0) return "";
+    return std::string(hash);
+}
+
+std::string bcrypt_hash_with_salt(const std::string &password, const std::string &salt) {
+    char hash[128];
+    if (bcrypt_hashpw(password.c_str(), salt.c_str(), hash) != 0) return "";
+    return std::string(hash);
+}
+
+bool bcrypt_verify(const std::string &password, const std::string &hash) {
+    return ::bcrypt_verify(password.c_str(), hash.c_str()) == 1;
+}
+
+std::string random_bytes(int byte_count) {
+    std::string result(byte_count, '\0');
+    int fd = open("/dev/urandom", O_RDONLY);
+    if (fd < 0) return "";
+    ssize_t n = read(fd, &result[0], byte_count);
+    close(fd);
+    if (n != byte_count) return "";
+    return result;
+}
+
+static const char hex_chars[] = "0123456789abcdef";
+
+std::string random_hex(int byte_count) {
+    std::string raw = random_bytes(byte_count);
+    if (raw.empty()) return "";
+    std::string hex(byte_count * 2, '\0');
+    for (int i = 0; i < byte_count; i++) {
+        hex[i * 2]     = hex_chars[(uint8_t)raw[i] >> 4];
+        hex[i * 2 + 1] = hex_chars[(uint8_t)raw[i] & 0x0f];
+    }
+    return hex;
+}
+
+bool secure_compare(const std::string &a, const std::string &b) {
+    if (a.size() != b.size()) return false;
+    volatile unsigned char result = 0;
+    for (size_t i = 0; i < a.size(); i++) {
+        result |= (unsigned char)a[i] ^ (unsigned char)b[i];
+    }
+    return result == 0;
+}
+
+} // namespace auth

+ 25 - 0
src/auth_crypto.h

@@ -0,0 +1,25 @@
+#pragma once
+/*
+ * auth_crypto.h - bcrypt C++ wrapper + secure random + timing-safe comparison
+ */
+
+#include <string>
+#include <cstdint>
+
+namespace auth {
+
+/* bcrypt password hashing */
+std::string bcrypt_hash(const std::string &password);
+std::string bcrypt_hash_with_salt(const std::string &password, const std::string &salt);
+bool bcrypt_verify(const std::string &password, const std::string &hash);
+
+/* Generate random bytes, returned as hex string */
+std::string random_hex(int byte_count);
+
+/* Generate random bytes, returned as raw string */
+std::string random_bytes(int byte_count);
+
+/* Timing-safe string comparison (constant-time) */
+bool secure_compare(const std::string &a, const std::string &b);
+
+} // namespace auth

+ 407 - 0
src/auth_db.cpp

@@ -0,0 +1,407 @@
+/*
+ * auth_db.cpp - SQLite database layer for authentication
+ */
+
+#include "auth_db.h"
+#include <sqlite3.h>
+#include <cstring>
+#include <cstdio>
+#include <mutex>
+
+namespace auth {
+
+static std::mutex db_mutex;
+
+AuthDB::AuthDB() {}
+
+AuthDB::~AuthDB() {
+    close();
+}
+
+bool AuthDB::exec(const std::string &sql) {
+    if (!db_) return false;
+    char *err = nullptr;
+    int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err);
+    if (rc != SQLITE_OK) {
+        fprintf(stderr, "[auth_db] SQL error: %s (sql: %.100s...)\n",
+                err ? err : "unknown", sql.c_str());
+        sqlite3_free(err);
+        return false;
+    }
+    return true;
+}
+
+bool AuthDB::init(const std::string &db_path) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+
+    int rc = sqlite3_open(db_path.c_str(), &db_);
+    if (rc != SQLITE_OK) {
+        fprintf(stderr, "[auth_db] Cannot open database: %s\n", db_path.c_str());
+        return false;
+    }
+
+    // Set WAL mode for better concurrent performance
+    exec("PRAGMA journal_mode=WAL");
+    exec("PRAGMA busy_timeout=5000");
+
+    // Create tables
+    const char *schema = R"SQL(
+        CREATE TABLE IF NOT EXISTS users (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            username TEXT UNIQUE NOT NULL,
+            password_hash TEXT NOT NULL,
+            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 IF NOT EXISTS sessions (
+            session_id TEXT PRIMARY KEY,
+            csrf_token TEXT NOT NULL,
+            user_id INTEGER NOT NULL,
+            username TEXT NOT NULL,
+            remote_ip TEXT DEFAULT '',
+            created_at INTEGER NOT NULL,
+            last_active INTEGER NOT NULL,
+            expires_at INTEGER NOT NULL,
+            remember INTEGER DEFAULT 0,
+            FOREIGN KEY (user_id) REFERENCES users(id)
+        );
+
+        CREATE TABLE IF NOT EXISTS audit_log (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            timestamp INTEGER NOT NULL,
+            event_type TEXT NOT NULL,
+            username TEXT DEFAULT '',
+            remote_ip TEXT DEFAULT '',
+            details TEXT DEFAULT '',
+            success INTEGER DEFAULT 0
+        );
+
+        CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
+        CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
+        CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
+        CREATE INDEX IF NOT EXISTS idx_audit_event ON audit_log(event_type);
+    )SQL";
+
+    if (!exec(schema)) {
+        fprintf(stderr, "[auth_db] Failed to create schema\n");
+        return false;
+    }
+
+    // Create default admin user if no users exist
+    sqlite3_stmt *stmt;
+    rc = sqlite3_prepare_v2(db_, "SELECT COUNT(*) FROM users", -1, &stmt, nullptr);
+    if (rc == SQLITE_OK) {
+        if (sqlite3_step(stmt) == SQLITE_ROW) {
+            int count = sqlite3_column_int(stmt, 0);
+            sqlite3_finalize(stmt);
+
+            if (count == 0) {
+                // Default admin will be created by the caller
+                // (they need to hash the password first)
+                printf("[auth_db] No users found, admin setup needed\n");
+            }
+        } else {
+            sqlite3_finalize(stmt);
+        }
+    }
+
+    printf("[auth_db] Database initialized: %s\n", db_path.c_str());
+    return true;
+}
+
+void AuthDB::close() {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (db_) {
+        sqlite3_close(db_);
+        db_ = nullptr;
+    }
+}
+
+bool AuthDB::create_user(const std::string &username, const std::string &password_hash,
+                          int role) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "INSERT INTO users (username, password_hash, role, enabled, "
+                      "failed_attempts, locked_until, created_at, updated_at) "
+                      "VALUES (?, ?, ?, 1, 0, 0, ?, ?)";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    time_t now = time(nullptr);
+    sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 2, password_hash.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int(stmt, 3, role);
+    sqlite3_bind_int64(stmt, 4, now);
+    sqlite3_bind_int64(stmt, 5, now);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::get_user_by_name(const std::string &username, User &user) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "SELECT id, username, password_hash, role, enabled, "
+                      "failed_attempts, locked_until, created_at, updated_at "
+                      "FROM users WHERE username = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT);
+
+    bool found = false;
+    if (sqlite3_step(stmt) == SQLITE_ROW) {
+        user.id = sqlite3_column_int(stmt, 0);
+        user.username = (const char*)sqlite3_column_text(stmt, 1);
+        user.password_hash = (const char*)sqlite3_column_text(stmt, 2);
+        user.role = sqlite3_column_int(stmt, 3);
+        user.enabled = sqlite3_column_int(stmt, 4);
+        user.failed_attempts = sqlite3_column_int(stmt, 5);
+        user.locked_until = sqlite3_column_int64(stmt, 6);
+        user.created_at = sqlite3_column_int64(stmt, 7);
+        user.updated_at = sqlite3_column_int64(stmt, 8);
+        found = true;
+    }
+
+    sqlite3_finalize(stmt);
+    return found;
+}
+
+bool AuthDB::update_user_password(int user_id, const std::string &new_hash) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "UPDATE users SET password_hash = ?, updated_at = ?, "
+                      "failed_attempts = 0, locked_until = 0 WHERE id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_text(stmt, 1, new_hash.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int64(stmt, 2, time(nullptr));
+    sqlite3_bind_int(stmt, 3, user_id);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::update_failed_attempts(int user_id, int count, time_t locked_until) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "UPDATE users SET failed_attempts = ?, locked_until = ?, "
+                      "updated_at = ? WHERE id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_int(stmt, 1, count);
+    sqlite3_bind_int64(stmt, 2, locked_until);
+    sqlite3_bind_int64(stmt, 3, time(nullptr));
+    sqlite3_bind_int(stmt, 4, user_id);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::reset_failed_attempts(int user_id) {
+    return update_failed_attempts(user_id, 0, 0);
+}
+
+bool AuthDB::create_session(const Session &sess) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "INSERT OR REPLACE INTO sessions "
+                      "(session_id, csrf_token, user_id, username, remote_ip, "
+                      "created_at, last_active, expires_at, remember) "
+                      "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_text(stmt, 1, sess.session_id.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 2, sess.csrf_token.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int(stmt, 3, sess.user_id);
+    sqlite3_bind_text(stmt, 4, sess.username.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 5, sess.remote_ip.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int64(stmt, 6, sess.created_at);
+    sqlite3_bind_int64(stmt, 7, sess.last_active);
+    sqlite3_bind_int64(stmt, 8, sess.expires_at);
+    sqlite3_bind_int(stmt, 9, sess.remember);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::get_session(const std::string &session_id, Session &sess) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "SELECT session_id, csrf_token, user_id, username, remote_ip, "
+                      "created_at, last_active, expires_at, remember "
+                      "FROM sessions WHERE session_id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_text(stmt, 1, session_id.c_str(), -1, SQLITE_TRANSIENT);
+
+    bool found = false;
+    if (sqlite3_step(stmt) == SQLITE_ROW) {
+        sess.session_id = (const char*)sqlite3_column_text(stmt, 0);
+        sess.csrf_token = (const char*)sqlite3_column_text(stmt, 1);
+        sess.user_id = sqlite3_column_int(stmt, 2);
+        sess.username = (const char*)sqlite3_column_text(stmt, 3);
+        sess.remote_ip = (const char*)sqlite3_column_text(stmt, 4);
+        sess.created_at = sqlite3_column_int64(stmt, 5);
+        sess.last_active = sqlite3_column_int64(stmt, 6);
+        sess.expires_at = sqlite3_column_int64(stmt, 7);
+        sess.remember = sqlite3_column_int(stmt, 8);
+        found = true;
+    }
+
+    sqlite3_finalize(stmt);
+    return found;
+}
+
+bool AuthDB::update_session_activity(const std::string &session_id, time_t now) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "UPDATE sessions SET last_active = ? WHERE session_id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_int64(stmt, 1, now);
+    sqlite3_bind_text(stmt, 2, session_id.c_str(), -1, SQLITE_TRANSIENT);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::delete_session(const std::string &session_id) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "DELETE FROM sessions WHERE session_id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_text(stmt, 1, session_id.c_str(), -1, SQLITE_TRANSIENT);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::delete_user_sessions(int user_id) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "DELETE FROM sessions WHERE user_id = ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_int(stmt, 1, user_id);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+bool AuthDB::cleanup_expired_sessions(time_t now) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+    char sql[128];
+    snprintf(sql, sizeof(sql), "DELETE FROM sessions WHERE expires_at < %ld", (long)now);
+    return exec(sql);
+}
+
+bool AuthDB::add_audit_log(const AuditEntry &entry) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return false;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "INSERT INTO audit_log (timestamp, event_type, username, "
+                      "remote_ip, details, success) VALUES (?, ?, ?, ?, ?, ?)";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return false;
+
+    sqlite3_bind_int64(stmt, 1, entry.timestamp);
+    sqlite3_bind_text(stmt, 2, entry.event_type.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 3, entry.username.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 4, entry.remote_ip.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 5, entry.details.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int(stmt, 6, entry.success);
+
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return rc == SQLITE_DONE;
+}
+
+std::vector<AuditEntry> AuthDB::get_audit_logs(int limit, int offset) {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    std::vector<AuditEntry> result;
+    if (!db_) return result;
+
+    sqlite3_stmt *stmt;
+    const char *sql = "SELECT id, timestamp, event_type, username, remote_ip, "
+                      "details, success FROM audit_log ORDER BY timestamp DESC "
+                      "LIMIT ? OFFSET ?";
+    int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return result;
+
+    sqlite3_bind_int(stmt, 1, limit);
+    sqlite3_bind_int(stmt, 2, offset);
+
+    while (sqlite3_step(stmt) == SQLITE_ROW) {
+        AuditEntry e;
+        e.id = sqlite3_column_int(stmt, 0);
+        e.timestamp = sqlite3_column_int64(stmt, 1);
+        e.event_type = (const char*)sqlite3_column_text(stmt, 2);
+        e.username = (const char*)sqlite3_column_text(stmt, 3);
+        e.remote_ip = (const char*)sqlite3_column_text(stmt, 4);
+        e.details = (const char*)sqlite3_column_text(stmt, 5);
+        e.success = sqlite3_column_int(stmt, 6);
+        result.push_back(e);
+    }
+
+    sqlite3_finalize(stmt);
+    return result;
+}
+
+int AuthDB::get_audit_log_count() {
+    std::lock_guard<std::mutex> lock(db_mutex);
+    if (!db_) return 0;
+
+    sqlite3_stmt *stmt;
+    int rc = sqlite3_prepare_v2(db_, "SELECT COUNT(*) FROM audit_log", -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) return 0;
+
+    int count = 0;
+    if (sqlite3_step(stmt) == SQLITE_ROW) {
+        count = sqlite3_column_int(stmt, 0);
+    }
+    sqlite3_finalize(stmt);
+    return count;
+}
+
+} // namespace auth

+ 92 - 0
src/auth_db.h

@@ -0,0 +1,92 @@
+#pragma once
+/*
+ * auth_db.h - SQLite database layer for authentication
+ * Tables: users, sessions, audit_log
+ */
+
+#include <string>
+#include <vector>
+#include <cstdint>
+#include <ctime>
+
+struct sqlite3;
+
+namespace auth {
+
+/* User record */
+struct User {
+    int id = 0;
+    std::string username;
+    std::string password_hash;  // bcrypt hash
+    int role = 0;               // 0=user, 1=admin, 2=superadmin
+    int enabled = 1;
+    int failed_attempts = 0;
+    time_t locked_until = 0;
+    time_t created_at = 0;
+    time_t updated_at = 0;
+};
+
+/* Session record */
+struct Session {
+    std::string session_id;     // 64-char hex
+    std::string csrf_token;     // 64-char hex
+    int user_id = 0;
+    std::string username;
+    std::string remote_ip;
+    time_t created_at = 0;
+    time_t last_active = 0;
+    time_t expires_at = 0;      // absolute timeout
+    int remember = 0;           // 1 = extended session (7 days)
+    int user_role = 0;          // fix24 v28: 0=user, 1=admin, 2=superadmin
+};
+
+/* Audit log entry */
+struct AuditEntry {
+    int id = 0;
+    time_t timestamp = 0;
+    std::string event_type;     // login, logout, failed_login, session_expired, etc.
+    std::string username;
+    std::string remote_ip;
+    std::string details;
+    int success = 0;
+};
+
+class AuthDB {
+public:
+    AuthDB();
+    ~AuthDB();
+
+    /* Initialize database (create tables if needed) */
+    bool init(const std::string &db_path);
+    void close();
+
+    /* User management */
+    bool create_user(const std::string &username, const std::string &password_hash,
+                     int role = 0);
+    bool get_user_by_name(const std::string &username, User &user);
+    bool update_user_password(int user_id, const std::string &new_hash);
+    bool update_failed_attempts(int user_id, int count, time_t locked_until);
+    bool reset_failed_attempts(int user_id);
+
+    /* Session management */
+    bool create_session(const Session &sess);
+    bool get_session(const std::string &session_id, Session &sess);
+    bool update_session_activity(const std::string &session_id, time_t now);
+    bool delete_session(const std::string &session_id);
+    bool delete_user_sessions(int user_id);
+    bool cleanup_expired_sessions(time_t now);
+
+    /* Audit log */
+    bool add_audit_log(const AuditEntry &entry);
+    std::vector<AuditEntry> get_audit_logs(int limit = 100, int offset = 0);
+    int get_audit_log_count();
+
+    /* Get SQLite handle for advanced queries */
+    sqlite3* handle() { return db_; }
+
+private:
+    sqlite3 *db_ = nullptr;
+    bool exec(const std::string &sql);
+};
+
+} // namespace auth

+ 265 - 0
src/auth_middleware.cpp

@@ -0,0 +1,265 @@
+/*
+ * auth_middleware.cpp - Authentication middleware implementation
+ */
+
+#include "auth_middleware.h"
+#include <cstdio>
+#include <cstring>
+#include <sstream>
+
+namespace auth {
+
+AuthMiddleware::AuthMiddleware() {}
+
+void AuthMiddleware::init(SessionManager *sessions, RateLimiter *limiter) {
+    sessions_ = sessions;
+    limiter_ = limiter;
+    printf("[auth] AuthMiddleware initialized\n");
+}
+
+const std::set<std::string>& AuthMiddleware::public_routes() {
+    static const std::set<std::string> routes = {
+        // fix24 v27: 首页和监控数据API均需登录(从公开路由移除)
+        // "/" — 首页需要登录
+        // "/api/monitor", "/api/messages", "/api/connections" — 首页数据API需要登录
+        "/login", "/api/auth/login", "/api/auth/logout", "/api/auth/status",
+        // Video/photo streams (keep public for external monitoring)
+        "/video", "/weight", "/photo",
+        "/api/weight/records",
+        // ROI endpoints (read)
+        "/api/roi/snapshot", "/api/roi/config",
+        // Streaming
+        "/api/stream", "/api/mjpeg",
+        // fix24 v24: 管理页面和API均需登录认证
+        // /system, /frpc, /ssh-keys, /logs 及其 API 均需登录后访问
+        // Audit log (read, requires admin but checked at API level)
+        "/api/auth/audit",
+        // Static assets
+        "/favicon.ico",
+    };
+    return routes;
+}
+
+bool AuthMiddleware::is_public_route(const std::string &path) {
+    // Exact match
+    if (public_routes().count(path)) return true;
+
+    // Prefix match for static assets
+    if (path.rfind("/assets/", 0) == 0) return true;
+    if (path.rfind("/static/", 0) == 0) return true;
+
+    return false;
+}
+
+std::string AuthMiddleware::extract_cookie(const httplib::Request &req,
+                                            const std::string &name) {
+    auto it = req.headers.find("Cookie");
+    if (it == req.headers.end()) return "";
+
+    std::string cookies = it->second;
+    std::string search = name + "=";
+    size_t pos = cookies.find(search);
+    if (pos == std::string::npos) return "";
+
+    pos += search.length();
+    size_t end = cookies.find(';', pos);
+    if (end == std::string::npos) end = cookies.length();
+
+    return cookies.substr(pos, end - pos);
+}
+
+std::string AuthMiddleware::get_client_ip(const httplib::Request &req) {
+    // Check X-Forwarded-For (for reverse proxy)
+    auto it = req.headers.find("X-Forwarded-For");
+    if (it != req.headers.end() && !it->second.empty()) {
+        // Take first IP in the list
+        size_t comma = it->second.find(',');
+        if (comma != std::string::npos)
+            return it->second.substr(0, comma);
+        return it->second;
+    }
+
+    // Check X-Real-IP
+    it = req.headers.find("X-Real-IP");
+    if (it != req.headers.end() && !it->second.empty()) {
+        return it->second;
+    }
+
+    // Fall back to remote address
+    return req.remote_addr;
+}
+
+std::string AuthMiddleware::check_auth(const httplib::Request &req, Session &sess) {
+    std::string session_id = extract_cookie(req, "session_id");
+    if (session_id.empty()) return "";
+
+    if (!sessions_) return "";
+
+    if (sessions_->validate_session(session_id, sess)) {
+        return session_id;
+    }
+
+    return "";
+}
+
+httplib::Server::HandlerWithResponse AuthMiddleware::get_pre_routing_handler() {
+    return [this](const httplib::Request &req, httplib::Response &res)
+        -> httplib::Server::HandlerResponse {
+        // Skip auth for public routes
+        if (is_public_route(req.path)) {
+            return httplib::Server::HandlerResponse::Unhandled;  // 继续路由
+        }
+
+        // Check authentication
+        Session sess;
+        std::string session_id = check_auth(req, sess);
+
+        if (session_id.empty()) {
+            // Check if this is an API request
+            bool is_api = (req.path.rfind("/api/", 0) == 0);
+
+            if (is_api) {
+                res.status = 401;
+                res.set_header("Content-Type", "application/json");
+                res.body = "{\"error\":\"Unauthorized\",\"message\":\"Please login\"}";
+            } else {
+                // Redirect to login page
+                res.status = 302;
+                res.set_header("Location", "/login?redirect=" + req.path);
+            }
+            return httplib::Server::HandlerResponse::Handled;  // 已处理,中断路由
+        }
+
+        // CSRF check for state-changing methods (POST/PUT/DELETE)
+        if (req.method == "POST" || req.method == "PUT" || req.method == "DELETE") {
+            // Get CSRF token from header or body
+            std::string csrf_token;
+            auto it = req.headers.find("X-CSRF-Token");
+            if (it != req.headers.end()) {
+                csrf_token = it->second;
+            } else {
+                // Check form body / query params
+                csrf_token = req.get_param_value("csrf_token");
+            }
+
+            std::string expected_csrf = sessions_->get_csrf_token(session_id);
+            if (!expected_csrf.empty() && csrf_token != expected_csrf) {
+                res.status = 403;
+                res.set_header("Content-Type", "application/json");
+                res.body = "{\"error\":\"Forbidden\",\"message\":\"CSRF token invalid\"}";
+                fprintf(stderr, "[auth] CSRF check failed for %s from %s\n",
+                        req.path.c_str(), get_client_ip(req).c_str());
+                return httplib::Server::HandlerResponse::Handled;  // 已处理,中断路由
+            }
+        }
+
+        // Role-based access control (fix24 v28)
+        // role=0: 普通用户 — 可访问首页/视频/称重/锁定管理
+        // role=1: 管理员 — 可访问首页/视频/称重/锁定管理/配置
+        // role=2: 超级管理员 — 所有页面(含帮助手册)
+        int user_role = sess.user_role;
+
+        // 超级管理员专属 (role >= 2)
+        if (user_role < 2) {
+            static const std::set<std::string> superadmin_exact = {
+                "/system", "/frpc", "/ssh-keys", "/logs", "/help",
+            };
+            static const char* superadmin_prefixes[] = {
+                "/api/frpc/", "/api/ssh/", "/api/logs/", "/api/monitor/",
+                nullptr
+            };
+            bool blocked = superadmin_exact.count(req.path) > 0;
+            if (!blocked) {
+                for (int i = 0; superadmin_prefixes[i]; i++) {
+                    if (req.path.rfind(superadmin_prefixes[i], 0) == 0) {
+                        blocked = true;
+                        break;
+                    }
+                }
+            }
+            if (blocked) {
+                bool is_api = (req.path.rfind("/api/", 0) == 0);
+                if (is_api) {
+                    res.status = 403;
+                    res.set_header("Content-Type", "application/json");
+                    res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires superadmin\"}";
+                } else {
+                    res.status = 302;
+                    res.set_header("Location", "/");
+                }
+                return httplib::Server::HandlerResponse::Handled;
+            }
+        }
+
+        // 管理员专属 (role >= 1)
+        if (user_role < 1) {
+            static const std::set<std::string> admin_exact = {
+                "/config",
+            };
+            static const char* admin_prefixes[] = {
+                "/api/config/", "/api/locks/",
+                nullptr
+            };
+            bool blocked = admin_exact.count(req.path) > 0;
+            if (!blocked) {
+                for (int i = 0; admin_prefixes[i]; i++) {
+                    if (req.path.rfind(admin_prefixes[i], 0) == 0) {
+                        blocked = true;
+                        break;
+                    }
+                }
+            }
+            if (blocked) {
+                bool is_api = (req.path.rfind("/api/", 0) == 0);
+                if (is_api) {
+                    res.status = 403;
+                    res.set_header("Content-Type", "application/json");
+                    res.body = "{\"error\":\"Forbidden\",\"message\":\"Requires admin\"}";
+                } else {
+                    res.status = 302;
+                    res.set_header("Location", "/");
+                }
+                return httplib::Server::HandlerResponse::Handled;
+            }
+        }
+
+        return httplib::Server::HandlerResponse::Unhandled;  // 认证通过,继续路由
+    };
+}
+
+httplib::Server::HandlerWithResponse AuthMiddleware::get_post_routing_handler() {
+    return [](const httplib::Request &req, httplib::Response &res) {
+        // Security headers (所有响应)
+        res.set_header("X-Content-Type-Options", "nosniff");
+        res.set_header("X-Frame-Options", "DENY");
+        res.set_header("X-XSS-Protection", "1; mode=block");
+        res.set_header("Referrer-Policy", "strict-origin-when-cross-origin");
+
+        // fix24 v35: 静态文件缓存优化
+        // 对 /assets/ 路径下的静态文件设置缓存,减少浏览器重复请求
+        bool is_static = (req.path.rfind("/assets/", 0) == 0 || 
+                          req.path.rfind("/static/", 0) == 0);
+        if (is_static) {
+            // 静态文件缓存1小时
+            res.set_header("Cache-Control", "public, max-age=3600");
+            res.set_header("Pragma", "");
+        } else {
+            // 动态页面和API不缓存
+            res.set_header("Cache-Control", "no-store, no-cache, must-revalidate");
+            res.set_header("Pragma", "no-cache");
+            // CSP只在HTML页面设置(减少其他资源类型的开销)
+            if (res.get_header_value("Content-Type").find("text/html") != std::string::npos) {
+                res.set_header("Content-Security-Policy",
+                    "default-src 'self'; "
+                    "script-src 'self' 'unsafe-inline'; "
+                    "style-src 'self' 'unsafe-inline'; "
+                    "img-src 'self' data:; "
+                    "connect-src 'self' ws: wss:;");
+            }
+        }
+
+        return httplib::Server::HandlerResponse::Handled;
+    };
+}
+
+} // namespace auth

+ 51 - 0
src/auth_middleware.h

@@ -0,0 +1,51 @@
+#pragma once
+/*
+ * auth_middleware.h - HTTP authentication middleware + security headers
+ * 
+ * Implements pre_routing_handler for httplib::Server
+ * - Checks session cookie for protected routes
+ * - Adds security response headers
+ * - Handles CSRF token validation for state-changing requests
+ */
+
+#include "auth_session.h"
+#include "auth_rate_limiter.h"
+#include <httplib.h>
+#include <string>
+#include <set>
+
+namespace auth {
+
+class AuthMiddleware {
+public:
+    AuthMiddleware();
+
+    void init(SessionManager *sessions, RateLimiter *limiter);
+
+    /* Get the pre-routing handler for httplib::Server
+     * Returns HandlerWithResponse: Handled=已处理(中断路由), Unhandled=继续路由 */
+    httplib::Server::HandlerWithResponse get_pre_routing_handler();
+
+    /* Get the post-routing handler (for adding security headers) */
+    httplib::Server::HandlerWithResponse get_post_routing_handler();
+
+    /* Check if a request is authenticated. Returns session_id if valid. */
+    std::string check_auth(const httplib::Request &req, Session &sess);
+
+    /* Check if a route is public (no auth needed) */
+    bool is_public_route(const std::string &path);
+
+    /* Get the client's real IP address */
+    static std::string get_client_ip(const httplib::Request &req);
+
+    /* Set of public routes that don't require authentication */
+    static const std::set<std::string>& public_routes();
+
+private:
+    SessionManager *sessions_ = nullptr;
+    RateLimiter *limiter_ = nullptr;
+
+    std::string extract_cookie(const httplib::Request &req, const std::string &name);
+};
+
+} // namespace auth

+ 95 - 0
src/auth_rate_limiter.h

@@ -0,0 +1,95 @@
+#pragma once
+/*
+ * auth_rate_limiter.h - IP-based sliding window rate limiter (header-only)
+ * 
+ * Limits login attempts per IP address.
+ * Default: 5 attempts per 5-minute window.
+ */
+
+#include <string>
+#include <unordered_map>
+#include <deque>
+#include <mutex>
+#include <ctime>
+#include <cstdint>
+
+namespace auth {
+
+class RateLimiter {
+public:
+    RateLimiter(int max_attempts = 5, int window_sec = 300)
+        : max_attempts_(max_attempts), window_sec_(window_sec) {}
+
+    /* Check if an IP is allowed to make a request. Returns true if allowed. */
+    bool check(const std::string &ip) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        time_t now = time(nullptr);
+        auto &attempts = attempts_[ip];
+
+        // Remove expired entries
+        while (!attempts.empty() && attempts.front() < now - window_sec_) {
+            attempts.pop_front();
+        }
+
+        return (int)attempts.size() < max_attempts_;
+    }
+
+    /* Record a failed attempt for an IP */
+    void record_failure(const std::string &ip) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        time_t now = time(nullptr);
+        auto &attempts = attempts_[ip];
+
+        // Remove expired entries
+        while (!attempts.empty() && attempts.front() < now - window_sec_) {
+            attempts.pop_front();
+        }
+
+        attempts.push_back(now);
+    }
+
+    /* Clear all attempts for an IP (after successful login) */
+    void clear(const std::string &ip) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        attempts_.erase(ip);
+    }
+
+    /* Get remaining attempts for an IP */
+    int remaining(const std::string &ip) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        time_t now = time(nullptr);
+        auto &attempts = attempts_[ip];
+
+        while (!attempts.empty() && attempts.front() < now - window_sec_) {
+            attempts.pop_front();
+        }
+
+        int rem = max_attempts_ - (int)attempts.size();
+        return rem > 0 ? rem : 0;
+    }
+
+    /* Periodic cleanup of stale entries */
+    void cleanup() {
+        std::lock_guard<std::mutex> lock(mutex_);
+        time_t now = time(nullptr);
+        for (auto it = attempts_.begin(); it != attempts_.end(); ) {
+            auto &q = it->second;
+            while (!q.empty() && q.front() < now - window_sec_) {
+                q.pop_front();
+            }
+            if (q.empty()) {
+                it = attempts_.erase(it);
+            } else {
+                ++it;
+            }
+        }
+    }
+
+private:
+    int max_attempts_;
+    int window_sec_;
+    std::unordered_map<std::string, std::deque<time_t>> attempts_;
+    std::mutex mutex_;
+};
+
+} // namespace auth

+ 187 - 0
src/auth_session.cpp

@@ -0,0 +1,187 @@
+/*
+ * auth_session.cpp - Session management implementation
+ * fix24 v35: 性能优化 — 消除每次请求的SQLite写操作
+ */
+
+#include "auth_session.h"
+#include "auth_crypto.h"
+#include <cstdio>
+
+namespace auth {
+
+SessionManager::SessionManager() {}
+
+void SessionManager::init(AuthDB *db, int idle_timeout_sec,
+                           int absolute_timeout_sec, int remember_timeout_sec) {
+    db_ = db;
+    idle_timeout_ = idle_timeout_sec;
+    absolute_timeout_ = absolute_timeout_sec;
+    remember_timeout_ = remember_timeout_sec;
+
+    printf("[session] SessionManager initialized (idle=%ds, absolute=%ds, remember=%ds)\n",
+           idle_timeout_, absolute_timeout_, remember_timeout_);
+}
+
+std::string SessionManager::create_session(int user_id, const std::string &username,
+                                            const std::string &remote_ip, bool remember,
+                                            int user_role) {
+    if (!db_) return "";
+
+    Session sess;
+    sess.session_id = random_hex(32);
+    sess.csrf_token = random_hex(32);
+    sess.user_id = user_id;
+    sess.username = username;
+    sess.remote_ip = remote_ip;
+    sess.user_role = user_role;
+
+    time_t now = time(nullptr);
+    sess.created_at = now;
+    sess.last_active = now;
+    sess.remember = remember ? 1 : 0;
+
+    if (remember) {
+        sess.expires_at = now + remember_timeout_;
+    } else {
+        sess.expires_at = now + absolute_timeout_;
+    }
+
+    // Save to SQLite (创建session时才写数据库)
+    if (!db_->create_session(sess)) {
+        fprintf(stderr, "[session] Failed to create session in DB\n");
+        return "";
+    }
+
+    // Cache in memory
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        cache_[sess.session_id] = sess;
+    }
+
+    printf("[session] Created session for user '%s' from %s (remember=%d)\n",
+           username.c_str(), remote_ip.c_str(), remember);
+    return sess.session_id;
+}
+
+bool SessionManager::is_expired(const Session &sess, time_t now) {
+    if (now >= sess.expires_at) return true;
+    if (now - sess.last_active > idle_timeout_) return true;
+    return false;
+}
+
+bool SessionManager::validate_session(const std::string &session_id, Session &sess) {
+    if (session_id.empty()) return false;
+
+    time_t now = time(nullptr);
+
+    // Check memory cache first
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        auto it = cache_.find(session_id);
+        if (it != cache_.end()) {
+            sess = it->second;
+            if (is_expired(sess, now)) {
+                cache_.erase(it);
+                // 异步删除过期session,不阻塞当前请求
+                if (db_) {
+                    std::string sid = session_id;
+                    // 直接删除(已在锁内,但db_有自己的mutex)
+                    db_->delete_session(sid);
+                }
+                printf("[session] Session expired for user '%s'\n", sess.username.c_str());
+                return false;
+            }
+            // fix24 v35: 只更新内存中的last_active,不写数据库!
+            // 这消除了每次HTTP请求的SQLite写操作,大幅提升性能
+            sess.last_active = now;
+            it->second.last_active = now;
+            // 不调用 db_->update_session_activity() — 性能关键优化
+            return true;
+        }
+    }
+
+    // Cache miss - check SQLite
+    if (!db_) return false;
+    if (!db_->get_session(session_id, sess)) return false;
+
+    // Load user role from users table
+    {
+        User u;
+        if (db_->get_user_by_name(sess.username, u)) {
+            sess.user_role = u.role;
+        }
+    }
+
+    if (is_expired(sess, now)) {
+        db_->delete_session(session_id);
+        printf("[session] Session expired (from DB) for user '%s'\n", sess.username.c_str());
+        return false;
+    }
+
+    // 缓存到内存,只在首次从DB加载时更新一次last_active
+    sess.last_active = now;
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        cache_[session_id] = sess;
+    }
+
+    return true;
+}
+
+void SessionManager::destroy_session(const std::string &session_id) {
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        cache_.erase(session_id);
+    }
+    if (db_) db_->delete_session(session_id);
+    printf("[session] Session destroyed\n");
+}
+
+void SessionManager::destroy_user_sessions(int user_id) {
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        for (auto it = cache_.begin(); it != cache_.end(); ) {
+            if (it->second.user_id == user_id) {
+                it = cache_.erase(it);
+            } else {
+                ++it;
+            }
+        }
+    }
+    if (db_) db_->delete_user_sessions(user_id);
+}
+
+std::string SessionManager::get_csrf_token(const std::string &session_id) {
+    std::lock_guard<std::mutex> lock(cache_mutex_);
+    auto it = cache_.find(session_id);
+    if (it != cache_.end()) return it->second.csrf_token;
+    return "";
+}
+
+void SessionManager::cleanup() {
+    time_t now = time(nullptr);
+
+    // Clean memory cache
+    {
+        std::lock_guard<std::mutex> lock(cache_mutex_);
+        for (auto it = cache_.begin(); it != cache_.end(); ) {
+            if (is_expired(it->second, now)) {
+                it = cache_.erase(it);
+            } else {
+                ++it;
+            }
+        }
+    }
+
+    // Clean SQLite
+    if (db_) {
+        db_->cleanup_expired_sessions(now);
+    }
+}
+
+int SessionManager::active_count() {
+    std::lock_guard<std::mutex> lock(cache_mutex_);
+    return (int)cache_.size();
+}
+
+} // namespace auth

+ 62 - 0
src/auth_session.h

@@ -0,0 +1,62 @@
+#pragma once
+/*
+ * auth_session.h - In-memory + SQLite dual-layer session management
+ * 
+ * Sessions are cached in memory for fast lookup, with SQLite as backing store.
+ * Idle timeout: 30 minutes (1800s)
+ * Absolute timeout: 8 hours (28800s), or 7 days if "remember me"
+ */
+
+#include "auth_db.h"
+#include <string>
+#include <unordered_map>
+#include <mutex>
+#include <ctime>
+
+namespace auth {
+
+class SessionManager {
+public:
+    SessionManager();
+
+    void init(AuthDB *db, int idle_timeout_sec = 1800,
+              int absolute_timeout_sec = 28800,
+              int remember_timeout_sec = 604800);
+
+    /* Create a new session for a user. Returns session_id. */
+    std::string create_session(int user_id, const std::string &username,
+                               const std::string &remote_ip, bool remember = false,
+                               int user_role = 0);
+
+    /* Validate a session. Returns true if valid. Updates last_active. */
+    bool validate_session(const std::string &session_id, Session &sess);
+
+    /* Destroy a session (logout) */
+    void destroy_session(const std::string &session_id);
+
+    /* Destroy all sessions for a user */
+    void destroy_user_sessions(int user_id);
+
+    /* Get CSRF token for a session */
+    std::string get_csrf_token(const std::string &session_id);
+
+    /* Periodic cleanup of expired sessions */
+    void cleanup();
+
+    /* Get active session count */
+    int active_count();
+
+private:
+    AuthDB *db_ = nullptr;
+    int idle_timeout_ = 1800;
+    int absolute_timeout_ = 28800;
+    int remember_timeout_ = 604800;
+
+    /* In-memory cache: session_id -> Session */
+    std::unordered_map<std::string, Session> cache_;
+    std::mutex cache_mutex_;
+
+    bool is_expired(const Session &sess, time_t now);
+};
+
+} // namespace auth

+ 122 - 0
src/bcrypt/bcrypt.c

@@ -0,0 +1,122 @@
+/*
+ * bcrypt.c - bcrypt password hashing using system libcrypt
+ * Falls back to custom implementation if system doesn't support bcrypt
+ */
+
+#define _GNU_SOURCE
+#include <crypt.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include "bcrypt.h"
+
+/* Generate random bytes from /dev/urandom */
+static int get_random_bytes(uint8_t *buf, size_t len) {
+    int fd = open("/dev/urandom", O_RDONLY);
+    if (fd < 0) return -1;
+    ssize_t n = read(fd, buf, len);
+    close(fd);
+    return (n == (ssize_t)len) ? 0 : -1;
+}
+
+/* bcrypt base64 alphabet for salt encoding */
+static const char bcrypt_base64[] =
+    "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+/* Encode bytes to bcrypt base64 */
+static void encode_bcrypt_salt(char *dst, const uint8_t *src, size_t len) {
+    while (len >= 3) {
+        uint32_t c = ((uint32_t)src[0] << 16) |
+                     ((uint32_t)src[1] << 8) |
+                     (uint32_t)src[2];
+        *dst++ = bcrypt_base64[(c >> 18) & 0x3f];
+        *dst++ = bcrypt_base64[(c >> 12) & 0x3f];
+        *dst++ = bcrypt_base64[(c >>  6) & 0x3f];
+        *dst++ = bcrypt_base64[ c        & 0x3f];
+        src += 3;
+        len -= 3;
+    }
+    if (len > 0) {
+        uint32_t c = (uint32_t)src[0] << 16;
+        if (len == 2) c |= (uint32_t)src[1] << 8;
+        *dst++ = bcrypt_base64[(c >> 18) & 0x3f];
+        *dst++ = bcrypt_base64[(c >> 12) & 0x3f];
+        if (len > 1)
+            *dst++ = bcrypt_base64[(c >> 6) & 0x3f];
+    }
+    *dst = '\0';
+}
+
+int
+bcrypt_gensalt(int cost, char *output)
+{
+    uint8_t salt[16];
+    char salt_b64[24];
+
+    if (cost < 4 || cost > 31) return -1;
+
+    if (get_random_bytes(salt, sizeof(salt)) != 0) {
+        return -1;
+    }
+
+    encode_bcrypt_salt(salt_b64, salt, sizeof(salt));
+
+    if (cost < 10)
+        snprintf(output, 64, "$2b$0%d$%s", cost, salt_b64);
+    else
+        snprintf(output, 64, "$2b$%d$%s", cost, salt_b64);
+
+    return 0;
+}
+
+int
+bcrypt_hashpw(const char *password, const char *salt_str, char *output)
+{
+    char *result;
+
+    if (!password || !salt_str || !output) return -1;
+
+    /* Use system crypt_r (thread-safe) */
+    struct crypt_data cdata;
+    memset(&cdata, 0, sizeof(cdata));
+    result = crypt_r(password, salt_str, &cdata);
+
+    if (!result) return -1;
+
+    /* Check if the result is valid (not an error marker like *0 or *1) */
+    if (result[0] == '*' && (result[1] == '0' || result[1] == '1')) {
+        return -1;
+    }
+
+    strcpy(output, result);
+    return 0;
+}
+
+int
+bcrypt_verify(const char *password, const char *hash)
+{
+    char computed[128];
+    size_t len, i;
+    int result;
+
+    if (!password || !hash) return -1;
+
+    if (bcrypt_hashpw(password, hash, computed) != 0) {
+        return -1;
+    }
+
+    len = strlen(hash);
+    if (len != strlen(computed)) return 0;
+
+    /* Constant-time comparison */
+    result = 0;
+    for (i = 0; i < len; i++) {
+        result |= (unsigned char)(hash[i] ^ computed[i]);
+    }
+
+    return (result == 0) ? 1 : 0;
+}

+ 38 - 0
src/bcrypt/bcrypt.h

@@ -0,0 +1,38 @@
+/*
+ * bcrypt.h - bcrypt password hashing (using OpenSSL Blowfish)
+ */
+#ifndef BCRYPT_H
+#define BCRYPT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Hash a password using bcrypt.
+ * password: input password (will be truncated to 72 bytes)
+ * salt:     salt string in format "$2b$NN$22charsalt"
+ * output:   output buffer (must be >= 61 bytes)
+ * Returns 0 on success, -1 on error.
+ */
+int bcrypt_hashpw(const char *password, const char *salt, char *output);
+
+/*
+ * Generate a bcrypt salt string.
+ * cost:     log2 of rounds (4-31, typically 10-12)
+ * output:   output buffer (must be >= 30 bytes)
+ * Returns 0 on success, -1 on error.
+ */
+int bcrypt_gensalt(int cost, char *output);
+
+/*
+ * Verify a password against a bcrypt hash.
+ * Returns 1 if match, 0 if no match, -1 on error.
+ */
+int bcrypt_verify(const char *password, const char *hash);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BCRYPT_H */

+ 154 - 0
src/bcrypt/bf_sbox.h

@@ -0,0 +1,154 @@
+/*
+ * Auto-generated Blowfish S-box constants
+ * Derived from the hexadecimal digits of pi
+ * These are the standard OpenBSD Blowfish initialization constants
+ */
+#ifndef BF_SBOX_H
+#define BF_SBOX_H
+
+#include <stdint.h>
+
+static const uint32_t bf_initial_P[18] = {
+    0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
+    0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
+    0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
+    0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
+    0x9216d5d9, 0x8979fb1b,
+};
+
+static const uint32_t bf_initial_S[4 * 256] = {
+    /* S-box 0 */
+    0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
+    0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
+    0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
+    0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
+    0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
+    0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
+    0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
+    0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
+    0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
+    0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
+    0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
+    0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
+    0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
+    0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
+    0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
+    0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
+    0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
+    0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
+    0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
+    0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
+    0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
+    0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
+    0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
+    0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
+    0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
+    0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
+    0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
+    0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
+    0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
+    0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
+    0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
+    0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
+    /* S-box 1 */
+    0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
+    0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
+    0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
+    0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
+    0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
+    0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
+    0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
+    0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
+    0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
+    0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
+    0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
+    0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
+    0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
+    0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
+    0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
+    0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
+    0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
+    0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
+    0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
+    0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
+    0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
+    0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
+    0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
+    0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
+    0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
+    0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
+    0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
+    0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
+    0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
+    0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
+    0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
+    0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
+    /* S-box 2 */
+    0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
+    0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
+    0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
+    0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
+    0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
+    0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
+    0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
+    0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
+    0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
+    0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
+    0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
+    0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
+    0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
+    0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
+    0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
+    0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
+    0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
+    0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
+    0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
+    0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
+    0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
+    0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
+    0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
+    0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
+    0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
+    0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
+    0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
+    0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
+    0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
+    0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
+    0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
+    0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
+    /* S-box 3 */
+    0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
+    0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
+    0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
+    0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
+    0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
+    0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
+    0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
+    0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
+    0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
+    0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
+    0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
+    0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
+    0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
+    0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
+    0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
+    0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
+    0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
+    0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
+    0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
+    0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
+    0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
+    0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
+    0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
+    0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
+    0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
+    0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
+    0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
+    0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
+    0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
+    0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
+    0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
+    0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6,
+};
+
+#endif /* BF_SBOX_H */

+ 240 - 0
src/common.cpp

@@ -0,0 +1,240 @@
+/**
+ * common.cpp — 全局变量定义
+ * v43.2 模块化拆分 (编译修复版)
+ */
+#include "common.h"
+#define CPPHTTPLIB_OPENSSL_SUPPORT  // fix24-v12: 启用HTTPS/SSL支持
+#include "httplib.h"
+
+// 路径
+std::string pathComm = "";  // 运行时由main()根据可执行文件路径动态设置
+std::string model_path = "";  // 运行时由main()根据pathComm动态设置
+
+// 配置变量
+std::string project_name = "南汇支线7标";
+std::string point_number = "GD202400138";
+std::string throughway = "一号门";
+std::string rtsp_url_front_in = "rtsp://admin:123456@192.168.50.203/stream_main";
+std::string rtsp_url_front_out = "rtsp://admin:123456@192.168.50.204/stream_main";
+std::string rtsp_url_side_in = "rtsp://admin:123456qwe@192.168.50.205";
+std::string rtsp_url_side_out = "rtsp://admin:123456qwe@192.168.50.206";
+std::string app_api = "https://ets.lhsr.sh.gov.cn/shcws/api/cws-plstb/threeBill/";
+std::string app_key = "960b8b4f-240c-4109-9f72-988d2109f6a3";
+std::string app_secret = "YJjIRiTkxijv3kYi7QIZBDelQFlzeIquFQfp8IzBD35XOeB1yaym3Xzp5jgU9EV3";
+int DEBUG_LOG = 1;
+std::atomic<int> TestFlag{ 1 };
+int g_wType = 1;
+int g_time_window_min = 5;
+double g_PLATE_CONFIDENCE_THRESHOLD = 0.9845;
+double g_PLATE_LOG_THRESHOLD = 0.80;
+int g_max_photo_groups = 2;
+
+// v43新增全局变量
+bool g_alternating_merge_enabled = false;
+ROIConfig g_roi_in;
+ROIConfig g_roi_out;
+bool g_roi_debug_enabled = false;
+DecodeMode g_hw_decode_mode = DecodeMode::AUTO;
+std::string g_hw_decode_device = "/dev/dri/renderD128";
+std::string g_rtsp_transport = "tcp";
+bool g_suppress_emergency_alert = false;
+int g_in_out_interval_sec = 300;
+
+// fix24-v8: 共享摄像头模式(进站和出站共用同一个前向摄像头)
+bool g_shared_capture_mode = false;
+
+// fix24-v14: 本地时区偏移(秒),用于SQLite中create_time字符串转Unix时间戳的校正
+// SQLite的strftime('%s', datetime_string)将输入视为UTC,但create_time存储的是本地时间
+int g_tz_offset_seconds = 0;
+
+// fix18: config.ini路径
+std::string g_config_ini_path;
+
+// fix19: 文件清理配置(照片容量阈值MB + 日志保留天数)
+long long g_photo_max_capacity_mb = 1024;  // 默认1GB
+int g_log_retention_days = 30;             // 默认30天
+
+// fix18: 称重系统默认配置(key与config.ini [weight]节点匹配)
+bool g_weight_enabled = false;
+double g_weight_threshold_in = 500.0;
+double g_weight_threshold_out = 500.0;
+double g_weight_default_entry = 500.0;
+double g_weight_default_exit = 2000.0;
+int g_weight_detection_time = 20;
+int g_weight_tcp_connect_timeout = 15;
+std::string g_weight_server_ip = "192.168.0.171";
+int g_weight_server_port = 8887;
+double g_weight_max_kg = 60000.0;
+double g_weight_min_kg = 100.0;
+int g_weight_stable_samples = 3;
+double g_weight_stable_threshold = 500.0;
+int g_weight_candidate_count = 5;
+int g_weight_max_retries = 3;
+int g_weight_retry_delay_ms = 2000;
+
+// fix18: MQTT默认配置
+bool g_mqtt_enabled = false;
+std::string g_mqtt_host;
+int g_mqtt_port = 1883;
+std::string g_mqtt_username;
+std::string g_mqtt_password;
+std::string g_mqtt_topic = "plate/recognition";
+std::string g_mqtt_client_id = "PlateRecApp";
+// MQTT消息附加字段
+std::string g_plate_color = "蓝色";
+std::string g_vehicle_type = "货车";
+
+// 交替锁定
+std::map<std::string, PlateStationState> g_plate_station_cache;
+std::mutex g_station_cache_mtx;
+const int STATION_CLEANUP_INTERVAL_SEC = 60;
+// ✅ fix16: 移除OUT_NO_RECORD_TIMEOUT_SEC和g_out_no_record_first_seen
+// 出站无进站记录永远拦截,不再需要超时放行机制
+
+// 帧级别去重
+std::unordered_map<std::string, time_t> g_in_plate_last_processed;
+std::unordered_map<std::string, time_t> g_out_plate_last_processed;
+std::mutex g_in_plate_last_processed_mtx;
+std::mutex g_out_plate_last_processed_mtx;
+
+// 飞书
+std::string g_feishu_app_id;
+std::string g_feishu_app_secret;
+std::string g_feishu_chat_id;
+long g_feishu_timeout_second = 10L;
+
+// 联单缓存
+std::unordered_map<std::string, InBillCacheEntry> g_in_bill_cache;
+std::mutex g_in_bill_cache_mtx;
+std::unordered_map<std::string, OutBillCacheEntry> g_out_bill_cache;
+std::mutex g_out_bill_cache_mtx;
+std::unordered_map<std::string, int> g_in_record;
+std::mutex g_in_record_mtx;
+std::unordered_map<std::string, int> g_out_record;
+std::mutex g_out_record_mtx;
+
+// 场景上传记录
+std::unordered_map<std::string, SceneUploadRecord> g_in_upload_records;
+std::unordered_map<std::string, SceneUploadRecord> g_out_upload_records;
+
+// 特殊车牌
+std::vector<std::string> g_special_plates;
+std::mutex g_special_plates_mtx;
+
+// 重试队列
+std::deque<std::shared_ptr<CarPlateInfo>> g_retry_queue;
+std::mutex g_retry_queue_mtx;
+
+// 数据库
+sqlite3* g_db = nullptr;
+std::mutex g_db_mtx;
+std::string g_db_path;
+
+// Token
+std::string g_tenant_token;
+std::atomic<time_t> g_token_expire_time{ 0 };
+std::mutex g_token_mtx;
+
+// 全局状态
+volatile std::sig_atomic_t g_running = 1;
+std::atomic<time_t> g_last_data_activity_time{ 0 };
+
+// PlateRecApp实例
+PlateRecApp plate_rec_app;
+uint32_t count = 0;
+std::atomic<int> g_active_detach_threads{0};
+
+// Web服务器
+std::unique_ptr<std::thread> g_web_server_thread;
+std::unique_ptr<httplib::Server> g_web_server;
+std::atomic<bool> g_web_server_running{ false };
+std::atomic<bool> g_web_server_initialized{ false };
+
+// ✅ fix24-v19: HTTP重定向服务器(HTTP->HTTPS自动跳转)
+std::unique_ptr<std::thread> g_http_redirect_thread;
+std::unique_ptr<httplib::Server> g_http_redirect_server;
+
+// fix24-v12: HTTPS/SSL配置
+bool g_ssl_enabled = false;
+std::string g_ssl_cert_path;
+std::string g_ssl_key_path;
+
+// ✅ fix24-v16: Web服务端口(从config.ini [server] ip字段解析,默认8080)
+int g_web_server_port = WEB_SERVER_PORT_DEFAULT;
+
+// ✅ fix24 功能一:登录认证系统全局对象
+auth::AuthDB *g_auth_db = nullptr;
+auth::SessionManager *g_session_mgr = nullptr;
+auth::RateLimiter *g_rate_limiter = nullptr;
+auth::AuthMiddleware *g_auth_middleware = nullptr;
+bool g_auth_enabled = true;  // 默认启用认证
+std::string g_auth_db_path;  // 认证数据库路径(空=自动从g_db_path推导)
+
+// ==================== 系统监控配置 (默认值) ====================
+int g_sys_monitor_interval = 2;
+int g_sys_temp_alert_threshold = 70;
+int g_sys_cpu_alert_threshold = 90;
+std::string g_sys_db_path = "data/system_metrics.db";
+int g_sys_history_retention_days = 7;
+int g_sys_aggregation_retention_days = 365;
+std::string g_sys_flush_time = "23:20";
+
+// ==================== frpc 管理配置 (默认值) ====================
+std::string g_frpc_config_path = "/opt/openAI/frp/frpc.toml";
+std::string g_frpc_backup_path = "/opt/openAI/frp/frpc.toml.bak";
+std::string g_frpc_protected_tunnels;
+int g_frpc_restart_check_timeout = 5;
+bool g_frpc_auto_rollback = true;
+bool g_frpc_web_enabled = true;
+std::string g_frpc_admin_addr = "127.0.0.1";
+int g_frpc_admin_port = 7400;
+std::string g_frpc_admin_user = "admin";
+std::string g_frpc_admin_password;
+
+// ==================== 终端/SSH 配置 (默认值) ====================
+int g_ttyd_port = 7681;
+std::string g_ttyd_credential;
+int g_ttyd_max_clients = 3;
+std::string g_ssh_keys_path = "/root/.ssh/authorized_keys";
+
+// ==================== 日志管理配置 (默认值) ====================
+int g_log_buffer_size_mb = 20;           // ✅ fix24-v36: 默认20MB(原500)
+std::string g_log_flush_time = "23:20";
+std::string g_log_file = "PlateRecApp.log";
+// g_log_retention_days 已在 fix19 中定义(第55行),此处不重复定义
+bool g_log_redirect_stdout = true;
+// ✅ fix24-v36: 内存日志缓冲新增配置
+bool g_log_enabled = true;               // 默认启用内存模式
+bool g_log_auto_flush_on_exit = true;    // 默认退出时刷盘
+
+// 监控指标
+ScenePlateStatus g_in_plate_status;
+ScenePlateStatus g_out_plate_status;
+MonitorMetrics g_metrics;
+
+// MonitorMetrics::print_report 实现
+void MonitorMetrics::print_report() {
+    std::cout << "========== 运行统计 ==========" << std::endl;
+    std::cout << " 运行时间: " << get_uptime_string() << std::endl;
+    std::cout << " 进站ROI裁剪: " << roi_in_crop_count << " 全帧: " << roi_in_fullframe_count << std::endl;
+    std::cout << " 出站ROI裁剪: " << roi_out_crop_count << " 全帧: " << roi_out_fullframe_count << std::endl;
+    std::cout << " Web重试: " << web_retry_count << " 成功: " << web_retry_success_count << std::endl;
+    std::cout << " 交替锁定: 进站" << station_lock_in_count << " 出站" << station_lock_out_count
+              << " 拦截: 进站" << station_in_block_count << " 出站" << station_out_block_count << std::endl;
+    std::cout << " 进站: " << in_station_count << " 出站: " << out_station_count << std::endl;
+    std::cout << " 联单创建: " << createbill_total << " 成功: " << createbill_success
+              << " 成功率: " << std::fixed << std::setprecision(1) << get_createbill_success_rate() * 100 << "%" << std::endl;
+    std::cout << " 缓存命中(进): " << in_cache_hits << " 未命中: " << in_cache_misses
+              << " 命中率: " << std::fixed << std::setprecision(1) << get_cache_hit_rate(true) * 100 << "%" << std::endl;
+    std::cout << " 缓存命中(出): " << out_cache_hits << " 未命中: " << out_cache_misses
+              << " 命中率: " << std::fixed << std::setprecision(1) << get_cache_hit_rate(false) * 100 << "%" << std::endl;
+    std::cout << " 平均上传时间: " << get_avg_upload_time() << "ms" << std::endl;
+    std::cout << " 推理耗时: 最近=" << get_last_inference_time() << "ms 平均=" << get_avg_inference_time() << "ms" << std::endl;
+    std::cout << " 错误: " << upload_errors << " 永久失败: " << permanent_failure_count << std::endl;
+    std::cout << " 飞书成功: " << feishu_success_count << " 跳过: " << feishu_skip_count << " 告警发送: " << alert_sent_count << std::endl;
+    std::cout << " DRM硬解: " << drm_decode_count << " 软解: " << soft_decode_count << std::endl;
+    std::cout << " DB重复跳过: " << db_duplicate_skip_count << " 插入错误: " << db_insert_error_count
+              << " 重试成功: " << db_insert_retry_success << std::endl;
+    std::cout << " 主循环阻塞: " << main_loop_block_count << " 最大阻塞: " << main_loop_max_block_ms << "ms" << std::endl;
+    std::cout << "=================" << std::endl;
+}

+ 784 - 0
src/common.h

@@ -0,0 +1,784 @@
+/**
+ * common.h — 公共类型定义、枚举、结构体、全局变量声明
+ * v43.2 模块化拆分 (编译修复版)
+ */
+#ifndef COMMON_H
+#define COMMON_H
+
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <fstream>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+#include <deque>
+#include <list>
+#include <stdexcept>
+#include <unistd.h>
+#include <chrono>
+#include <time.h>
+#include <functional>
+#include <cmath>
+#include <climits>
+#include <sys/stat.h>
+#include <cstdlib>
+#include <cstring>
+#include <unordered_set>
+#include <unordered_map>
+#include <memory>
+#include <atomic>
+#include <csignal>
+#include <algorithm>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <ctype.h>
+#include <vector>
+#include <string>
+#include <map>
+#include <utility>
+#include <ctime>
+#include <limits.h>
+
+#include <curl/curl.h>
+#include <cjson/cJSON.h>
+#include "opencv2/opencv.hpp"
+#include <sqlite3.h>
+#include <openssl/md5.h>
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/avutil.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_drm.h>
+#include <libavutil/imgutils.h>
+#include <libswscale/swscale.h>
+}
+
+// HyperLPR3 SDK
+#include "hyper_lpr_sdk.h"
+
+// httplib 前向声明(避免在所有模块中包含大头文件)
+namespace httplib { class Server; }
+
+// ==================== 全局常量定义 ====================
+// ✅ fix24-v16: Web服务端口从config.ini [server] ip字段解析,不再硬编码
+#define WEB_SERVER_PORT_DEFAULT 8080
+#define MAX_DB_RECORDS 10000
+#define INTERVAL_SECONDS (5 * 60)
+#define KEYVALLEN 100
+
+extern int g_max_photo_groups;
+const int MAX_UPLOAD_FAIL = 5;
+const int BLOCK_TIMEOUT_SEC = 300;
+const int PLATE_DUPLICATE_INTERVAL_MS = 2000;
+const int REQUIRED_SUCCESS_GROUPS = 2;
+const int PHOTO_RETRY_COUNT = 2;
+const int HTTP_STATUS_OK = 200;
+const int BUSINESS_ERROR_CODE = 257;
+const int DB_CLEANUP_THRESHOLD_MB = 500;
+const long long DB_CLEANUP_THRESHOLD_BYTES = (long long)DB_CLEANUP_THRESHOLD_MB * 1024 * 1024;
+const int TOKEN_EXPIRE_SECONDS = 7200;
+const int TOKEN_REFRESH_BUFFER_SECONDS = 300;
+const int MAX_BILL_COUNT_PER_WINDOW = 1;
+const int MAX_RETRY_COUNT = 5;
+const int MAX_UPLOAD_PER_CYCLE = 2;
+const int PLATE_CLEANUP_TIMEOUT_SEC = 60;
+const int DAILY_CLEANUP_HOUR = 23;
+const int DAILY_CLEANUP_MINUTE = 10;
+const int STATUS_CLEANUP_INTERVAL_SEC = 600;
+const int MAIN_LOOP_BLOCK_THRESHOLD_MS = 500;
+const int DB_INSERT_RETRY_COUNT = 1;
+const int DB_BUSY_TIMEOUT_MS = 100;
+const int DB_HEALTH_CHECK_INTERVAL_SEC = 60;
+
+// ==================== 每日文件清理配置(fix19修改) ====================
+const int FILE_CLEANUP_HOUR = 2;          // 凌晨2点执行文件清理
+const int FILE_CLEANUP_MINUTE = 0;
+// 照片清理:按容量阈值(MB),默认1GB,可通过config.ini配置
+extern long long g_photo_max_capacity_mb;
+// 日志清理:按天数,默认30天,可通过config.ini配置
+extern int g_log_retention_days;
+
+// ==================== 错误码定义 ====================
+enum class ErrorCode {
+    SUCCESS = 0,
+    NETWORK_ERROR = -1,
+    BUSINESS_ERROR = -2,
+    PARSE_ERROR = -3,
+    INVALID_PARAM = -4,
+    BLOCKED = -5,
+    QUOTA_EXCEEDED = -6,
+    DB_ERROR = -7,
+    PERMANENT_FAILURE = -8
+};
+
+// ==================== 照片上传结果结构体 ====================
+struct PhotoUploadResult {
+    bool lo_success;
+    bool hi_success;
+    int success_groups;
+    PhotoUploadResult() : lo_success(false), hi_success(false), success_groups(0) {}
+};
+
+// ==================== 核心:进站/出站完全独立的状态管理 ====================
+struct ScenePlateStatus {
+private:
+    std::unordered_set<std::string> blocked_plates;
+    std::unordered_map<std::string, int> fail_count;
+    std::unordered_map<std::string, time_t> block_time;
+    mutable std::mutex mtx;
+
+public:
+    bool is_blocked(const std::string& plate) {
+        std::lock_guard<std::mutex> lock(mtx);
+        auto it = blocked_plates.find(plate);
+        if (it == blocked_plates.end()) return false;
+        if (time(nullptr) >= block_time[plate]) {
+            blocked_plates.erase(it);
+            fail_count.erase(plate);
+            block_time.erase(plate);
+            return false;
+        }
+        return true;
+    }
+
+    void add_fail(const std::string& plate) {
+        std::lock_guard<std::mutex> lock(mtx);
+        fail_count[plate]++;
+        if (fail_count[plate] >= MAX_UPLOAD_FAIL) {
+            blocked_plates.insert(plate);
+            block_time[plate] = time(nullptr) + BLOCK_TIMEOUT_SEC;
+        }
+    }
+
+    void reset(const std::string& plate) {
+        std::lock_guard<std::mutex> lock(mtx);
+        blocked_plates.erase(plate);
+        fail_count.erase(plate);
+        block_time.erase(plate);
+    }
+
+    void cleanup_expired() {
+        std::lock_guard<std::mutex> lock(mtx);
+        time_t now = time(nullptr);
+        for (auto it = block_time.begin(); it != block_time.end(); ) {
+            if (now >= it->second) {
+                blocked_plates.erase(it->first);
+                fail_count.erase(it->first);
+                it = block_time.erase(it);
+            } else ++it;
+        }
+    }
+};
+
+extern ScenePlateStatus g_in_plate_status;
+extern ScenePlateStatus g_out_plate_status;
+
+// ==================== ROI配置结构体(v43新增) ====================
+struct ROIConfig {
+    int x = 0;
+    int y = 0;
+    int width = 0;
+    int height = 0;
+    bool enabled = false;
+
+    bool isFullFrame() const {
+        return x == 0 && y == 0 && width == 0 && height == 0;
+    }
+    cv::Rect getRect(int img_w, int img_h) const {
+        return cv::Rect(
+            std::max(0, std::min(x, img_w - 1)),
+            std::max(0, std::min(y, img_h - 1)),
+            std::min(width > 0 ? width : img_w, img_w - std::max(0, std::min(x, img_w - 1))),
+            std::min(height > 0 ? height : img_h, img_h - std::max(0, std::min(y, img_h - 1)))
+        );
+    }
+    std::string toString() const {
+        return "[" + std::to_string(x) + "," + std::to_string(y) + ","
+               + std::to_string(width) + "," + std::to_string(height) + "]"
+               + (enabled ? " ON" : " OFF");
+    }
+};
+
+// ==================== 解码模式枚举(v43新增) ====================
+enum class DecodeMode {
+    AUTO = 0,
+    SOFT = 1,
+    DRM = 2
+};
+
+// ==================== 进站/出站交替锁定机制 ====================
+enum class StationMode : int {
+    INBOUND_ALLOWED = 0,
+    OUTBOUND_ALLOWED = 1
+};
+
+struct PlateStationState {
+    std::string plate;
+    time_t last_in_time;
+    time_t last_out_time;
+    StationMode current_mode;
+    PlateStationState() : last_in_time(0), last_out_time(0),
+                          current_mode(StationMode::INBOUND_ALLOWED) {}
+};
+
+// ==================== 联单缓存结构 ====================
+struct InBillCacheEntry {
+    std::string tb_num;
+    time_t create_time;
+    int hit_count;
+    int photo_count;
+    bool bill_created;
+};
+
+struct OutBillCacheEntry {
+    std::string tb_num;
+    time_t create_time;
+    int hit_count;
+    int photo_count;
+    bool bill_created;
+};
+
+// ==================== 错误恢复策略 ====================
+enum class UploadError {
+    SUCCESS = 0,
+    NETWORK_TIMEOUT,
+    NETWORK_CONNECTION_FAILED,
+    SERVER_ERROR_4XX,
+    SERVER_ERROR_5XX,
+    AUTH_FAILED,
+    INVALID_RESPONSE,
+    IMAGE_TOO_LARGE,
+    UNKNOWN_ERROR
+};
+
+struct RecoveryStrategy {
+    int max_retries;
+    int retry_delay_ms;
+    bool should_retry;
+};
+
+// ==================== 场景上传记录(TIME_WINDOW持久化) ====================
+struct SceneUploadRecord {
+    int bill_count;
+    time_t last_time;
+    time_t trigger_time;
+    bool bill_created;
+    int photo_success_count;
+    bool feishu_sent;
+    int generation;
+    time_t create_time;
+    std::string tb_num;
+
+    SceneUploadRecord() : bill_count(0), last_time(0), trigger_time(0),
+        bill_created(false), photo_success_count(0), feishu_sent(false),
+        generation(0), create_time(time(NULL)) {}
+};
+
+// ==================== 监控指标 ====================
+struct MonitorMetrics {
+    std::atomic<int> createbill_total{ 0 };
+    std::atomic<int> createbill_success{ 0 };
+    std::atomic<int> createbill_business_error{ 0 };
+    std::atomic<int> createbill_quota_exceeded{ 0 };
+    std::deque<std::pair<time_t, int>> upload_time_records;
+    std::mutex upload_time_mtx;
+    std::atomic<int> time_window_hits{ 0 };
+    std::atomic<int> in_cache_hits{ 0 };
+    std::atomic<int> in_cache_misses{ 0 };
+    std::atomic<int> out_cache_hits{ 0 };
+    std::atomic<int> out_cache_misses{ 0 };
+    std::atomic<int> upload_errors{ 0 };
+    std::atomic<int> network_timeouts{ 0 };
+    std::atomic<int> server_errors{ 0 };
+    std::atomic<int> in_station_count{ 0 };
+    std::atomic<int> out_station_count{ 0 };
+    std::atomic<int> feishu_success_count{ 0 };
+    std::deque<time_t> five_min_upload_times;
+    std::mutex five_min_mtx;
+    time_t start_time{ 0 };
+    std::atomic<long long> main_loop_block_count{ 0 };
+    std::atomic<long long> main_loop_max_block_ms{ 0 };
+    std::atomic<int> photo_lo_success{ 0 };
+    std::atomic<int> photo_hi_success{ 0 };
+    std::atomic<int> photo_group_success{ 0 };
+    std::atomic<int> db_duplicate_skip_count{ 0 };
+    std::atomic<int> dir_create_error_count{ 0 };
+    std::atomic<int> db_insert_error_count{ 0 };
+    std::atomic<int> db_insert_retry_success{ 0 };
+    std::atomic<int> permanent_failure_count{ 0 };
+    std::atomic<int> alert_sent_count{ 0 };
+    std::atomic<int> db_health_check_count{ 0 };
+    // v42新增:交替锁定监控指标
+    std::atomic<int> station_lock_in_count{ 0 };
+    std::atomic<int> station_lock_out_count{ 0 };
+    std::atomic<int> station_in_block_count{ 0 };
+    std::atomic<int> station_out_block_count{ 0 };
+    std::atomic<int> station_timeout_cleanup_count{ 0 };
+    // v43新增:ROI监控指标
+    std::atomic<int> roi_in_crop_count{ 0 };
+    std::atomic<int> roi_out_crop_count{ 0 };
+    std::atomic<int> roi_in_fullframe_count{ 0 };
+    std::atomic<int> roi_out_fullframe_count{ 0 };
+    // v43新增:飞书/解码/Web监控指标
+    std::atomic<int> feishu_skip_count{ 0 };
+    std::atomic<int> drm_decode_count{ 0 };
+    std::atomic<int> soft_decode_count{ 0 };
+    std::atomic<int> web_retry_count{ 0 };
+    std::atomic<int> web_retry_success_count{ 0 };
+    // fix20新增:推理耗时追踪
+    std::deque<std::pair<time_t, int>> infer_time_records;
+    std::mutex infer_time_mtx;
+
+    void record_roi_crop(bool is_in) {
+        is_in ? roi_in_crop_count++ : roi_out_crop_count++;
+    }
+    void record_roi_fullframe(bool is_in) {
+        is_in ? roi_in_fullframe_count++ : roi_out_fullframe_count++;
+    }
+    void record_feishu_skip() { feishu_skip_count++; }
+    void record_drm_decode() { drm_decode_count++; }
+    void record_soft_decode() { soft_decode_count++; }
+    void record_web_retry(bool success) {
+        web_retry_count++;
+        if (success) web_retry_success_count++;
+    }
+
+    void record_station_lock(bool is_in) {
+        is_in ? station_lock_in_count++ : station_lock_out_count++;
+    }
+    void record_station_block(bool is_in) {
+        is_in ? station_in_block_count++ : station_out_block_count++;
+    }
+    void record_station_timeout_cleanup() {
+        station_timeout_cleanup_count++;
+    }
+
+    void record_upload_time(int duration_ms) {
+        std::lock_guard<std::mutex> lock(upload_time_mtx);
+        upload_time_records.push_back({ time(NULL), duration_ms });
+        if (upload_time_records.size() > 10) {
+            upload_time_records.pop_front();
+        }
+    }
+
+    void record_createbill(bool success, bool is_business_error = false, bool is_quota_exceeded = false) {
+        createbill_total++;
+        if (success) createbill_success++;
+        if (is_business_error) createbill_business_error++;
+        if (is_quota_exceeded) createbill_quota_exceeded++;
+    }
+
+    void record_cache_hit(bool is_in, bool hit) {
+        if (is_in) hit ? in_cache_hits++ : in_cache_misses++;
+        else hit ? out_cache_hits++ : out_cache_misses++;
+    }
+
+    void record_error(UploadError error) {
+        upload_errors++;
+        switch (error) {
+        case UploadError::NETWORK_TIMEOUT: network_timeouts++; break;
+        case UploadError::SERVER_ERROR_5XX: server_errors++; break;
+        default: break;
+        }
+    }
+
+    void record_station(bool is_in) {
+        is_in ? in_station_count++ : out_station_count++;
+    }
+
+    void record_feishu_success() { feishu_success_count++; }
+
+    void record_photo_upload(bool lo_success, bool hi_success, bool group_success) {
+        if (lo_success) photo_lo_success++;
+        if (hi_success) photo_hi_success++;
+        if (group_success) photo_group_success++;
+    }
+
+    void record_main_loop_block(long long duration_ms) {
+        main_loop_block_count++;
+        if (duration_ms > main_loop_max_block_ms.load()) {
+            main_loop_max_block_ms.store(duration_ms);
+        }
+        if (duration_ms > MAIN_LOOP_BLOCK_THRESHOLD_MS) {
+            std::cerr << "[告警] 主循环阻塞 " << duration_ms << "ms,超过阈值 "
+                      << MAIN_LOOP_BLOCK_THRESHOLD_MS << "ms" << std::endl;
+        }
+    }
+
+    void record_db_duplicate_skip() { db_duplicate_skip_count++; }
+    void record_dir_create_error() { dir_create_error_count++; }
+    void record_db_insert_error() { db_insert_error_count++; }
+    void record_db_insert_retry_success() { db_insert_retry_success++; }
+    void record_permanent_failure() { permanent_failure_count++; }
+    void record_alert_sent() { alert_sent_count++; }
+    void record_db_health_check() { db_health_check_count++; }
+
+    void record_inference_time(int duration_ms) {
+        std::lock_guard<std::mutex> lock(infer_time_mtx);
+        infer_time_records.push_back({ time(NULL), duration_ms });
+        if (infer_time_records.size() > 20) {
+            infer_time_records.pop_front();
+        }
+    }
+    int get_avg_inference_time() {
+        std::lock_guard<std::mutex> lock(infer_time_mtx);
+        if (infer_time_records.empty()) return 0;
+        int total = 0;
+        for (auto& p : infer_time_records) total += p.second;
+        return total / (int)infer_time_records.size();
+    }
+    int get_last_inference_time() {
+        std::lock_guard<std::mutex> lock(infer_time_mtx);
+        if (infer_time_records.empty()) return 0;
+        return infer_time_records.back().second;
+    }
+
+    int get_avg_upload_time() {
+        std::lock_guard<std::mutex> lock(upload_time_mtx);
+        if (upload_time_records.empty()) return 0;
+        int total = 0;
+        for (auto& p : upload_time_records) total += p.second;
+        return total / upload_time_records.size();
+    }
+    float get_createbill_success_rate() {
+        int total = createbill_total.load();
+        return total > 0 ? (float)createbill_success.load() / total : 0.0f;
+    }
+    float get_cache_hit_rate(bool is_in) {
+        if (is_in) {
+            int total = in_cache_hits.load() + in_cache_misses.load();
+            return total > 0 ? (float)in_cache_hits.load() / total : 0.0f;
+        } else {
+            int total = out_cache_hits.load() + out_cache_misses.load();
+            return total > 0 ? (float)out_cache_hits.load() / total : 0.0f;
+        }
+    }
+    std::string get_uptime_string() {
+        if (start_time == 0) return "0h00m00s";
+        auto secs = time(NULL) - start_time;
+        int h = secs / 3600; int m = (secs % 3600) / 60; int s = secs % 60;
+        std::ostringstream oss;
+        oss << h << "h" << std::setfill('0') << std::setw(2) << m << "m" << std::setw(2) << s << "s";
+        return oss.str();
+    }
+    void print_report();
+};
+
+extern MonitorMetrics g_metrics;
+
+// ==================== C风格枚举和结构体 ====================
+typedef enum {
+    BUILDING = 1,
+    DOCK = 2,
+    BACKFILL = 3
+} WTYPE;
+
+typedef enum {
+    STATION_IN = 1,
+    STATION_OUT = 2
+} DTYPE;
+
+typedef enum {
+    POSITION_LO = 1,
+    POSITION_HI = 2
+} GTYPE;
+
+typedef enum {
+    CAPTURE_NORMAL = 1,
+    CAPTURE_CAMERA_OFFLINE,
+    CAPTURE_STORAGE_ERROR,
+    CAPTURE_LENS_OBSTRUCTED,
+    CAPTURE_ANGLE_ABNORMAL
+} CaptureStatus;
+
+typedef struct {
+    WTYPE wtype;
+    DTYPE dtype;
+    GTYPE gtype;
+    CaptureStatus status;
+    time_t capture_time = 0;  // ✅ fix23: 抓拍时间戳,保存帧被捕获时的时间
+} CaptureInfo;
+
+typedef struct {
+    std::string tb_num;
+    float confidence = 0.0f;
+    std::string type;
+    std::string code;
+    cv::Mat image_front;
+    cv::Mat image_side;
+    int count = 0;
+    bool is_report = false;
+    std::string pic_path_front;
+    std::string pic_path_side;
+    int retry_count = 0;
+    std::unique_ptr<CaptureInfo> cap_info_copy;
+    time_t first_seen_time = 0;
+    int db_id = 0;
+    bool is_inbound = false;
+    bool is_outbound = false;
+    bool photo_saved = false;
+    time_t last_station_blocked_time = 0;  // ✅ fix11: 交替锁定拦截时间戳,防止被拦截后空转
+    bool blocked_by_station_lock = false;  // ✅ fix13: 区分BLOCKED来源(true=交替锁定, false=is_blocked)
+    std::atomic<bool> upload_in_progress{false};  // ✅ fix18-P0: 上传进行中标志,防止peek与try_lock竞态
+    time_t upload_complete_time = 0;  // ✅ fix24-v6: 上传完成时间戳,防止刚上传完就被peek重置导致重复保存
+    time_t db_capture_time = 0;  // ✅ fix24-v18: 照片保存时固定的capture_time,防止识别线程竞态覆盖导致DB时间不一致
+} CarPlateInfo;
+
+// ==================== RTSPCapture 前向声明 ====================
+class RTSPCapture;
+
+// ==================== PlateRecApp 结构体 ====================
+typedef struct {
+    P_HLPR_Context hlpr_ctx;
+    RTSPCapture* capture_front_in;
+    RTSPCapture* capture_front_out;
+    RTSPCapture* capture_side_in;
+    RTSPCapture* capture_side_out;
+    CaptureInfo front_info_in;
+    CaptureInfo front_info_out;
+    CaptureInfo side_info_in;
+    CaptureInfo side_info_out;
+    time_t timer_front_in;
+    time_t timer_front_out;
+    std::mutex map_mtx;
+    std::unordered_map<std::string, std::shared_ptr<CarPlateInfo>> map;
+} PlateRecApp;
+
+// ==================== 全局变量 extern 声明 ====================
+// 路径
+extern std::string pathComm;
+extern std::string model_path;
+
+// 配置变量
+extern std::string project_name;
+extern std::string point_number;
+extern std::string throughway;
+extern std::string rtsp_url_front_in;
+extern std::string rtsp_url_front_out;
+extern std::string rtsp_url_side_in;
+extern std::string rtsp_url_side_out;
+extern std::string app_api;
+extern std::string app_key;
+extern std::string app_secret;
+extern int DEBUG_LOG;
+extern std::atomic<int> TestFlag;
+extern int g_wType;
+extern int g_time_window_min;
+extern double g_PLATE_CONFIDENCE_THRESHOLD;
+extern double g_PLATE_LOG_THRESHOLD;
+
+// v43新增全局变量
+extern bool g_alternating_merge_enabled;
+extern ROIConfig g_roi_in;
+extern ROIConfig g_roi_out;
+extern bool g_roi_debug_enabled;
+extern DecodeMode g_hw_decode_mode;
+extern std::string g_hw_decode_device;
+extern std::string g_rtsp_transport;
+extern bool g_suppress_emergency_alert;
+extern int g_in_out_interval_sec;
+
+// fix24-v8: 共享摄像头模式(进站和出站共用同一个前向摄像头)
+// 启用后,根据交替锁定状态自动判断检测到的车牌是进站还是出站
+extern bool g_shared_capture_mode;
+
+// fix24-v14: 本地时区偏移(秒),用于SQLite中create_time字符串转Unix时间戳的校正
+extern int g_tz_offset_seconds;
+
+// fix18: config.ini路径(用于ROI写回)
+extern std::string g_config_ini_path;
+
+// fix18: 称重系统全局变量(config.ini key映射已在config.cpp中匹配)
+extern bool g_weight_enabled;
+extern double g_weight_threshold_in;
+extern double g_weight_threshold_out;
+extern double g_weight_default_entry;
+extern double g_weight_default_exit;
+extern int g_weight_detection_time;
+extern int g_weight_tcp_connect_timeout;
+extern std::string g_weight_server_ip;
+extern int g_weight_server_port;
+extern double g_weight_max_kg;
+extern double g_weight_min_kg;
+extern int g_weight_stable_samples;
+extern double g_weight_stable_threshold;
+extern int g_weight_candidate_count;
+extern int g_weight_max_retries;
+extern int g_weight_retry_delay_ms;
+
+// fix18: MQTT全局变量
+extern bool g_mqtt_enabled;
+extern std::string g_mqtt_host;
+extern int g_mqtt_port;
+extern std::string g_mqtt_username;
+extern std::string g_mqtt_password;
+extern std::string g_mqtt_topic;
+extern std::string g_mqtt_client_id;
+// MQTT消息附加字段(从[MQTT]节点读取)
+extern std::string g_plate_color;
+extern std::string g_vehicle_type;
+
+// 交替锁定
+extern std::map<std::string, PlateStationState> g_plate_station_cache;
+extern std::mutex g_station_cache_mtx;
+extern const int STATION_CLEANUP_INTERVAL_SEC;
+const int STATION_LOCK_EXPIRE_SEC = 7200;   // ✅ fix18: 交替锁定记录2小时过期,超时清零
+// ✅ fix16: 移除OUT_NO_RECORD_TIMEOUT_SEC和g_out_no_record_first_seen
+// 出站无进站记录永远拦截,不再需要超时放行机制
+
+// 帧级别去重
+extern std::unordered_map<std::string, time_t> g_in_plate_last_processed;
+extern std::unordered_map<std::string, time_t> g_out_plate_last_processed;
+extern std::mutex g_in_plate_last_processed_mtx;
+extern std::mutex g_out_plate_last_processed_mtx;
+
+// 飞书
+extern std::string g_feishu_app_id;
+extern std::string g_feishu_app_secret;
+extern std::string g_feishu_chat_id;
+extern long g_feishu_timeout_second;
+
+// 联单缓存
+extern std::unordered_map<std::string, InBillCacheEntry> g_in_bill_cache;
+extern std::mutex g_in_bill_cache_mtx;
+extern std::unordered_map<std::string, OutBillCacheEntry> g_out_bill_cache;
+extern std::mutex g_out_bill_cache_mtx;
+extern std::unordered_map<std::string, int> g_in_record;
+extern std::mutex g_in_record_mtx;
+extern std::unordered_map<std::string, int> g_out_record;
+extern std::mutex g_out_record_mtx;
+
+// 场景上传记录
+extern std::unordered_map<std::string, SceneUploadRecord> g_in_upload_records;
+extern std::unordered_map<std::string, SceneUploadRecord> g_out_upload_records;
+
+// 特殊车牌
+extern std::vector<std::string> g_special_plates;
+extern std::mutex g_special_plates_mtx;
+
+// 重试队列
+extern std::deque<std::shared_ptr<CarPlateInfo>> g_retry_queue;
+extern std::mutex g_retry_queue_mtx;
+
+// 数据库
+extern sqlite3* g_db;
+extern std::mutex g_db_mtx;
+extern std::string g_db_path;
+
+// Token
+extern std::string g_tenant_token;
+extern std::atomic<time_t> g_token_expire_time;
+extern std::mutex g_token_mtx;
+
+// 全局状态
+extern volatile std::sig_atomic_t g_running;
+extern std::atomic<time_t> g_last_data_activity_time;
+
+// PlateRecApp实例
+extern PlateRecApp plate_rec_app;
+extern uint32_t count;
+extern std::atomic<int> g_active_detach_threads;
+
+// Web服务器
+extern std::unique_ptr<std::thread> g_web_server_thread;
+extern std::unique_ptr<httplib::Server> g_web_server;
+extern std::atomic<bool> g_web_server_running;
+extern std::atomic<bool> g_web_server_initialized;
+
+// ✅ fix24-v19: HTTP重定向服务器(HTTP->HTTPS自动跳转)
+extern std::unique_ptr<std::thread> g_http_redirect_thread;
+extern std::unique_ptr<httplib::Server> g_http_redirect_server;
+
+// fix24-v12: HTTPS/SSL配置
+extern bool g_ssl_enabled;
+extern std::string g_ssl_cert_path;
+extern std::string g_ssl_key_path;
+
+// ✅ fix24-v16: Web服务端口(从config.ini [server] ip字段解析)
+extern int g_web_server_port;
+
+// ✅ fix24 功能一:登录认证系统全局对象
+// 必须在 httplib.h 被 include 之前定义 OpenSSL 宏(auth_middleware.h 会触发 httplib.h 包含)
+#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#endif
+#include "auth_db.h"
+#include "auth_session.h"
+#include "auth_rate_limiter.h"
+#include "auth_middleware.h"
+extern auth::AuthDB *g_auth_db;
+extern auth::SessionManager *g_session_mgr;
+extern auth::RateLimiter *g_rate_limiter;
+extern auth::AuthMiddleware *g_auth_middleware;
+extern bool g_auth_enabled;  // 是否启用认证(config.ini [auth] enabled)
+extern std::string g_auth_db_path;  // 认证数据库路径(config.ini [auth] db_path)
+
+// ==================== 系统监控配置 (config.ini [system]) ====================
+extern int g_sys_monitor_interval;       // 监控采样间隔(秒)
+extern int g_sys_temp_alert_threshold;   // 温度告警阈值(°C)
+extern int g_sys_cpu_alert_threshold;    // CPU告警阈值(%)
+extern std::string g_sys_db_path;        // 系统监控数据库路径
+extern int g_sys_history_retention_days; // 全量数据保留天数
+extern int g_sys_aggregation_retention_days; // 聚合数据保留天数
+extern std::string g_sys_flush_time;      // 每日刷盘时间(HH:MM,默认23:20)
+
+// ==================== frpc 管理配置 (config.ini [frpc]) ====================
+extern std::string g_frpc_config_path;       // frpc.toml 路径
+extern std::string g_frpc_backup_path;       // 备份路径
+extern std::string g_frpc_protected_tunnels; // 受保护隧道列表
+extern int g_frpc_restart_check_timeout;     // 重启后等待检测时间(秒)
+extern bool g_frpc_auto_rollback;            // 重启失败自动回滚
+extern bool g_frpc_web_enabled;              // Web管理页面开关
+extern std::string g_frpc_admin_addr;        // frpc admin API 地址
+extern int g_frpc_admin_port;                // frpc admin API 端口
+extern std::string g_frpc_admin_user;        // frpc admin 用户名
+extern std::string g_frpc_admin_password;    // frpc admin 密码
+
+// ==================== 终端/SSH 配置 (config.ini [terminal]) ====================
+extern int g_ttyd_port;                  // ttyd 端口
+extern std::string g_ttyd_credential;    // ttyd 凭证 (user:pass)
+extern int g_ttyd_max_clients;           // ttyd 最大客户端数
+extern std::string g_ssh_keys_path;      // authorized_keys 路径
+
+// ==================== 日志管理配置 (config.ini [log]) ====================
+extern int g_log_buffer_size_mb;         // 日志缓冲区大小(MB)
+extern std::string g_log_flush_time;     // 日志写回时间(HH:MM)
+extern std::string g_log_file;           // 日志文件路径
+// g_log_retention_days 已在 fix19 中声明(第106行),此处不重复声明
+extern bool g_log_redirect_stdout;       // 是否重定向 stdout/stderr
+// ✅ fix24-v36: 内存日志缓冲新增配置
+extern bool g_log_enabled;               // 是否启用内存日志模式(1=内存, 0=直写文件)
+extern bool g_log_auto_flush_on_exit;    // 程序退出时是否自动刷盘
+
+// 数据库记录结构
+struct DbRecord {
+    int id;
+    std::string create_time;
+    time_t capture_time = 0;  // ✅ fix24-v13: 抓拍时刻Unix时间戳
+    std::string plate_number;
+    std::string tb_num;
+    int station_type;
+    std::string lo_photo_path;
+    std::string hi_photo_path;
+    int lo_upload_status;
+    int hi_upload_status;
+    int feishu_status;
+    int retry_count;
+};
+
+// 重试结果
+struct RetryResult {
+    bool success = false;
+    std::string message;
+    std::string tb_num;
+    int uploaded_photos = 0;
+};
+
+#endif // COMMON_H

+ 250 - 0
src/config.cpp

@@ -0,0 +1,250 @@
+/**
+ * config.cpp — 配置解析实现
+ * v43.2 模块化拆分 (编译修复版)
+ */
+#include "config.h"
+#include "ini.h"
+#include <cctype>
+
+// fix21: 大小写不敏感的section比较(解决[MQTT]大写导致配置不加载的问题)
+static int section_cmp(const char* section, const char* target) {
+    while (*section && *target) {
+        if (tolower((unsigned char)*section) != tolower((unsigned char)*target)) return 1;
+        section++;
+        target++;
+    }
+    return (*section != '\0' || *target != '\0') ? 1 : 0;
+}
+
+// ✅ fix24-v16: 从URL字符串中解析端口号
+// 支持格式: http://0.0.0.0:8000, https://0.0.0.0:8443, http://192.168.1.1:9000 等
+static int parse_port_from_url(const char* url) {
+    std::string s(url);
+    // 找到 :// 之后的部分
+    size_t scheme_end = s.find("://");
+    std::string host_part = (scheme_end != std::string::npos) ? s.substr(scheme_end + 3) : s;
+    // 找到最后一个 : 后面的端口号
+    size_t colon_pos = host_part.rfind(':');
+    if (colon_pos != std::string::npos) {
+        int port = atoi(host_part.substr(colon_pos + 1).c_str());
+        if (port > 0 && port <= 65535) return port;
+    }
+    // 无法解析时根据协议返回默认端口
+    if (s.find("https") == 0) return 443;
+    return 80;
+}
+
+static int config_ini_handler(void* user, const char* section, const char* name, const char* value)
+{
+    // ✅ fix24-v16: 解析[server]段,从ip字段提取Web服务端口
+    if (section_cmp(section, "server") == 0) {
+        if (strcmp(name, "ip") == 0) {
+            g_web_server_port = parse_port_from_url(value);
+            std::cout << "[配置] Web服务端口: " << g_web_server_port
+                      << " (来自 ip=" << value << ")" << std::endl;
+        }
+    }
+    else if (section_cmp(section, "config") == 0) {
+        if (strcmp(name, "project_name") == 0) project_name = value;
+        else if (strcmp(name, "point_number") == 0) point_number = value;
+        else if (strcmp(name, "throughway") == 0) throughway = value;
+        else if (strcmp(name, "rtsp_url_front_in") == 0) rtsp_url_front_in = value;
+        else if (strcmp(name, "rtsp_url_front_out") == 0) rtsp_url_front_out = value;
+        else if (strcmp(name, "rtsp_url_side_in") == 0) rtsp_url_side_in = value;
+        else if (strcmp(name, "rtsp_url_side_out") == 0) rtsp_url_side_out = value;
+        else if (strcmp(name, "app_api") == 0) app_api = value;
+        else if (strcmp(name, "app_key") == 0) app_key = value;
+        else if (strcmp(name, "app_secret") == 0) app_secret = value;
+        else if (strcmp(name, "DEBUG_LOG") == 0) DEBUG_LOG = atoi(value);
+        else if (strcmp(name, "TestFlag") == 0) TestFlag = atoi(value);
+        else if (strcmp(name, "wType") == 0) g_wType = atoi(value);
+        else if (strcmp(name, "TIME_WINDOW") == 0) g_time_window_min = atoi(value);
+        else if (strcmp(name, "MAX_PHOTO_GROUPS") == 0) g_max_photo_groups = atoi(value);
+        else if (strcmp(name, "in_out_interval") == 0) {
+            int interval_min = atoi(value);
+            g_in_out_interval_sec = (interval_min > 0 ? interval_min * 60 : 300);
+        }
+        else if (strcmp(name, "PLATE_CONFIDENCE_THRESHOLD") == 0) g_PLATE_CONFIDENCE_THRESHOLD = atof(value);
+        else if (strcmp(name, "PLATE_LOG_THRESHOLD") == 0) g_PLATE_LOG_THRESHOLD = atof(value);
+        else if (strcmp(name, "AlternatingMerge") == 0) { g_alternating_merge_enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "roi_in_x") == 0) { g_roi_in.x = atoi(value); }
+        else if (strcmp(name, "roi_in_y") == 0) { g_roi_in.y = atoi(value); }
+        else if (strcmp(name, "roi_in_w") == 0) { g_roi_in.width = atoi(value); }
+        else if (strcmp(name, "roi_in_h") == 0) { g_roi_in.height = atoi(value); }
+        else if (strcmp(name, "roi_in_enabled") == 0) { g_roi_in.enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "roi_out_x") == 0) { g_roi_out.x = atoi(value); }
+        else if (strcmp(name, "roi_out_y") == 0) { g_roi_out.y = atoi(value); }
+        else if (strcmp(name, "roi_out_w") == 0) { g_roi_out.width = atoi(value); }
+        else if (strcmp(name, "roi_out_h") == 0) { g_roi_out.height = atoi(value); }
+        else if (strcmp(name, "roi_out_enabled") == 0) { g_roi_out.enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "roi_debug_enabled") == 0) { g_roi_debug_enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "hw_decode_mode") == 0) {
+            if (strcmp(value, "soft") == 0) g_hw_decode_mode = DecodeMode::SOFT;
+            else if (strcmp(value, "drm") == 0) g_hw_decode_mode = DecodeMode::DRM;
+            else g_hw_decode_mode = DecodeMode::AUTO;
+        }
+        else if (strcmp(name, "hw_decode_device") == 0) { g_hw_decode_device = value; }
+        else if (strcmp(name, "rtsp_transport") == 0) { g_rtsp_transport = value; }
+        // fix19: 文件清理配置
+        else if (strcmp(name, "PhotoMaxCapacityMB") == 0) { g_photo_max_capacity_mb = atoll(value); if (g_photo_max_capacity_mb <= 0) g_photo_max_capacity_mb = 1024; }
+        else if (strcmp(name, "LogRetentionDays") == 0) { g_log_retention_days = atoi(value); if (g_log_retention_days <= 0) g_log_retention_days = 30; }
+    }
+    else if (section_cmp(section, "feishu") == 0) {
+        if (strcmp(name, "APP_ID") == 0) g_feishu_app_id = value;
+        else if (strcmp(name, "APP_SECRET") == 0) g_feishu_app_secret = value;
+        else if (strcmp(name, "CHAT_ID") == 0) g_feishu_chat_id = value;
+        else if (strcmp(name, "TIMEOUT_SECOND") == 0) g_feishu_timeout_second = atol(value);
+        else if (strcmp(name, "WarningSigns") == 0) { g_suppress_emergency_alert = (atoi(value) == 1); }
+    }
+    // ✅ fix18新增:称重系统配置(key与config.ini完全匹配)
+    else if (section_cmp(section, "weight") == 0) {
+        if (strcmp(name, "flagWeight") == 0) g_weight_enabled = (atoi(value) == 1);
+        else if (strcmp(name, "weight_threshold_in") == 0) g_weight_threshold_in = atof(value);
+        else if (strcmp(name, "weight_threshold_out") == 0) g_weight_threshold_out = atof(value);
+        else if (strcmp(name, "default_entry_weight") == 0) g_weight_default_entry = atof(value);
+        else if (strcmp(name, "default_exit_weight") == 0) g_weight_default_exit = atof(value);
+        else if (strcmp(name, "weight_detection_time") == 0) g_weight_detection_time = atoi(value);
+        else if (strcmp(name, "tcp_connect_timeout") == 0) g_weight_tcp_connect_timeout = atoi(value);
+        else if (strcmp(name, "weight_server_ip") == 0) g_weight_server_ip = value;
+        else if (strcmp(name, "weight_server_port") == 0) g_weight_server_port = atoi(value);
+        else if (strcmp(name, "MAX_WEIGHT") == 0) g_weight_max_kg = atof(value);
+        else if (strcmp(name, "STABLE_SAMPLE_COUNT") == 0) g_weight_stable_samples = atoi(value);
+        else if (strcmp(name, "STABLE_THRESHOLD_KG") == 0) g_weight_stable_threshold = atof(value);
+        else if (strcmp(name, "CANDIDATE_DATA_COUNT") == 0) g_weight_candidate_count = atoi(value);
+        else if (strcmp(name, "MAX_UPLOAD_RETRIES") == 0) g_weight_max_retries = atoi(value);
+        else if (strcmp(name, "RETRY_DELAY_MS") == 0) g_weight_retry_delay_ms = atoi(value);
+    }
+    // ✅ fix18新增:MQTT配置(key与config.ini完全匹配)
+    else if (section_cmp(section, "mqtt") == 0) {
+        if (strcmp(name, "MQTT_HOST") == 0) { g_mqtt_host = value; g_mqtt_enabled = true; }
+        else if (strcmp(name, "MQTT_PORT") == 0) g_mqtt_port = atoi(value);
+        else if (strcmp(name, "MQTT_USER") == 0) g_mqtt_username = value;
+        else if (strcmp(name, "MQTT_PASS") == 0) g_mqtt_password = value;
+        else if (strcmp(name, "MQTT_TOPIC") == 0) g_mqtt_topic = value;
+        else if (strcmp(name, "MQTT_CLIENT_ID") == 0) g_mqtt_client_id = value;
+        else if (strcmp(name, "PLATE_COLOR") == 0) g_plate_color = value;
+        else if (strcmp(name, "VEHICLE_TYPE") == 0) g_vehicle_type = value;
+    }
+    // ✅ fix24 登录认证系统:[auth] 配置段
+    else if (section_cmp(section, "auth") == 0) {
+        if (strcmp(name, "enabled") == 0) {
+            g_auth_enabled = (atoi(value) == 1);
+        }
+        else if (strcmp(name, "db_path") == 0) {
+            g_auth_db_path = pathComm + value;
+        }
+    }
+    // ✅ fix24-v12新增:SSL/HTTPS配置
+    else if (section_cmp(section, "ssl") == 0) {
+        if (strcmp(name, "enabled") == 0) { g_ssl_enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "cert_path") == 0) { g_ssl_cert_path = value; }
+        else if (strcmp(name, "key_path") == 0) { g_ssl_key_path = value; }
+    }
+    // ==================== 系统监控 [system] ====================
+    else if (section_cmp(section, "system") == 0) {
+        if (strcmp(name, "monitor_interval") == 0) { g_sys_monitor_interval = atoi(value); }
+        else if (strcmp(name, "temp_alert_threshold") == 0) { g_sys_temp_alert_threshold = atoi(value); }
+        else if (strcmp(name, "cpu_alert_threshold") == 0) { g_sys_cpu_alert_threshold = atoi(value); }
+        else if (strcmp(name, "db_path") == 0) { g_sys_db_path = pathComm + value; }
+        else if (strcmp(name, "history_retention_days") == 0) { g_sys_history_retention_days = atoi(value); }
+        else if (strcmp(name, "aggregation_retention_days") == 0) { g_sys_aggregation_retention_days = atoi(value); }
+        else if (strcmp(name, "flush_time") == 0) { g_sys_flush_time = value; }
+    }
+    // ==================== frpc 管理 [frpc] ====================
+    else if (section_cmp(section, "frpc") == 0) {
+        if (strcmp(name, "config_path") == 0) { g_frpc_config_path = value; }
+        else if (strcmp(name, "backup_path") == 0) { g_frpc_backup_path = value; }
+        else if (strcmp(name, "protected_tunnels") == 0) { g_frpc_protected_tunnels = value; }
+        else if (strcmp(name, "restart_check_timeout") == 0) { g_frpc_restart_check_timeout = atoi(value); }
+        else if (strcmp(name, "auto_rollback") == 0) { g_frpc_auto_rollback = (atoi(value) == 1); }
+        else if (strcmp(name, "web_enabled") == 0) { g_frpc_web_enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "admin_addr") == 0) { g_frpc_admin_addr = value; }
+        else if (strcmp(name, "admin_port") == 0) { g_frpc_admin_port = atoi(value); }
+        else if (strcmp(name, "admin_user") == 0) { g_frpc_admin_user = value; }
+        else if (strcmp(name, "admin_password") == 0) { g_frpc_admin_password = value; }
+    }
+    // ==================== 终端/SSH [terminal] ====================
+    else if (section_cmp(section, "terminal") == 0) {
+        if (strcmp(name, "ttyd_port") == 0) { g_ttyd_port = atoi(value); }
+        else if (strcmp(name, "ttyd_credential") == 0) { g_ttyd_credential = value; }
+        else if (strcmp(name, "ttyd_max_clients") == 0) { g_ttyd_max_clients = atoi(value); }
+        else if (strcmp(name, "ssh_keys_path") == 0) { g_ssh_keys_path = value; }
+    }
+    // ==================== 日志管理 [log] ====================
+    else if (section_cmp(section, "log") == 0) {
+        if (strcmp(name, "enabled") == 0) { g_log_enabled = (atoi(value) == 1); }
+        else if (strcmp(name, "log_file") == 0) { g_log_file = value; }
+        else if (strcmp(name, "buffer_size_mb") == 0) {
+            g_log_buffer_size_mb = atoi(value);
+            if (g_log_buffer_size_mb <= 0) g_log_buffer_size_mb = 20;
+        }
+        else if (strcmp(name, "flush_time") == 0) { g_log_flush_time = value; }
+        else if (strcmp(name, "retention_days") == 0) {
+            g_log_retention_days = atoi(value);
+            if (g_log_retention_days <= 0) g_log_retention_days = 30;
+        }
+        else if (strcmp(name, "redirect_stdout") == 0) { g_log_redirect_stdout = (atoi(value) == 1); }
+        else if (strcmp(name, "auto_flush_on_exit") == 0) { g_log_auto_flush_on_exit = (atoi(value) == 1); }
+    }
+    return 1;
+}
+
+void parse_config_ini(const char* path)
+{
+    if (!path) return;
+    int err = ini_parse(path, config_ini_handler, NULL);
+    if (err < 0) {
+        std::cerr << "无法打开配置文件: " << path << std::endl;
+    }
+    else if (err > 0) {
+        std::cerr << "解析配置文件时第 " << err << " 行发生错误" << std::endl;
+    }
+    else {
+        std::cout << "配置文件解析成功" << std::endl;
+        std::cout << "   TIME_WINDOW = " << g_time_window_min << " 分钟" << std::endl;
+        std::cout << "   MAX_PHOTO_GROUPS = " << g_max_photo_groups << " 组/侧" << std::endl;
+        std::cout << "   飞书APP_ID: " << (g_feishu_app_id.empty() ? "未配置" : "已配置") << std::endl;
+        if (g_weight_enabled) {
+            std::cout << "   称重系统: 已启用" << std::endl;
+            std::cout << "     仪表: " << g_weight_server_ip << ":" << g_weight_server_port << std::endl;
+            std::cout << "     进站阈值: " << g_weight_threshold_in << "公斤"
+                      << " 出站阈值: " << g_weight_threshold_out << "公斤" << std::endl;
+            std::cout << "     检测时间: " << g_weight_detection_time << "秒"
+                      << " 稳定样本: " << g_weight_stable_samples << std::endl;
+        } else {
+            std::cout << "   称重系统: 未启用" << std::endl;
+        }
+        if (g_mqtt_enabled) {
+            std::cout << "   MQTT: 已启用 " << g_mqtt_host << ":" << g_mqtt_port
+                      << " topic=" << g_mqtt_topic << std::endl;
+            std::cout << "     车牌颜色: " << g_plate_color << " 车型: " << g_vehicle_type << std::endl;
+        } else {
+            std::cout << "   MQTT: 未启用" << std::endl;
+        }
+        std::cout << "   照片容量阈值: " << g_photo_max_capacity_mb << " MB" << std::endl;
+        std::cout << "   日志系统: " << (g_log_enabled ? "内存缓冲模式" : "直写文件模式") << std::endl;
+        std::cout << "   日志缓冲区: " << g_log_buffer_size_mb << " MB" << std::endl;
+        std::cout << "   日志刷盘时间: " << g_log_flush_time << std::endl;
+        std::cout << "   日志保留天数: " << g_log_retention_days << " 天" << std::endl;
+        if (g_ssl_enabled) {
+            std::cout << "   HTTPS/SSL: 已启用" << std::endl;
+            std::cout << "     证书: " << g_ssl_cert_path << std::endl;
+            std::cout << "     私钥: " << g_ssl_key_path << std::endl;
+        } else {
+            std::cout << "   HTTPS/SSL: 未启用 (HTTP模式)" << std::endl;
+        }
+        // ✅ fix24-v16: 打印Web服务端口
+        std::cout << "   Web服务端口: " << g_web_server_port << std::endl;
+        // ✅ fix24 登录认证系统状态
+        std::cout << "   登录认证: " << (g_auth_enabled ? "已启用" : "未启用") << std::endl;
+    }
+}
+
+int load_config(const char* config_path)
+{
+    if (ini_parse(config_path, config_ini_handler, nullptr) != 0) {
+        std::cerr << "配置文件解析失败: " << config_path << std::endl;
+        return -1;
+    }
+    return 0;
+}

+ 6 - 0
src/config.h

@@ -0,0 +1,6 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+#include "common.h"
+int load_config(const char* config_path);
+void parse_config_ini(const char* path);
+#endif

+ 966 - 0
src/database.cpp

@@ -0,0 +1,966 @@
+/**
+ * database.cpp — 数据库实现
+ */
+#include "database.h"
+#include "utils.h"
+#include <iostream>
+
+// 前向声明(static函数定义在后面,但init_database()需要调用)
+static void _db_load_tw_cache_unlocked();
+
+bool check_db_health() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return false;
+    
+    sqlite3_stmt* stmt = nullptr;
+    const char* sql = "SELECT 1;";
+    int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[数据库健康检查] 失败: " << sqlite3_errmsg(g_db) << std::endl;
+        return false;
+    }
+    
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    
+    if (rc != SQLITE_ROW) {
+        std::cerr << "[数据库健康检查] 执行失败" << std::endl;
+        return false;
+    }
+    
+    g_metrics.record_db_health_check();
+    return true;
+}
+
+int init_database() {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	g_db_path = pathComm + "data/upload_records.db";
+	int rc = sqlite3_open(g_db_path.c_str(), &g_db);
+	if (rc) {
+		std::cerr << "无法打开数据库: " << sqlite3_errmsg(g_db) << std::endl;
+		g_db = nullptr;
+		return -1;
+	}
+
+	if (sqlite3_db_readonly(g_db, "main") == 1) {
+		std::cerr << "数据库为只读,尝试修复权限..." << std::endl;
+		sqlite3_close(g_db);
+		chmod(g_db_path.c_str(), 0666);
+		rc = sqlite3_open_v2(g_db_path.c_str(), &g_db,
+			SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
+		if (rc != SQLITE_OK) {
+			std::cerr << "无法以读写方式打开数据库" << std::endl;
+			g_db = nullptr;
+			return -1;
+		}
+	}
+
+	// ✅ v41 优化:SQLite busy_timeout 设为 100ms
+	sqlite3_busy_timeout(g_db, DB_BUSY_TIMEOUT_MS);
+
+	const char* create_table_sql =
+		"CREATE TABLE IF NOT EXISTS upload_records ("
+		"    id INTEGER PRIMARY KEY AUTOINCREMENT,"
+		"    create_time DATETIME DEFAULT (datetime('now', 'localtime')),"
+		"    capture_time INTEGER,"  // ✅ fix24-v13: 抓拍时刻Unix时间戳,解决DB插入延迟导致create_time不准
+		"    plate_number TEXT NOT NULL,"
+		"    tb_num TEXT NOT NULL,"
+		"    station_type INTEGER,"
+		"    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"
+		");";
+
+	char* errMsg = nullptr;
+	rc = sqlite3_exec(g_db, create_table_sql, nullptr, nullptr, &errMsg);
+	if (rc != SQLITE_OK) {
+		std::cerr << "创建表失败: " << errMsg << std::endl;
+		sqlite3_free(errMsg);
+		return -1;
+	}
+
+	// ✅ fix24-v13: 为已有数据库添加capture_time列(新库已包含,旧库需ALTER)
+	const char* alter_sql = "ALTER TABLE upload_records ADD COLUMN capture_time INTEGER;";
+	sqlite3_exec(g_db, alter_sql, nullptr, nullptr, nullptr);  // 忽略"duplicate column"错误
+
+	// ✅ 新增覆盖所有查询条件的联合索引,解决N+1查询性能问题
+	const char* create_index_sql =
+		"CREATE INDEX IF NOT EXISTS idx_plate_number ON upload_records(plate_number);"
+		"CREATE INDEX IF NOT EXISTS idx_tb_num ON upload_records(tb_num);"
+		"CREATE INDEX IF NOT EXISTS idx_create_time ON upload_records(create_time);"
+		"CREATE INDEX IF NOT EXISTS idx_plate_station_time_status "
+		"ON upload_records(plate_number, station_type, create_time, lo_upload_status, hi_upload_status);";
+
+	// ✅ TIME_WINDOW持久化表:存储进站/出站工单配额缓存
+	const char* create_tw_cache_sql =
+		"CREATE TABLE IF NOT EXISTS time_window_cache ("
+		"    id INTEGER PRIMARY KEY AUTOINCREMENT,"
+		"    plate_number TEXT NOT NULL,"
+		"    station_type INTEGER NOT NULL,"  // 0=进站, 1=出站
+		"    create_time INTEGER NOT NULL,"   // Unix时间戳:首次处理时间
+		"    last_time INTEGER NOT NULL,"     // Unix时间戳:最后处理时间
+		"    bill_count INTEGER DEFAULT 0,"  // 工单数量
+		"    photo_count INTEGER DEFAULT 0,"  // 照片计数(进站用)
+		"    tb_num TEXT,"                    // 最后一次工单编号
+		"    generation INTEGER DEFAULT 1,"   // 周期代数
+		"    UNIQUE(plate_number, station_type)"
+		");";
+
+	rc = sqlite3_exec(g_db, create_tw_cache_sql, nullptr, nullptr, &errMsg);
+	if (rc != SQLITE_OK) {
+		std::cerr << "创建time_window_cache表失败: " << errMsg << std::endl;
+		sqlite3_free(errMsg);
+	} else {
+		// 创建索引
+		const char* create_tw_index_sql =
+			"CREATE INDEX IF NOT EXISTS idx_tw_plate ON time_window_cache(plate_number, station_type);"
+			"CREATE INDEX IF NOT EXISTS idx_tw_create_time ON time_window_cache(create_time);";
+		sqlite3_exec(g_db, create_tw_index_sql, nullptr, nullptr, nullptr);
+		std::cout << "✅ time_window_cache表初始化成功" << std::endl;
+	}  // ✅ v43修复:闭合else块,station_lock_cache不嵌套在内
+
+	// ✅ v42新增/v43修复:交替锁定缓存表(独立于time_window_cache的else块)
+	const char* create_station_lock_sql =
+		"CREATE TABLE IF NOT EXISTS station_lock_cache ("
+		"    plate_number TEXT PRIMARY KEY,"
+		"    last_in_time INTEGER NOT NULL DEFAULT 0,"
+		"    last_out_time INTEGER NOT NULL DEFAULT 0,"
+		"    current_mode INTEGER NOT NULL DEFAULT 0"  // ✅ v43修复:DEFAULT 0 = INBOUND_ALLOWED
+		");"
+		"CREATE INDEX IF NOT EXISTS idx_slc_plate ON station_lock_cache(plate_number);";
+
+	rc = sqlite3_exec(g_db, create_station_lock_sql, nullptr, nullptr, &errMsg);
+	if (rc != SQLITE_OK) {
+		std::cerr << "创建station_lock_cache表失败: " << errMsg << std::endl;
+		sqlite3_free(errMsg);
+	} else {
+		std::cout << "✅ station_lock_cache表初始化成功" << std::endl;
+	}
+
+	// ✅ fix24-v9新增:称重记录表
+	const char* create_weight_sql =
+		"CREATE TABLE IF NOT EXISTS weight_records ("
+		"    id INTEGER PRIMARY KEY AUTOINCREMENT,"
+		"    create_time DATETIME DEFAULT (datetime('now', 'localtime')),"
+		"    plate_number TEXT NOT NULL,"
+		"    tb_num TEXT NOT NULL,"
+		"    station_type INTEGER NOT NULL,"
+		"    weight_kg REAL NOT NULL,"
+		"    upload_status INTEGER DEFAULT 0,"
+		"    retry_count INTEGER DEFAULT 0,"
+		"    response_msg TEXT"
+		");"
+		"CREATE INDEX IF NOT EXISTS idx_wr_plate ON weight_records(plate_number);"
+		"CREATE INDEX IF NOT EXISTS idx_wr_tb_num ON weight_records(tb_num);"
+		"CREATE INDEX IF NOT EXISTS idx_wr_status ON weight_records(upload_status);";
+
+	rc = sqlite3_exec(g_db, create_weight_sql, nullptr, nullptr, &errMsg);
+	if (rc != SQLITE_OK) {
+		std::cerr << "创建weight_records表失败: " << errMsg << std::endl;
+		sqlite3_free(errMsg);
+	} else {
+		std::cout << "✅ weight_records表初始化成功" << std::endl;
+	}
+
+	rc = sqlite3_exec(g_db, create_index_sql, nullptr, nullptr, &errMsg);
+	if (rc != SQLITE_OK) {
+		sqlite3_free(errMsg);
+	}
+
+	std::cout << "✅ 数据库初始化成功: " << g_db_path << std::endl;
+	
+	return 0;
+}
+
+// ✅ 优化N+1查询为GROUP BY聚合查询,启动速度提升100倍以上
+void load_recent_tbnums_from_db()
+{
+	if (!g_db) return;
+	int window_seconds = g_time_window_min * 60;
+	time_t now = time(NULL);
+
+	std::vector<std::tuple<std::string, std::string, int, time_t, int>> records;
+	{
+		std::lock_guard<std::mutex> lock(g_db_mtx);
+		const char* sql = 
+			"SELECT ur1.plate_number, ur1.tb_num, ur1.station_type, ur1.create_time, "
+			"COUNT(ur2.id) as photo_count "
+			"FROM upload_records ur1 "
+			"LEFT JOIN upload_records ur2 ON "
+			"ur2.plate_number = ur1.plate_number "
+			"AND ur2.station_type = ur1.station_type "
+			"AND ur2.create_time >= ur1.create_time "
+			"AND ur2.lo_upload_status = 1 "
+			"AND ur2.hi_upload_status = 1 "
+			"GROUP BY ur1.plate_number, ur1.station_type, ur1.create_time, ur1.tb_num "
+			"ORDER BY ur1.create_time DESC LIMIT 1000;";
+
+		sqlite3_stmt* stmt = nullptr;
+		int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+		if (rc != SQLITE_OK) {
+			std::cerr << "准备恢复缓存语句失败: " << sqlite3_errmsg(g_db) << std::endl;
+			return;
+		}
+
+		while (sqlite3_step(stmt) == SQLITE_ROW) {
+			const unsigned char* plate_text = sqlite3_column_text(stmt, 0);
+			const unsigned char* tb_text = sqlite3_column_text(stmt, 1);
+			int station_type = sqlite3_column_int(stmt, 2);
+			const unsigned char* create_time_text = sqlite3_column_text(stmt, 3);
+			int photo_count = sqlite3_column_int(stmt, 4);
+
+			if (!plate_text || !tb_text || !create_time_text) continue;
+
+			std::string plate = reinterpret_cast<const char*>(plate_text);
+			std::string tb = reinterpret_cast<const char*>(tb_text);
+			std::string create_time_str = reinterpret_cast<const char*>(create_time_text);
+
+			struct tm tm_buf;
+			memset(&tm_buf, 0, sizeof(tm_buf));
+			if (strptime(create_time_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_buf) == NULL) continue;
+			time_t ct = mktime(&tm_buf);
+			if (ct == (time_t)-1) continue;
+
+			if (difftime(now, ct) <= window_seconds) {
+				records.emplace_back(plate, tb, station_type, ct, photo_count);
+			}
+			else {
+				break;
+			}
+		}
+
+		sqlite3_finalize(stmt);
+	}
+
+	int restored_count = 0;
+	for (const auto& rec : records) {
+		const std::string& plate = std::get<0>(rec);
+		const std::string& tb = std::get<1>(rec);
+		int station_type = std::get<2>(rec);
+		time_t ct = std::get<3>(rec);
+		int photo_count = std::get<4>(rec);
+
+		if (station_type == 1) {
+			std::lock_guard<std::mutex> lock2(g_in_bill_cache_mtx);
+			if (g_in_bill_cache.find(plate) == g_in_bill_cache.end()) {
+				g_in_bill_cache[plate] = { tb, ct, 0, photo_count, true };
+				restored_count++;
+			}
+		}
+		else if (station_type == 2) {
+			std::lock_guard<std::mutex> lock2(g_out_bill_cache_mtx);
+			if (g_out_bill_cache.find(plate) == g_out_bill_cache.end()) {
+				g_out_bill_cache[plate] = { tb, ct, 0, photo_count, true };
+				restored_count++;
+			}
+		}
+	}
+
+	std::cout << "✅ 从数据库恢复了 " << restored_count << " 条联单缓存" << std::endl;
+}
+
+void close_database() {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (g_db) {
+		sqlite3_close(g_db);
+		g_db = nullptr;
+	}
+}
+
+int db_self_test() {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db) return -1;
+
+	const char* insert_sql = "INSERT INTO upload_records (plate_number, tb_num, station_type) "
+		"VALUES ('__db_test__', 'TESTTB', 1);";
+	char* err = nullptr;
+	int rc = sqlite3_exec(g_db, insert_sql, nullptr, nullptr, &err);
+	if (rc != SQLITE_OK) {
+		std::cerr << "数据库自测失败: 插入失败" << std::endl;
+		if (err) sqlite3_free(err);
+		return -1;
+	}
+
+	const char* select_sql = "SELECT id FROM upload_records WHERE plate_number='__db_test__' LIMIT 1;";
+	sqlite3_stmt* stmt = nullptr;
+	rc = sqlite3_prepare_v2(g_db, select_sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK || sqlite3_step(stmt) != SQLITE_ROW) {
+		std::cerr << "数据库自测失败: 查询失败" << std::endl;
+		sqlite3_finalize(stmt);
+		return -1;
+	}
+
+	int test_id = sqlite3_column_int(stmt, 0);
+	sqlite3_finalize(stmt);
+
+	char delete_sql[256];
+	snprintf(delete_sql, sizeof(delete_sql), "DELETE FROM upload_records WHERE id=%d;", test_id);
+	rc = sqlite3_exec(g_db, delete_sql, nullptr, nullptr, &err);
+	if (rc != SQLITE_OK) {
+		if (err) sqlite3_free(err);
+	}
+
+	std::cout << "✅ 数据库自测通过" << std::endl;
+	return 0;
+}
+
+void db_cleanup_old_records_unlocked() {
+    if (!g_db) return;
+
+    struct stat st;
+    if (stat(g_db_path.c_str(), &st) == -1) {
+        std::cerr << "[数据库清理] stat调用失败: " << strerror(errno) 
+                  << ",跳过本次清理" << std::endl;
+        return;
+    }
+
+    if (st.st_size < DB_CLEANUP_THRESHOLD_BYTES) {
+        return;
+    }
+
+    char* err_msg = nullptr;
+    
+    const char* delete_sql = "DELETE FROM upload_records WHERE create_time < datetime('now', '-30 days');";
+    int rc = sqlite3_exec(g_db, delete_sql, nullptr, nullptr, &err_msg);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[数据库清理] 删除旧记录失败: " << err_msg << std::endl;
+        sqlite3_free(err_msg);
+    } else {
+        int affected = sqlite3_changes(g_db);
+        std::cout << "[数据库清理] 删除30天前记录 " << affected << " 条" << std::endl;
+    }
+}
+
+
+// ============================================
+// ============================================
+// TIME_WINDOW持久化函数
+// ============================================
+
+// 保存单个车牌的时间窗口缓存到数据库(仅保存到数据库,不更新内存缓存)
+void db_save_tw_cache(const std::string& plate, bool is_in, time_t create_time, time_t last_time, int bill_count, int photo_count, const std::string& tb_num, int generation) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return;
+    
+    char* errMsg = nullptr;
+    std::string sql = "INSERT OR REPLACE INTO time_window_cache (plate_number, station_type, create_time, last_time, bill_count, photo_count, tb_num, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
+        sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_int(stmt, 2, is_in ? 0 : 1);  // 0=进站, 1=出站
+        sqlite3_bind_int64(stmt, 3, create_time);
+        sqlite3_bind_int64(stmt, 4, last_time);
+        sqlite3_bind_int(stmt, 5, bill_count);
+        sqlite3_bind_int(stmt, 6, photo_count);
+        sqlite3_bind_text(stmt, 7, tb_num.empty() ? "" : tb_num.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_int(stmt, 8, generation);
+        sqlite3_step(stmt);
+        sqlite3_finalize(stmt);
+    }
+    // ✅ 修复:不再在此处更新内存缓存,避免死锁
+    // 内存缓存的更新由调用方在持有正确锁的情况下完成
+}
+
+// 辅助函数:将单条TIME_WINDOW缓存记录加载到内存
+static void _load_single_tw_cache_record(const std::string& plate, int station_type, 
+    time_t create_time, time_t last_time, int bill_count, int photo_count, 
+    const std::string& tb_num, int generation) {
+    bool is_in = (station_type == 0);
+    
+    if (is_in) {
+        std::lock_guard<std::mutex> lock_in(g_in_bill_cache_mtx);
+        g_in_bill_cache[plate] = { tb_num, create_time, 0, photo_count, (bill_count > 0) };
+    } else {
+        std::lock_guard<std::mutex> lock_out(g_out_bill_cache_mtx);
+        g_out_bill_cache[plate] = { tb_num, create_time, 0, photo_count, (bill_count > 0) };
+    }
+    
+    std::lock_guard<std::mutex> lock_rec(is_in ? g_in_record_mtx : g_out_record_mtx);
+    auto& records = is_in ? g_in_upload_records : g_out_upload_records;
+    SceneUploadRecord rec;
+    rec.bill_count = bill_count;
+    rec.last_time = last_time;
+    rec.create_time = create_time;
+    rec.tb_num = tb_num;
+    rec.generation = generation;
+    rec.bill_created = (bill_count > 0);
+    records[plate] = rec;
+}
+
+// 加载时间窗口缓存到内存(内部版本,无锁,由db_load_tw_cache调用)
+static void _db_load_tw_cache_unlocked() {
+    if (!g_db) return;
+    
+    time_t now = time(NULL);
+    int window_seconds = g_time_window_min * 60;
+    
+    std::cout << "[DEBUG] 当前时间: " << now << ", TIME_WINDOW: " << g_time_window_min << "分钟(" << window_seconds << "秒)" << std::endl;
+    
+    // 从time_window_cache加载
+    const char* sql = "SELECT plate_number, station_type, create_time, last_time, bill_count, photo_count, tb_num, generation FROM time_window_cache;";
+    sqlite3_stmt* stmt;
+    
+    int count = 0;
+    int reset_count = 0;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+        while (sqlite3_step(stmt) == SQLITE_ROW) {
+            std::string plate = (const char*)sqlite3_column_text(stmt, 0);
+            int station_type = sqlite3_column_int(stmt, 1);
+            bool is_in = (station_type == 0);
+            time_t create_time = sqlite3_column_int64(stmt, 2);
+            time_t last_time = sqlite3_column_int64(stmt, 3);
+            int bill_count = sqlite3_column_int(stmt, 4);
+            int photo_count = sqlite3_column_int(stmt, 5);
+            const char* tb_num_cstr = (const char*)sqlite3_column_text(stmt, 6);
+            std::string tb_num = tb_num_cstr ? tb_num_cstr : "";
+            int generation = sqlite3_column_int(stmt, 7);
+            
+            // ✅ 核心判断:用last_time判断是否超过TIME_WINDOW
+            // 如果last_time=0,用create_time
+            time_t record_time = last_time > 0 ? last_time : create_time;
+            
+            // 如果record_time=0,说明是新记录(从未处理过),正常加载
+            if (record_time == 0) {
+                std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站") 
+                    << " 新记录,首次处理,正常加载" << std::endl;
+                // 正常加载到内存
+                _load_single_tw_cache_record(plate, station_type, create_time, last_time, 
+                    bill_count, photo_count, tb_num, generation);
+                count++;
+            } else {
+                std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站") 
+                    << " last_time=" << last_time << ", create_time=" << create_time 
+                    << ", 距今: " << (int)difftime(now, record_time) << "秒" << std::endl;
+                
+                // ✅ 判断是否超过TIME_WINDOW
+                if (difftime(now, record_time) >= window_seconds) {
+                    std::cout << "[TIME_WINDOW重置] " << plate << (is_in ? "进站" : "出站")
+                        << " 距上次处理" << (int)difftime(now, record_time) << "秒超过" << window_seconds << "秒,重启后允许重置" << std::endl;
+                    
+                    // ✅ v43.2修复:使用参数化查询防止SQL注入
+                    const char* del_sql = "DELETE FROM time_window_cache WHERE plate_number=? AND station_type=?;";
+                    sqlite3_stmt* del_stmt;
+                    if (sqlite3_prepare_v2(g_db, del_sql, -1, &del_stmt, nullptr) == SQLITE_OK) {
+                        sqlite3_bind_text(del_stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+                        sqlite3_bind_int(del_stmt, 2, station_type);
+                        sqlite3_step(del_stmt);
+                        sqlite3_finalize(del_stmt);
+                    }
+                    
+                    // ✅ 关键修复:同时清理 g_upload_records 中的旧记录,避免重启后配额检查错误
+                    if (is_in) {
+                        std::lock_guard<std::mutex> lock(g_in_record_mtx);
+                        g_in_upload_records.erase(plate);
+                    } else {
+                        std::lock_guard<std::mutex> lock(g_out_record_mtx);
+                        g_out_upload_records.erase(plate);
+                    }
+                    
+                    reset_count++;
+                    // 不加载到内存,让程序重新识别
+                } else {
+                    // ✅ 5分钟内,正常加载缓存
+                    std::cout << "[DEBUG] " << plate << (is_in ? "进站" : "出站") 
+                        << " 5分钟内,正常加载缓存" << std::endl;
+                    _load_single_tw_cache_record(plate, station_type, create_time, last_time, 
+                        bill_count, photo_count, tb_num, generation);
+                    count++;
+                }
+            }
+        }
+        sqlite3_finalize(stmt);
+        std::cout << "✅ 从数据库加载" << count << "条TIME_WINDOW缓存记录,重置" << reset_count << "条" << std::endl;
+    }
+}
+
+// 加载时间窗口缓存到内存(带锁版本,供外部调用)
+void db_load_tw_cache() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    _db_load_tw_cache_unlocked();
+}
+
+// 清理过期的时间窗口缓存(超过24小时)
+void db_cleanup_expired_tw_cache() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return;
+    
+    time_t now = time(NULL);
+    int expired_time = now - 24 * 60 * 60;  // 24小时前
+    
+    // ✅ Bug修复:使用 last_time 判断过期,因为 create_time 可能为0
+    // 同时排除 last_time=0 的记录(表示从未处理过)
+    char sql[256];
+    snprintf(sql, sizeof(sql), 
+        "DELETE FROM time_window_cache WHERE last_time > 0 AND last_time < %ld;", 
+        expired_time);
+    
+    char* errMsg = nullptr;
+    int rc = sqlite3_exec(g_db, sql, nullptr, nullptr, &errMsg);
+    if (rc == SQLITE_OK) {
+        int changes = sqlite3_changes(g_db);
+        if (changes > 0) {
+            std::cout << "[TIME_WINDOW清理] 删除" << changes << "条过期缓存记录" << std::endl;
+        }
+    } else {
+        std::cerr << "[TIME_WINDOW清理] 删除失败: " << errMsg << std::endl;
+        sqlite3_free(errMsg);
+    }
+}
+
+
+void db_vacuum() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return;
+
+    char* err_msg = nullptr;
+    const char* vacuum_sql = "VACUUM;";
+    int rc = sqlite3_exec(g_db, vacuum_sql, nullptr, nullptr, &err_msg);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[数据库优化] VACUUM失败: " << err_msg << std::endl;
+        sqlite3_free(err_msg);
+    } else {
+        std::cout << "[数据库优化] VACUUM执行完成,释放磁盘空间" << std::endl;
+    }
+}
+
+void db_cleanup_old_records() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    db_cleanup_old_records_unlocked();
+}
+
+// ✅ v41 优化:单次数据库操作最大阻塞时间 < 100ms
+int db_insert_record_with_retry(const std::string& plate_number, const std::string& tb_num, int station_type,
+	const std::string& lo_photo_path, const std::string& hi_photo_path,
+	int lo_upload_status, int hi_upload_status, int feishu_status,
+	time_t capture_time) {  // ✅ fix24-v13: 新增capture_time参数
+    
+    for (int retry = 0; retry < DB_INSERT_RETRY_COUNT; retry++) {
+        if (retry > 0) {
+            // 仅重试1次,等待50ms
+            std::cerr << "[数据库插入] 重试第" << retry << "次,等待50ms..." << std::endl;
+            std::this_thread::sleep_for(std::chrono::milliseconds(50));
+        }
+        
+        int result = db_insert_record(plate_number, tb_num, station_type,
+            lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, feishu_status, capture_time);
+        
+        if (result > 0) {
+            if (retry > 0) {
+                g_metrics.record_db_insert_retry_success();
+            }
+            return result;
+        }
+    }
+    
+    g_metrics.record_db_insert_error();
+    // ✅ v41 新增:数据库插入失败时记录严重告警日志
+    std::cerr << "[严重] 数据库插入失败,车牌=" << plate_number 
+              << ",联单=" << tb_num << ",数据将在下次重试时重新插入" << std::endl;
+    return -1;
+}
+
+int db_insert_record(const std::string& plate_number, const std::string& tb_num, int station_type,
+	const std::string& lo_photo_path, const std::string& hi_photo_path,
+	int lo_upload_status, int hi_upload_status, int feishu_status,
+	time_t capture_time) {  // ✅ fix24-v13: 新增capture_time参数
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db) return -1;
+
+	const char* sql = "INSERT INTO upload_records "
+		"(plate_number, tb_num, station_type, lo_photo_path, hi_photo_path, "
+		"lo_upload_status, hi_upload_status, feishu_status, capture_time, create_time) "
+		"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime(?, 'unixepoch', 'localtime'));";
+
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) {
+		std::cerr << "准备插入语句失败: " << sqlite3_errmsg(g_db) << std::endl;
+		return -1;
+	}
+
+	sqlite3_bind_text(stmt, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
+	sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
+	sqlite3_bind_int(stmt, 3, station_type);
+	sqlite3_bind_text(stmt, 4, lo_photo_path.empty() ? nullptr : lo_photo_path.c_str(), -1, SQLITE_TRANSIENT);
+	sqlite3_bind_text(stmt, 5, hi_photo_path.empty() ? nullptr : hi_photo_path.c_str(), -1, SQLITE_TRANSIENT);
+	sqlite3_bind_int(stmt, 6, lo_upload_status);
+	sqlite3_bind_int(stmt, 7, hi_upload_status);
+	sqlite3_bind_int(stmt, 8, feishu_status);
+	sqlite3_bind_int64(stmt, 9, capture_time > 0 ? capture_time : time(NULL));  // ✅ fix24-v13
+	sqlite3_bind_int64(stmt, 10, capture_time > 0 ? capture_time : time(NULL));  // ✅ fix24-v17: create_time使用capture_time,避免DB插入延迟
+
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		std::cerr << "插入记录失败: " << sqlite3_errmsg(g_db) << std::endl;
+		sqlite3_finalize(stmt);
+		return -1;
+	}
+
+	int new_id = static_cast<int>(sqlite3_last_insert_rowid(g_db));
+	sqlite3_finalize(stmt);
+	db_cleanup_old_records_unlocked();
+
+	if (DEBUG_LOG) {
+		std::cout << "[DEBUG] 数据库插入成功,ID: " << new_id
+			<< ",车牌: " << plate_number << ",联单: " << tb_num << std::endl;
+	}
+	return new_id;
+}
+
+int db_update_upload_status(int id, int lo_status, int hi_status) {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db || id <= 0) return -1;
+
+	const char* sql = "UPDATE upload_records SET lo_upload_status=?, hi_upload_status=? WHERE id=?;";
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return -1;
+
+	sqlite3_bind_int(stmt, 1, lo_status);
+	sqlite3_bind_int(stmt, 2, hi_status);
+	sqlite3_bind_int(stmt, 3, id);
+
+	rc = sqlite3_step(stmt);
+	sqlite3_finalize(stmt);
+	return (rc == SQLITE_DONE) ? 0 : -1;
+}
+
+int db_update_feishu_status(int id, int status) {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db || id <= 0) return -1;
+
+	const char* sql = "UPDATE upload_records SET feishu_status=? WHERE id=?;";
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return -1;
+
+	sqlite3_bind_int(stmt, 1, status);
+	sqlite3_bind_int(stmt, 2, id);
+
+	rc = sqlite3_step(stmt);
+	sqlite3_finalize(stmt);
+	return (rc == SQLITE_DONE) ? 0 : -1;
+}
+
+int db_increment_retry_count(int id) {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db || id <= 0) return -1;
+
+	const char* sql = "UPDATE upload_records SET retry_count=retry_count+1 WHERE id=?;";
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return -1;
+
+	sqlite3_bind_int(stmt, 1, id);
+
+	rc = sqlite3_step(stmt);
+	sqlite3_finalize(stmt);
+	return (rc == SQLITE_DONE) ? 0 : -1;
+}
+
+int db_get_total_count() {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db) return 0;
+
+	const char* sql = "SELECT COUNT(*) FROM upload_records;";
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return 0;
+
+	int count = 0;
+	if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
+	sqlite3_finalize(stmt);
+	return count;
+}
+
+int db_get_failed_count() {
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db) return 0;
+
+	const char* sql = "SELECT COUNT(*) FROM upload_records WHERE "
+		"(lo_upload_status != 1 OR hi_upload_status != 1 OR feishu_status = 2);";
+
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return 0;
+
+	int count = 0;
+	if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
+	sqlite3_finalize(stmt);
+	return count;
+}
+
+std::vector<DbRecord> db_get_records(int page, int page_size, const std::string& keyword) {
+	std::vector<DbRecord> records;
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db) return records;
+
+	// ✅ fix24-v14: 校正strftime('%s')的时区问题
+	// SQLite的strftime('%s', str)将str视为UTC,但create_time存的是本地时间
+	// 需要减去时区偏移才能得到正确的Unix时间戳
+	// ✅ fix24-v15.1: 显式CAST为INTEGER,解决COALESCE类型不一致导致排序按字符串比较的Bug
+	std::string tz_modifier = std::to_string(-g_tz_offset_seconds) + " seconds";
+	std::string sort_expr = "CAST(COALESCE(capture_time, strftime('%s', create_time, '" + tz_modifier + "')) AS INTEGER)";
+	std::string sql;
+	if (keyword.empty()) {
+		sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
+			"lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
+			"feishu_status, retry_count FROM upload_records "
+			"ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
+	}
+	else {
+		sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
+			"lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
+			"feishu_status, retry_count FROM upload_records "
+			"WHERE plate_number LIKE ? OR tb_num LIKE ? OR create_time LIKE ? "
+			"ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
+	}
+
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return records;
+
+	int param_idx = 1;
+	if (!keyword.empty()) {
+		std::string like_keyword = "%" + keyword + "%";
+		sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
+		sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
+		sqlite3_bind_text(stmt, param_idx++, like_keyword.c_str(), -1, SQLITE_TRANSIENT);
+	}
+
+	sqlite3_bind_int(stmt, param_idx++, page_size);
+	sqlite3_bind_int(stmt, param_idx++, (page - 1) * page_size);
+
+	while (sqlite3_step(stmt) == SQLITE_ROW) {
+		DbRecord rec;
+		rec.id = sqlite3_column_int(stmt, 0);
+		rec.create_time = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
+		// ✅ fix24-v13: 读取capture_time,旧记录为NULL时fallback到create_time
+		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) {
+			rec.capture_time = sqlite3_column_int64(stmt, 2);
+		} else {
+			rec.capture_time = 0;
+		}
+		rec.plate_number = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
+		rec.tb_num = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
+		rec.station_type = sqlite3_column_int(stmt, 5);
+		const char* lo_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
+		rec.lo_photo_path = lo_path ? lo_path : "";
+		const char* hi_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
+		rec.hi_photo_path = hi_path ? hi_path : "";
+		rec.lo_upload_status = sqlite3_column_int(stmt, 8);
+		rec.hi_upload_status = sqlite3_column_int(stmt, 9);
+		rec.feishu_status = sqlite3_column_int(stmt, 10);
+		rec.retry_count = sqlite3_column_int(stmt, 11);
+		records.push_back(rec);
+	}
+
+	sqlite3_finalize(stmt);
+	return records;
+}
+
+DbRecord db_get_record(int id) {
+	DbRecord rec;
+	std::lock_guard<std::mutex> lock(g_db_mtx);
+	if (!g_db || id <= 0) return rec;
+
+	const char* sql = "SELECT id, create_time, capture_time, plate_number, tb_num, station_type, "
+		"lo_photo_path, hi_photo_path, lo_upload_status, hi_upload_status, "
+		"feishu_status, retry_count FROM upload_records WHERE id=?;";
+
+	sqlite3_stmt* stmt = nullptr;
+	int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr);
+	if (rc != SQLITE_OK) return rec;
+
+	sqlite3_bind_int(stmt, 1, id);
+	if (sqlite3_step(stmt) == SQLITE_ROW) {
+		rec.id = sqlite3_column_int(stmt, 0);
+		rec.create_time = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
+		// ✅ fix24-v13: 读取capture_time
+		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) {
+			rec.capture_time = sqlite3_column_int64(stmt, 2);
+		} else {
+			rec.capture_time = 0;
+		}
+		rec.plate_number = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
+		rec.tb_num = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
+		rec.station_type = sqlite3_column_int(stmt, 5);
+		const char* lo_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
+		rec.lo_photo_path = lo_path ? lo_path : "";
+		const char* hi_path = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
+		rec.hi_photo_path = hi_path ? hi_path : "";
+		rec.lo_upload_status = sqlite3_column_int(stmt, 8);
+		rec.hi_upload_status = sqlite3_column_int(stmt, 9);
+		rec.feishu_status = sqlite3_column_int(stmt, 10);
+		rec.retry_count = sqlite3_column_int(stmt, 11);
+	}
+
+	sqlite3_finalize(stmt);
+	return rec;
+}
+
+int manual_retry_record(int id) {
+	DbRecord rec = db_get_record(id);
+	if (rec.id == 0) return -1;
+	std::cout << "[手动重试] ID=" << id << " 车牌=" << rec.plate_number << std::endl;
+	db_increment_retry_count(id);
+	return 0;
+}
+
+// ==================== v43新增:增强重试功能 ====================
+
+// 重试结果结构体
+
+
+// v43新增:更新联单编号
+void db_update_tb_num(int id, const std::string& tb_num) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db || id <= 0) return;
+    const char* sql = "UPDATE upload_records SET tb_num = ? WHERE id = ?;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+        sqlite3_bind_text(stmt, 1, tb_num.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_int(stmt, 2, id);
+        sqlite3_step(stmt);
+        sqlite3_finalize(stmt);
+    }
+}
+
+// v43新增:更新单侧上传状态
+void db_update_upload_status_side(int id, const std::string& side, int status) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db || id <= 0) return;
+    std::string col = (side == "lo") ? "lo_upload_status" : "hi_upload_status";
+    std::string sql = "UPDATE upload_records SET " + col + " = ? WHERE id = ?;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
+        sqlite3_bind_int(stmt, 1, status);
+        sqlite3_bind_int(stmt, 2, id);
+        sqlite3_step(stmt);
+        sqlite3_finalize(stmt);
+    }
+}
+
+// v43新增:增强重试记录 — 重新获取联单编号 + 重传失败照片 + 更新DB
+
+// ==================== fix24-v9: 称重记录管理 ====================
+
+int db_insert_weight_record(const std::string& plate_number, const std::string& tb_num,
+    int station_type, double weight_kg, int upload_status, const std::string& response_msg) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return -1;
+    const char* sql = "INSERT INTO weight_records (plate_number, tb_num, station_type, weight_kg, upload_status, response_msg) VALUES (?, ?, ?, ?, ?, ?);";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
+    sqlite3_bind_text(stmt, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int(stmt, 3, station_type);
+    sqlite3_bind_double(stmt, 4, weight_kg);
+    sqlite3_bind_int(stmt, 5, upload_status);
+    sqlite3_bind_text(stmt, 6, response_msg.c_str(), -1, SQLITE_TRANSIENT);
+    int rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return (rc == SQLITE_DONE) ? (int)sqlite3_last_insert_rowid(g_db) : -1;
+}
+
+int db_update_weight_upload_status(int id, int upload_status, const std::string& response_msg) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db || id <= 0) return -1;
+    const char* sql = "UPDATE weight_records SET upload_status = ?, response_msg = ? WHERE id = ?;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
+    sqlite3_bind_int(stmt, 1, upload_status);
+    sqlite3_bind_text(stmt, 2, response_msg.c_str(), -1, SQLITE_TRANSIENT);
+    sqlite3_bind_int(stmt, 3, id);
+    int rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return (rc == SQLITE_DONE) ? 0 : -1;
+}
+
+int db_increment_weight_retry_count(int id) {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db || id <= 0) return -1;
+    const char* sql = "UPDATE weight_records SET retry_count = retry_count + 1 WHERE id = ?;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1;
+    sqlite3_bind_int(stmt, 1, id);
+    int rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    return (rc == SQLITE_DONE) ? 0 : -1;
+}
+
+std::vector<DbRecord> db_get_weight_records(int page, int page_size, const std::string& keyword) {
+    std::vector<DbRecord> records;
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return records;
+    if (page < 1) page = 1;
+    if (page_size < 1 || page_size > 100) page_size = 20;
+    int offset = (page - 1) * page_size;
+
+    std::string sql;
+    if (keyword.empty()) {
+        sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records ORDER BY id DESC LIMIT ? OFFSET ?;";
+    } else {
+        sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records WHERE plate_number LIKE ? OR tb_num LIKE ? ORDER BY id DESC LIMIT ? OFFSET ?;";
+    }
+
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return records;
+
+    if (keyword.empty()) {
+        sqlite3_bind_int(stmt, 1, page_size);
+        sqlite3_bind_int(stmt, 2, offset);
+    } else {
+        std::string kw = "%" + keyword + "%";
+        sqlite3_bind_text(stmt, 1, kw.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_text(stmt, 2, kw.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_int(stmt, 3, page_size);
+        sqlite3_bind_int(stmt, 4, offset);
+    }
+
+    while (sqlite3_step(stmt) == SQLITE_ROW) {
+        DbRecord rec;
+        rec.id = sqlite3_column_int(stmt, 0);
+        rec.create_time = (const char*)sqlite3_column_text(stmt, 1);
+        rec.plate_number = (const char*)sqlite3_column_text(stmt, 2);
+        rec.tb_num = (const char*)sqlite3_column_text(stmt, 3);
+        rec.station_type = sqlite3_column_int(stmt, 4);
+        rec.lo_upload_status = sqlite3_column_int(stmt, 6); // upload_status
+        rec.hi_upload_status = sqlite3_column_int(stmt, 7); // retry_count
+        rec.feishu_status = 0;
+        double weight = sqlite3_column_double(stmt, 5);
+        rec.hi_photo_path = std::to_string(weight);
+        const char* msg = (const char*)sqlite3_column_text(stmt, 8);
+        rec.lo_photo_path = msg ? msg : "";
+        records.push_back(rec);
+    }
+    sqlite3_finalize(stmt);
+    return records;
+}
+
+int db_get_weight_total_count() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return 0;
+    const char* sql = "SELECT COUNT(*) FROM weight_records;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return 0;
+    int count = 0;
+    if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
+    sqlite3_finalize(stmt);
+    return count;
+}
+
+int db_get_weight_failed_count() {
+    std::lock_guard<std::mutex> lock(g_db_mtx);
+    if (!g_db) return 0;
+    const char* sql = "SELECT COUNT(*) FROM weight_records WHERE upload_status = 2;";
+    sqlite3_stmt* stmt;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return 0;
+    int count = 0;
+    if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
+    sqlite3_finalize(stmt);
+    return count;
+}

+ 37 - 0
src/database.h

@@ -0,0 +1,37 @@
+#ifndef DATABASE_H
+#define DATABASE_H
+#include "common.h"
+int init_database();
+void close_database();
+int db_self_test();
+bool check_db_health();
+void db_cleanup_old_records();
+void db_cleanup_old_records_unlocked();
+void db_vacuum();
+int db_insert_record_with_retry(const std::string& plate_number, const std::string& tb_num, int station_type, const std::string& lo_photo_path, const std::string& hi_photo_path, int lo_upload_status, int hi_upload_status, int feishu_status, time_t capture_time = 0);  // ✅ fix24-v13
+int db_insert_record(const std::string& plate_number, const std::string& tb_num, int station_type, const std::string& lo_photo_path, const std::string& hi_photo_path, int lo_upload_status, int hi_upload_status, int feishu_status, time_t capture_time = 0);  // ✅ fix24-v13
+int db_update_upload_status(int id, int lo_status, int hi_status);
+int db_update_feishu_status(int id, int status);
+int db_increment_retry_count(int id);
+int db_get_total_count();
+int db_get_failed_count();
+std::vector<DbRecord> db_get_records(int page, int page_size, const std::string& keyword);
+DbRecord db_get_record(int id);
+void db_update_tb_num(int id, const std::string& tb_num);
+void db_update_upload_status_side(int id, const std::string& side, int status);
+void db_save_tw_cache(const std::string& plate, bool is_in, time_t create_time, time_t last_time, int bill_count, int photo_count, const std::string& tb_num, int generation);
+void db_load_tw_cache();
+void db_cleanup_expired_tw_cache();
+void load_recent_tbnums_from_db();
+RetryResult enhanced_retry_record(int db_id);
+
+// ==================== fix24-v9: 称重记录管理 ====================
+int db_insert_weight_record(const std::string& plate_number, const std::string& tb_num,
+    int station_type, double weight_kg, int upload_status, const std::string& response_msg = "");
+int db_update_weight_upload_status(int id, int upload_status, const std::string& response_msg = "");
+int db_increment_weight_retry_count(int id);
+std::vector<DbRecord> db_get_weight_records(int page, int page_size, const std::string& keyword = "");
+int db_get_weight_total_count();
+int db_get_weight_failed_count();
+
+#endif

+ 193 - 0
src/feishu_client.cpp

@@ -0,0 +1,193 @@
+/**
+ * feishu_client.cpp — 飞书消息实现
+ */
+#include "feishu_client.h"
+#include "network_client.h"
+#include <iostream>
+
+bool send_feishu_msg(
+	const std::string& token,
+	const std::string& project_nameEx,
+	const std::string& plateNumber,
+	const std::string& outWorkSiteDate,
+	const std::string& workSiteNo,
+	const std::string& doorNo,
+	const std::string& dType,
+	const std::string& platenumcolor,
+	const std::string& vehicleType,
+	const std::string& tbNo
+) {
+	std::string msg_buf;
+	msg_buf += "工程名:    " + project_nameEx + "\n";
+	msg_buf += "车牌号:    " + plateNumber + "\n";
+	msg_buf += "时间:       " + outWorkSiteDate + "\n";
+	msg_buf += "工地编号:  " + workSiteNo + "\n";
+	msg_buf += "门号:        " + doorNo + "\n";
+	msg_buf += "进出站:     " + dType + "\n";
+	msg_buf += "车牌颜色:  " + platenumcolor + "\n";
+	msg_buf += "车型:         " + vehicleType + "\n";
+	msg_buf += "三联单编号:\n" + tbNo;
+
+	cJSON* content = cJSON_CreateObject();
+	cJSON_AddStringToObject(content, "text", msg_buf.c_str());
+	char* content_str = cJSON_PrintUnformatted(content);
+
+	cJSON* root = cJSON_CreateObject();
+	cJSON_AddStringToObject(root, "receive_id", g_feishu_chat_id.c_str());
+	cJSON_AddStringToObject(root, "msg_type", "text");
+	cJSON_AddStringToObject(root, "content", content_str);
+	char* post_data = cJSON_PrintUnformatted(root);
+
+	std::string url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id";
+	std::string auth = "Authorization: Bearer " + token;
+
+	// v41.2: MAX_RETRY=4 表示 初始请求 + 3次重试 = 4次请求
+	const int MAX_RETRY = 4;
+	bool success = false;
+
+	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) continue;
+
+		std::string response;
+		struct curl_slist* headers = NULL;
+		headers = curl_slist_append(headers, "Content-Type: application/json");
+		headers = curl_slist_append(headers, auth.c_str());
+
+		InitCurlCommon(curl, &response);
+		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+		curl_easy_setopt(curl, CURLOPT_POST, 1L);
+		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+		CURLcode res = curl_easy_perform(curl);
+		if (res == CURLE_OK) {
+			cJSON* resp_json = cJSON_Parse(response.c_str());
+			if (resp_json) {
+				cJSON* code_obj = cJSON_GetObjectItemCaseSensitive(resp_json, "code");
+				cJSON* msg_obj = cJSON_GetObjectItemCaseSensitive(resp_json, "msg");
+				if (code_obj && code_obj->valueint == 0) {
+					std::cout << "✅ 飞书消息发送成功" << std::endl;
+					success = true;
+					g_metrics.record_feishu_success();
+				}
+				else {
+					std::cerr << "❌ 飞书发送失败:code=" << (code_obj ? std::to_string(code_obj->valueint) : "null")
+						<< ",msg=" << (msg_obj ? msg_obj->valuestring : "无消息") << std::endl;
+					cJSON_Delete(resp_json);
+					curl_slist_free_all(headers);
+					curl_easy_cleanup(curl);
+					break;
+				}
+				cJSON_Delete(resp_json);
+			}
+		}
+		else {
+			std::cerr << "❌ 飞书请求失败:" << curl_easy_strerror(res) << std::endl;
+			if (res != CURLE_OPERATION_TIMEDOUT && res != CURLE_COULDNT_CONNECT) {
+				curl_slist_free_all(headers);
+				curl_easy_cleanup(curl);
+				break;
+			}
+		}
+
+		curl_slist_free_all(headers);
+		curl_easy_cleanup(curl);
+		if (success) break;
+	}
+
+	cJSON_Delete(content);
+	cJSON_Delete(root);
+	free(content_str);
+	free(post_data);
+	return success;
+}
+
+// ✅ 新增: get_format_time 前向声明
+std::string get_format_time(void);
+
+// ✅ 新增:永久失败告警函数
+bool send_permanent_failure_alert(const std::string& plate, bool is_in, int retry_count, int db_id) {
+    if (g_suppress_emergency_alert) return false;  // ✅ FIX9-1: WarningSigns=1抑制紧急告警
+    std::string token = get_cached_tenant_token();
+    if (token.empty()) {
+        std::cerr << "[告警发送] 获取飞书Token失败,无法发送告警" << std::endl;
+        return false;
+    }
+
+    std::string msg_buf;
+    msg_buf += "⚠️ 【紧急告警】车牌识别系统上传永久失败\n\n";
+    msg_buf += "工程名:    " + project_name + "\n";
+    msg_buf += "车牌号:    " + plate + "\n";
+    msg_buf += std::string("进出站:     ") + (is_in ? "进站" : "出站") + "\n";
+    msg_buf += "失败次数:  " + std::to_string(retry_count) + "次\n";
+    msg_buf += "数据库ID:  " + (db_id > 0 ? std::to_string(db_id) : "未插入") + "\n";
+    msg_buf += std::string("时间:       ") + get_format_time() + "\n";
+    msg_buf += "\n请运维人员立即登录Web后台手动处理!";
+
+    cJSON* content = cJSON_CreateObject();
+    cJSON_AddStringToObject(content, "text", msg_buf.c_str());
+    char* content_str = cJSON_PrintUnformatted(content);
+
+    cJSON* root = cJSON_CreateObject();
+    cJSON_AddStringToObject(root, "receive_id", g_feishu_chat_id.c_str());
+    cJSON_AddStringToObject(root, "msg_type", "text");
+    cJSON_AddStringToObject(root, "content", content_str);
+    char* post_data = cJSON_PrintUnformatted(root);
+
+    std::string url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id";
+    std::string auth = "Authorization: Bearer " + token;
+
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        cJSON_Delete(content);
+        cJSON_Delete(root);
+        free(content_str);
+        free(post_data);
+        return false;
+    }
+
+    std::string response;
+    struct curl_slist* headers = NULL;
+    headers = curl_slist_append(headers, "Content-Type: application/json");
+    headers = curl_slist_append(headers, auth.c_str());
+
+    InitCurlCommon(curl, &response);
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+    curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+    CURLcode res = curl_easy_perform(curl);
+    bool success = false;
+    if (res == CURLE_OK) {
+        cJSON* resp_json = cJSON_Parse(response.c_str());
+        if (resp_json) {
+            cJSON* code_obj = cJSON_GetObjectItemCaseSensitive(resp_json, "code");
+            if (code_obj && code_obj->valueint == 0) {
+                std::cout << "✅ 永久失败告警发送成功: " << plate << std::endl;
+                success = true;
+                g_metrics.record_alert_sent();
+            }
+            cJSON_Delete(resp_json);
+        }
+    }
+
+    curl_slist_free_all(headers);
+    curl_easy_cleanup(curl);
+    cJSON_Delete(content);
+    cJSON_Delete(root);
+    free(content_str);
+    free(post_data);
+
+    return success;
+}
+
+// ==================== 工具函数 ====================

+ 15 - 0
src/feishu_client.h

@@ -0,0 +1,15 @@
+#ifndef FEISHU_CLIENT_H
+#define FEISHU_CLIENT_H
+#include "common.h"
+bool send_feishu_msg(const std::string& token,
+    const std::string& project_nameEx,
+    const std::string& plateNumber,
+    const std::string& outWorkSiteDate,
+    const std::string& workSiteNo,
+    const std::string& doorNo,
+    const std::string& dType,
+    const std::string& platenumcolor,
+    const std::string& vehicleType,
+    const std::string& tbNo);
+bool send_permanent_failure_alert(const std::string& plate, bool is_in, int retry_count, int db_id);
+#endif

+ 368 - 0
src/frpc_manager.cpp

@@ -0,0 +1,368 @@
+/**
+ * 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() {
+    // 方法1: systemctl(如果 frpc 是 systemd 服务)
+    FILE *fp = popen((systemctl_cmd() + " is-active frpc 2>/dev/null").c_str(), "r");
+    if (fp) {
+        char buf[64] = {0};
+        if (fgets(buf, sizeof(buf) - 1, fp)) {
+            char *nl = strchr(buf, '\n');
+            if (nl) *nl = '\0';
+            pclose(fp);
+            if (strcmp(buf, "active") == 0) return true;
+        } else {
+            pclose(fp);
+        }
+    }
+    // 方法2: pgrep 检查进程是否存在
+    fp = popen("pgrep -x frpc >/dev/null 2>&1 && echo found || echo notfound", "r");
+    if (fp) {
+        char buf[64] = {0};
+        if (fgets(buf, sizeof(buf) - 1, fp)) {
+            pclose(fp);
+            if (strstr(buf, "found") && !strstr(buf, "notfound")) return true;
+        } else {
+            pclose(fp);
+        }
+    }
+    // 方法3: 尝试连接 admin API(如果可达则说明进程在运行)
+    {
+        std::string resp = call_admin_api("/api/serverinfo");
+        if (!resp.empty()) return true;
+    }
+    return false;
+}
+
+// ========== 获取 frpc 版本 ==========
+static std::string get_frpc_version() {
+    // 方法1: 从 admin API /api/serverinfo 获取
+    std::string resp = call_admin_api("/api/serverinfo");
+    if (!resp.empty()) {
+        // 尝试从响应中提取版本号
+        size_t pos = resp.find("\"version\"");
+        if (pos != std::string::npos) {
+            size_t colon = resp.find(':', pos);
+            size_t q1 = resp.find('"', colon + 1);
+            size_t q2 = resp.find('"', q1 + 1);
+            if (q1 != std::string::npos && q2 != std::string::npos) {
+                return resp.substr(q1 + 1, q2 - q1 - 1);
+            }
+        }
+    }
+    // 方法2: 命令行获取
+    FILE *fp = popen("frpc --version 2>/dev/null || /opt/openAI/frp/frpc --version 2>/dev/null", "r");
+    if (fp) {
+        char buf[128] = {0};
+        if (fgets(buf, sizeof(buf) - 1, fp)) {
+            pclose(fp);
+            char *nl = strchr(buf, '\n');
+            if (nl) *nl = '\0';
+            std::string ver(buf);
+            size_t start = ver.find_first_not_of(" \t\r\n");
+            if (start != std::string::npos) {
+                return ver.substr(start);
+            }
+        } else {
+            pclose(fp);
+        }
+    }
+    return "";
+}
+
+// ========== 获取 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;
+    }
+
+    // ✅ fix24-v39: 获取 frpc 版本
+    st.version = get_frpc_version();
+
+    // 调用 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

+ 50 - 0
src/frpc_manager.h

@@ -0,0 +1,50 @@
+/**
+ * frpc_manager.h - frpc 服务管理
+ * fix24 v24: 隧道状态/配置编辑/重启+回滚
+ */
+#ifndef FRPC_MANAGER_H
+#define FRPC_MANAGER_H
+
+#include <string>
+#include <vector>
+
+struct FrpcTunnel {
+    std::string name;        // 隧道名称
+    std::string type;        // tcp/udp/http/https/stcp/xtcp
+    std::string local_addr;  // 本地地址
+    std::string remote_addr; // 远程地址
+    std::string status;      // online/offline
+    long today_traffic_in;   // 今日入流量 (bytes)
+    long today_traffic_out;  // 今日出流量 (bytes)
+};
+
+struct FrpcStatus {
+    bool running;                          // frpc 进程是否运行
+    std::string version;                   // frpc 版本
+    std::vector<FrpcTunnel> tunnels;       // 隧道列表
+    std::string error;                     // 错误信息(如有)
+};
+
+namespace frpc_mgr {
+
+// 获取 frpc 状态(进程状态 + 隧道列表)
+FrpcStatus get_status();
+
+// 读取 frpc.toml 配置内容
+std::string read_config();
+
+// 保存 frpc.toml(自动备份旧文件)
+bool save_config(const std::string &content, std::string &error);
+
+// 重启 frpc 服务(可选自动回滚)
+bool restart_service(std::string &error);
+
+// 获取 frpc admin API 的 HTTP 响应
+std::string call_admin_api(const std::string &endpoint);
+
+// 检查 frpc 进程是否存活
+bool is_process_alive();
+
+}  // namespace frpc_mgr
+
+#endif  // FRPC_MANAGER_H

+ 311 - 0
src/frpc_manager1.cpp

@@ -0,0 +1,311 @@
+/**
+ * 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

+ 523 - 0
src/log_manager.cpp

@@ -0,0 +1,523 @@
+/**
+ * log_manager.cpp - 内存日志缓冲管理系统实现
+ * fix24 v36: 日志写入内存,定时刷盘,Web从内存读取
+ *
+ * 工作流程:
+ *   enabled=1: stdout/stderr → pipe → 读取线程 → 内存buffer → 定时刷盘
+ *   enabled=0: stdout/stderr → 原有方式(systemd重定向到文件)
+ *   Web API: 直接从内存buffer读取,零磁盘I/O
+ */
+#include "log_manager.h"
+#include "common.h"
+
+#include <fstream>
+#include <sstream>
+#include <cstdio>
+#include <cstring>
+#include <ctime>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <chrono>
+#include <algorithm>
+
+// ==================== 全局单例 ====================
+static MemoryLogBuffer g_log_buffer_instance;
+
+MemoryLogBuffer& MemoryLogBuffer::instance() {
+    return g_log_buffer_instance;
+}
+
+// ==================== 构造/析构 ====================
+MemoryLogBuffer::MemoryLogBuffer()
+    : max_memory_bytes_(20 * 1024 * 1024)  // 默认20MB
+    , current_bytes_(0)
+    , log_file_("PlateRecApp.log")
+    , flush_time_("23:20")
+    , retention_days_(30)
+    , redirect_stdout_(true)
+    , auto_flush_on_exit_(true)
+    , enabled_(true)
+    , flush_running_(false)
+    , pipe_fd_{-1, -1}
+    , saved_stdout_(-1)
+    , saved_stderr_(-1)
+    , stdout_redirect_active_(false)
+    , total_flush_count_(0)
+{
+}
+
+MemoryLogBuffer::~MemoryLogBuffer() {
+    stop();
+}
+
+// ==================== 初始化 ====================
+void MemoryLogBuffer::init(size_t max_bytes, const std::string& flush_time,
+                           const std::string& log_file, int retention_days,
+                           bool redirect_stdout, bool auto_flush_on_exit) {
+    max_memory_bytes_ = max_bytes;
+    flush_time_ = flush_time;
+    log_file_ = log_file;
+    retention_days_ = retention_days;
+    redirect_stdout_ = redirect_stdout;
+    auto_flush_on_exit_ = auto_flush_on_exit;
+    enabled_ = (max_bytes > 0);
+
+    std::cout << "[log_mgr] 初始化: "
+              << "enabled=" << (enabled_ ? 1 : 0)
+              << " buffer=" << (max_bytes / 1024 / 1024) << "MB"
+              << " flush_time=" << flush_time
+              << " log_file=" << log_file
+              << " retention=" << retention_days << "天"
+              << " redirect_stdout=" << (redirect_stdout ? 1 : 0)
+              << " auto_flush_on_exit=" << (auto_flush_on_exit ? 1 : 0)
+              << std::endl;
+}
+
+// ==================== 启动 ====================
+bool MemoryLogBuffer::start() {
+    if (!enabled_) {
+        std::cout << "[log_mgr] 内存模式禁用,使用传统直写模式" << std::endl;
+        return true;
+    }
+
+    // 启动刷盘线程
+    flush_running_.store(true);
+    flush_thread_ = std::thread(&MemoryLogBuffer::flush_thread_func, this);
+    std::cout << "[log_mgr] 刷盘线程已启动,定时刷盘时间: " << flush_time_ << std::endl;
+
+    // 启动stdout重定向
+    if (redirect_stdout_) {
+        if (pipe(pipe_fd_) != 0) {
+            std::cerr << "[log_mgr] 创建pipe失败: " << strerror(errno) << std::endl;
+            return false;
+        }
+
+        // 保存原始stdout/stderr fd
+        saved_stdout_ = dup(STDOUT_FILENO);
+        saved_stderr_ = dup(STDERR_FILENO);
+
+        // 将stdout/stderr重定向到pipe写端
+        dup2(pipe_fd_[1], STDOUT_FILENO);
+        dup2(pipe_fd_[1], STDERR_FILENO);
+
+        // 设置非阻塞,防止write阻塞
+        fcntl(pipe_fd_[0], F_SETFL, O_NONBLOCK);
+        fcntl(pipe_fd_[1], F_SETFL, O_NONBLOCK);
+
+        // 关闭不需要的写端副本(子进程不继承)
+        close(pipe_fd_[1]);
+        pipe_fd_[1] = -1;
+
+        stdout_redirect_active_.store(true);
+        stdout_thread_ = std::thread(&MemoryLogBuffer::stdout_reader_func, this);
+        std::cout << "[log_mgr] stdout/stderr已重定向到内存缓冲区" << std::endl;
+    }
+
+    return true;
+}
+
+// ==================== 停止 ====================
+void MemoryLogBuffer::stop() {
+    // 停止刷盘线程
+    if (flush_running_.load()) {
+        flush_running_.store(false);
+        flush_cv_.notify_all();
+        if (flush_thread_.joinable()) {
+            flush_thread_.join();
+        }
+    }
+
+    // 停止stdout重定向
+    if (stdout_redirect_active_.load()) {
+        // 1. 通知读取线程退出
+        stdout_redirect_active_.store(false);
+
+        // 2. 恢复原始stdout/stderr(必须在join之前,否则后续日志丢失)
+        if (saved_stdout_ >= 0) {
+            dup2(saved_stdout_, STDOUT_FILENO);
+            close(saved_stdout_);
+            saved_stdout_ = -1;
+        }
+        if (saved_stderr_ >= 0) {
+            dup2(saved_stderr_, STDERR_FILENO);
+            close(saved_stderr_);
+            saved_stderr_ = -1;
+        }
+
+        // 3. 等待读取线程退出(读取线程自己关闭pipe读端fd,避免跨线程关闭fd)
+        if (stdout_thread_.joinable()) {
+            stdout_thread_.join();
+        }
+        // pipe_fd_[0] 已由读取线程关闭
+        pipe_fd_[0] = -1;
+    }
+
+    // 最后一次刷盘
+    if (enabled_ && auto_flush_on_exit_ && !buffer_.empty()) {
+        std::string error;
+        flush_to_file(error);
+        std::cout << "[log_mgr] 退出前刷盘完成,共 " << buffer_.size() << " 条日志" << std::endl;
+    }
+}
+
+// ==================== 写入日志条目 ====================
+void MemoryLogBuffer::push(const std::string& level, const std::string& message) {
+    if (!enabled_) return;
+
+    LogEntry entry(current_timestamp(), level, message);
+
+    std::lock_guard<std::mutex> lock(mutex_);
+
+    // 环形缓冲:超限丢弃最旧
+    while (current_bytes_ + entry.byte_size > max_memory_bytes_ && !buffer_.empty()) {
+        current_bytes_ -= buffer_.front().byte_size;
+        buffer_.pop_front();
+    }
+
+    buffer_.push_back(std::move(entry));
+    current_bytes_ += buffer_.back().byte_size;
+}
+
+// ==================== 获取最后N条 ====================
+std::deque<LogEntry> MemoryLogBuffer::tail(int n) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    std::deque<LogEntry> result;
+    int start = (int)buffer_.size() - n;
+    if (start < 0) start = 0;
+    for (int i = start; i < (int)buffer_.size(); i++) {
+        result.push_back(buffer_[i]);
+    }
+    return result;
+}
+
+// ==================== 获取状态 ====================
+LogStatus MemoryLogBuffer::get_status() {
+    LogStatus st;
+    st.log_file_path = pathComm + log_file_;
+    st.buffer_size_mb = max_memory_bytes_ / (1024 * 1024);
+    st.flush_time = flush_time_;
+    st.retention_days = retention_days_;
+    st.redirect_enabled = redirect_stdout_;
+    st.memory_enabled = enabled_;
+
+    // 内存统计
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        st.line_count = buffer_.size();
+        st.memory_usage_bytes = current_bytes_;
+        st.buffer_usage_percent = (max_memory_bytes_ > 0) ?
+            (double)current_bytes_ / max_memory_bytes_ * 100.0 : 0;
+    }
+
+    // 磁盘文件统计
+    struct stat file_stat;
+    if (stat(st.log_file_path.c_str(), &file_stat) == 0) {
+        st.file_size_mb = file_stat.st_size / (1024 * 1024);
+        char time_buf[64];
+        struct tm *tm = localtime(&file_stat.st_mtime);
+        strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm);
+        st.last_modified = time_buf;
+    } else {
+        st.file_size_mb = 0;
+        st.last_modified = "文件不存在";
+    }
+
+    // 刷盘统计
+    {
+        std::lock_guard<std::mutex> lock(stats_mutex_);
+        st.last_flush_time = last_flush_time_;
+        st.total_flush_count = total_flush_count_.load();
+    }
+
+    return st;
+}
+
+// ==================== 手动刷盘 ====================
+bool MemoryLogBuffer::flush_to_file(std::string& error) {
+    if (buffer_.empty()) {
+        error = "缓冲区为空,无需刷盘";
+        return true;
+    }
+
+    // 生成归档文件名
+    std::string archive_path = pathComm + generate_archive_filename();
+
+    // 拷贝buffer到局部变量(减少锁持有时间)
+    std::deque<LogEntry> to_flush;
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        to_flush.swap(buffer_);
+        current_bytes_ = 0;
+    }
+
+    // 写入文件(追加模式)
+    std::ofstream ofs(archive_path, std::ios::app);
+    if (!ofs.is_open()) {
+        error = "无法打开日志文件: " + archive_path;
+        // 恢复buffer
+        std::lock_guard<std::mutex> lock(mutex_);
+        buffer_ = std::move(to_flush);
+        for (auto& e : buffer_) current_bytes_ += e.byte_size;
+        return false;
+    }
+
+    for (const auto& entry : to_flush) {
+        ofs << "[" << entry.timestamp << "] [" << entry.level << "] " << entry.message << "\n";
+    }
+    ofs.flush();
+    ofs.close();
+
+    // 同步到磁盘
+    sync();
+
+    // 更新统计
+    {
+        std::lock_guard<std::mutex> lock(stats_mutex_);
+        time_t now = time(NULL);
+        char time_buf[64];
+        struct tm *tm = localtime(&now);
+        strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm);
+        last_flush_time_ = time_buf;
+    }
+    total_flush_count_.fetch_add(1);
+
+    std::cout << "[log_mgr] 刷盘完成: " << archive_path
+              << " (" << to_flush.size() << " 条)" << std::endl;
+    return true;
+}
+
+// ==================== 清理过期日志 ====================
+bool MemoryLogBuffer::cleanup_old_logs(std::string& error) {
+    std::string dir = pathComm;
+    DIR* d = opendir(dir.c_str());
+    if (!d) {
+        error = "无法打开目录: " + dir;
+        return false;
+    }
+
+    time_t now = time(NULL);
+    time_t cutoff = now - (retention_days_ * 86400);
+    int removed_count = 0;
+
+    struct dirent* entry;
+    while ((entry = readdir(d)) != NULL) {
+        std::string name = entry->d_name;
+        // 匹配 .log 后缀的文件
+        if (name.size() > 4 && name.substr(name.size() - 4) == ".log") {
+            std::string full_path = dir + name;
+            struct stat st;
+            if (stat(full_path.c_str(), &st) == 0) {
+                // 跳过当前活跃日志文件
+                if (name == log_file_) continue;
+
+                if (st.st_mtime < cutoff) {
+                    if (remove(full_path.c_str()) == 0) {
+                        removed_count++;
+                        std::cout << "[log_mgr] 已删除过期日志: " << name << std::endl;
+                    }
+                }
+            }
+        }
+    }
+    closedir(d);
+
+    std::cout << "[log_mgr] 清理完成,删除 " << removed_count << " 个过期日志文件" << std::endl;
+    return true;
+}
+
+// ==================== 信号处理调用 ====================
+void MemoryLogBuffer::signal_flush() {
+    if (!enabled_) return;
+    if (buffer_.empty()) return;
+
+    // 注意:信号处理中不能加锁,这里用try_lock
+    std::unique_lock<std::mutex> lock(mutex_, std::try_to_lock);
+    if (!lock.owns_lock()) return;  // 被其他线程持有,放弃
+
+    // 直接写入文件
+    std::string archive_path = pathComm + generate_archive_filename();
+    std::ofstream ofs(archive_path, std::ios::app);
+    if (ofs.is_open()) {
+        for (const auto& entry : buffer_) {
+            ofs << "[" << entry.timestamp << "] [" << entry.level << "] " << entry.message << "\n";
+        }
+        ofs.flush();
+        ofs.close();
+    }
+    buffer_.clear();
+    current_bytes_ = 0;
+}
+
+// ==================== 刷盘线程 ====================
+void MemoryLogBuffer::flush_thread_func() {
+    int target_hour = 23, target_minute = 20;
+    parse_flush_time(flush_time_, target_hour, target_minute);
+
+    while (flush_running_.load()) {
+        // 等待60秒或收到退出信号
+        {
+            std::unique_lock<std::mutex> lock(flush_mutex_);
+            flush_cv_.wait_for(lock, std::chrono::seconds(60), [this]() {
+                return !flush_running_.load();
+            });
+        }
+
+        if (!flush_running_.load()) break;
+
+        // 检查是否到达刷盘时间
+        time_t now = time(NULL);
+        struct tm* tm_now = localtime(&now);
+
+        if (tm_now->tm_hour == target_hour && tm_now->tm_min == target_minute) {
+            std::string error;
+            flush_to_file(error);
+
+            // 刷盘后清理过期日志
+            cleanup_old_logs(error);
+
+            // 等待2分钟避免重复触发(同一分钟内只刷一次)
+            std::this_thread::sleep_for(std::chrono::seconds(120));
+        }
+    }
+}
+
+// ==================== stdout读取线程 ====================
+void MemoryLogBuffer::stdout_reader_func() {
+    const size_t READ_BUF_SIZE = 8192;
+    char read_buf[READ_BUF_SIZE];
+    std::string line_buffer;
+    int fd = pipe_fd_[0];  // 本地保存fd,避免主线程提前关闭
+
+    while (stdout_redirect_active_.load()) {
+        ssize_t n = read(fd, read_buf, sizeof(read_buf) - 1);
+        if (n > 0) {
+            read_buf[n] = '\0';
+            line_buffer += read_buf;
+
+            // 按行分割
+            size_t pos;
+            while ((pos = line_buffer.find('\n')) != std::string::npos) {
+                std::string line = line_buffer.substr(0, pos);
+                line_buffer.erase(0, pos + 1);
+
+                // 去掉行尾\r
+                if (!line.empty() && line.back() == '\r') {
+                    line.pop_back();
+                }
+                if (line.empty()) continue;
+
+                // 解析日志级别
+                std::string level = "INFO";
+                if (line.find("[ERROR]") != std::string::npos ||
+                    line.find("错误") != std::string::npos ||
+                    line.find("失败") != std::string::npos) {
+                    level = "ERROR";
+                } else if (line.find("[WARN") != std::string::npos ||
+                           line.find("[告警]") != std::string::npos) {
+                    level = "WARN";
+                } else if (line.find("[DEBUG]") != std::string::npos) {
+                    level = "DEBUG";
+                }
+
+                push(level, line);
+            }
+        } else if (n < 0) {
+            if (errno == EAGAIN || errno == EWOULDBLOCK) {
+                std::this_thread::sleep_for(std::chrono::milliseconds(50));
+            } else {
+                break;  // EBADF或其他错误 → 退出
+            }
+        } else {
+            break;  // n == 0: pipe已关闭
+        }
+    }
+
+    // 处理残留数据
+    if (!line_buffer.empty()) {
+        push("INFO", line_buffer);
+    }
+
+    // 读取线程负责关闭pipe读端(避免跨线程关闭fd)
+    close(fd);
+}
+
+// ==================== 辅助方法 ====================
+bool MemoryLogBuffer::parse_flush_time(const std::string& time_str, int& hour, int& minute) {
+    // 解析 "HH:MM" 格式
+    size_t colon_pos = time_str.find(':');
+    if (colon_pos == std::string::npos || colon_pos == 0 || colon_pos >= time_str.size() - 1) {
+        hour = 23;
+        minute = 20;
+        return false;
+    }
+    hour = atoi(time_str.substr(0, colon_pos).c_str());
+    minute = atoi(time_str.substr(colon_pos + 1).c_str());
+    if (hour < 0 || hour > 23) hour = 23;
+    if (minute < 0 || minute > 59) minute = 20;
+    return true;
+}
+
+std::string MemoryLogBuffer::generate_archive_filename() {
+    time_t now = time(NULL);
+    struct tm* tm_now = localtime(&now);
+    char buf[64];
+    // 生成 PlateRecApp_20260705.log 格式
+    snprintf(buf, sizeof(buf), "PlateRecApp_%04d%02d%02d.log",
+             tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday);
+    return std::string(buf);
+}
+
+std::string MemoryLogBuffer::current_timestamp() {
+    time_t now = time(NULL);
+    struct tm* tm_now = localtime(&now);
+    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,
+             tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec);
+    return std::string(buf);
+}
+
+// ==================== 兼容旧接口(namespace log_mgr) ====================
+namespace log_mgr {
+
+LogStatus get_log_status() {
+    return MemoryLogBuffer::instance().get_status();
+}
+
+bool flush_log(std::string& error) {
+    return MemoryLogBuffer::instance().flush_to_file(error);
+}
+
+bool cleanup_old_logs(std::string& error) {
+    return MemoryLogBuffer::instance().cleanup_old_logs(error);
+}
+
+std::string tail_log(int lines) {
+    // 从内存buffer读取
+    auto entries = MemoryLogBuffer::instance().tail(lines);
+    std::string result;
+    for (const auto& entry : entries) {
+        result += "[" + entry.timestamp + "] [" + entry.level + "] " + entry.message + "\n";
+    }
+
+    // 如果内存buffer为空(刚启动或enabled=0),回退到文件读取
+    if (result.empty() && !MemoryLogBuffer::instance().get_status().memory_enabled) {
+        std::string log_path = pathComm + g_log_file;
+        std::string cmd = "tail -n " + std::to_string(lines) + " " + log_path + " 2>/dev/null";
+        FILE* fp = popen(cmd.c_str(), "r");
+        if (fp) {
+            char buf[4096];
+            while (fgets(buf, sizeof(buf), fp)) {
+                result += buf;
+            }
+            pclose(fp);
+        }
+    }
+
+    return result;
+}
+
+}  // namespace log_mgr

+ 150 - 0
src/log_manager.h

@@ -0,0 +1,150 @@
+/**
+ * log_manager.h - 内存日志缓冲管理系统
+ * fix24 v36: 日志写入内存,定时刷盘,Web从内存读取
+ */
+#ifndef LOG_MANAGER_H
+#define LOG_MANAGER_H
+
+#include <string>
+#include <deque>
+#include <mutex>
+#include <atomic>
+#include <thread>
+#include <condition_variable>
+
+// ==================== 日志条目结构 ====================
+struct LogEntry {
+    std::string timestamp;   // "2026-07-05 23:15:00"
+    std::string level;       // INFO / WARN / ERROR
+    std::string message;     // 日志内容(单行)
+    size_t byte_size;        // 该条目占用内存字节数
+
+    LogEntry() : byte_size(0) {}
+    LogEntry(const std::string& ts, const std::string& lv, const std::string& msg)
+        : timestamp(ts), level(lv), message(msg) {
+        byte_size = ts.size() + lv.size() + msg.size() + 32; // overhead
+    }
+};
+
+// ==================== 日志状态(Web API返回) ====================
+struct LogStatus {
+    std::string log_file_path;     // 日志文件路径
+    long file_size_mb;             // 磁盘文件大小 (MB)
+    long buffer_size_mb;           // 缓冲区上限配置 (MB)
+    double buffer_usage_percent;   // 缓冲区使用率 (%)
+    std::string flush_time;        // 定时刷盘时间
+    int retention_days;            // 保留天数
+    long line_count;               // 内存中总条数
+    std::string last_modified;     // 最后修改时间(文件/刷盘)
+    bool redirect_enabled;         // stdout 重定向是否启用
+    bool memory_enabled;           // 内存模式是否启用
+    long memory_usage_bytes;       // 当前内存占用(字节)
+    std::string last_flush_time;   // 上次刷盘时间
+    long total_flush_count;        // 累计刷盘次数
+};
+
+// ==================== 内存日志缓冲区 ====================
+class MemoryLogBuffer {
+public:
+    MemoryLogBuffer();
+    ~MemoryLogBuffer();
+
+    // 初始化(从config读取参数后调用)
+    void init(size_t max_bytes, const std::string& flush_time,
+              const std::string& log_file, int retention_days,
+              bool redirect_stdout, bool auto_flush_on_exit);
+
+    // 启动(创建刷盘线程 + stdout重定向)
+    bool start();
+
+    // 停止(刷盘 + 关闭线程)
+    void stop();
+
+    // 写入日志条目
+    void push(const std::string& level, const std::string& message);
+
+    // 获取最后N条
+    std::deque<LogEntry> tail(int n);
+
+    // 获取状态
+    LogStatus get_status();
+
+    // 手动刷盘到文件
+    bool flush_to_file(std::string& error);
+
+    // 清理过期日志
+    bool cleanup_old_logs(std::string& error);
+
+    // 信号处理调用(SIGTERM/SIGINT时调用)
+    void signal_flush();
+
+    // 全局单例
+    static MemoryLogBuffer& instance();
+
+private:
+    // 刷盘线程
+    void flush_thread_func();
+
+    // stdout读取线程(当redirect_stdout=true时)
+    void stdout_reader_func();
+
+    // 解析刷盘时间 "HH:MM" → 时、分
+    bool parse_flush_time(const std::string& time_str, int& hour, int& minute);
+
+    // 生成带日期的归档文件名
+    std::string generate_archive_filename();
+
+    // 获取当前时间字符串
+    std::string current_timestamp();
+
+    // 环形缓冲区
+    std::deque<LogEntry> buffer_;
+    size_t max_memory_bytes_;
+    size_t current_bytes_;
+    std::mutex mutex_;
+
+    // 配置
+    std::string log_file_;          // PlateRecApp.log
+    std::string flush_time_;        // "23:20"
+    int retention_days_;
+    bool redirect_stdout_;
+    bool auto_flush_on_exit_;
+    bool enabled_;                  // enabled=1 内存模式, =0 直写模式
+
+    // 刷盘线程
+    std::thread flush_thread_;
+    std::mutex flush_mutex_;
+    std::condition_variable flush_cv_;
+    std::atomic<bool> flush_running_;
+
+    // stdout重定向
+    std::thread stdout_thread_;
+    int pipe_fd_[2];                // pipefd[0]=读端, pipefd[1]=写端
+    int saved_stdout_;              // 备份原始stdout fd
+    int saved_stderr_;              // 备份原始stderr fd
+    std::atomic<bool> stdout_redirect_active_;
+
+    // 统计
+    std::atomic<long> total_flush_count_;
+    std::string last_flush_time_;
+    std::mutex stats_mutex_;
+};
+
+// ==================== 兼容旧接口(namespace log_mgr) ====================
+namespace log_mgr {
+
+// 获取日志状态
+LogStatus get_log_status();
+
+// 手动触发日志刷新(sync)
+bool flush_log(std::string &error);
+
+// 清理过期日志
+bool cleanup_old_logs(std::string &error);
+
+// 获取日志文件最后 N 行
+std::string tail_log(int lines);
+
+}  // namespace log_mgr
+
+#endif  // LOG_MANAGER_H

+ 806 - 0
src/main.cpp

@@ -0,0 +1,806 @@
+/**
+ * main.cpp — 主入口 + 调度 + 业务编排
+ * v43.2 模块化拆分
+ */
+#include "common.h"
+#include "config.h"
+#include "rtsp_capture.h"
+#include "plate_recognizer.h"
+#include "station_lock.h"
+#include "database.h"
+#include "network_client.h"
+#include "feishu_client.h"
+#include "web_server.h"
+#include "monitor.h"
+#include "weight_scale.h"
+#include "mqtt_client.h"
+#include "utils.h"
+#include "md5ex1.h"
+#include "ini.h"
+#include "auth_crypto.h"
+#include "auth_db.h"
+#include "auth_session.h"
+#include "auth_rate_limiter.h"
+#include "auth_middleware.h"
+#include "log_manager.h"
+#include "system_metrics_manager.h"
+#include <iostream>
+
+int main(int argc, char** argv)
+{
+#ifdef __linux__
+	char buf[PATH_MAX];
+	getcwd(buf, sizeof(buf));
+	if (DEBUG_LOG == 1)
+		printf("current working directory: %s\n", buf);
+	char dir[PATH_MAX] = { 0 };
+	int n = readlink("/proc/self/exe", dir, PATH_MAX - 1);
+	if (n == -1) {
+		fprintf(stderr, "readlink failed: %s\n", strerror(errno));
+		return 1;
+	}
+	dir[n] = '\0';
+	char file_path[PATH_MAX] = { 0 };
+	strncpy(file_path, dir, PATH_MAX - 1);
+	file_path[PATH_MAX - 1] = '\0';
+	char* seperator_pos = strrchr(file_path, '/');
+	char file_name[256] = { 0 };
+	if (seperator_pos == NULL) {
+		fprintf(stderr, "Not a valid file path: %s\n", file_path);
+		exit(1);
+	}
+	char* p = seperator_pos + 1;
+	int i = 0;
+	while (*p != '\0' && i < 255) {
+		file_name[i++] = *p;
+		p++;
+	}
+	file_name[255] = '\0';
+	char dirT[PATH_MAX] = { 0 };
+	size_t dir_len = strlen(dir);
+	size_t file_name_len = strlen(file_name);
+	if (dir_len > file_name_len && dir_len < PATH_MAX) {
+		memcpy(dirT, dir, dir_len - file_name_len);
+		dirT[dir_len - file_name_len] = '\0';
+	}
+	else {
+		fprintf(stderr, "路径长度异常\n");
+		return 1;
+	}
+	pathComm = dirT;
+	model_path = pathComm + "hyperlpr3/resource/models/r2_mobile";
+	std::cout << "path: " << pathComm << std::endl;
+	char filepath_config[PATH_MAX] = { 0 };
+	strncpy(filepath_config, dirT, PATH_MAX - 1);
+	char cpu_serial[32] = { 0 };
+	char computed_md5[33] = { 0 };
+	char stored_md5[33] = { 0 };
+	const char* config_file = (argc > 1) ? argv[1] : "config.txt";
+	snprintf(filepath_config + strlen(filepath_config),
+		PATH_MAX - strlen(filepath_config), "%s", config_file);
+	if (read_cpu_serial(cpu_serial, sizeof(cpu_serial)) != 0) {
+		fprintf(stderr, "错误: 无法读取CPU序列号\n");
+		return 1;
+	}
+	md5_tripleEx1(cpu_serial, computed_md5);
+	if (read_config_md5(filepath_config, stored_md5, sizeof(stored_md5)) != 0) {
+		fprintf(stderr, "错误: 无法读取配置文件MD5校验值\n");
+		return 1;
+	}
+	if (strcmp(computed_md5, stored_md5) != 0) {
+		fprintf(stderr, "错误: 配置文件校验失败,请检查config.txt\n");
+		fprintf(stderr, "计算值: %s\n", computed_md5);
+		fprintf(stderr, "存储值: %s\n", stored_md5);
+		return 1;
+	}
+	//parse_config_ini(filepath_config);
+
+  char config_ini_path[PATH_MAX] = { 0 };
+	snprintf(config_ini_path, PATH_MAX, "%sconfig.ini", dirT);
+	parse_config_ini(config_ini_path);
+	g_config_ini_path = config_ini_path;  // ✅ fix18: 保存config.ini路径用于ROI写回
+	load_special_plates(std::string(config_ini_path));
+#endif
+
+	signal(SIGINT, signal_handler);
+	signal(SIGTERM, signal_handler);
+
+	// ✅ fix24-v14: 计算本地时区偏移(秒)
+	// SQLite的strftime('%s', datetime_str)将字符串视为UTC,
+	// 但create_time使用datetime('now','localtime')存储的是本地时间,
+	// 需要校正时区偏移才能使ORDER BY正确排序
+	{
+		time_t now_utc = time(NULL);
+		struct tm* utc_tm = gmtime(&now_utc);
+		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 << " 秒 ("
+		          << g_tz_offset_seconds / 3600 << " 小时)" << std::endl;
+	}
+
+	lib_curl_init();
+
+	// ✅ fix24-v29: 修复 g_sys_db_path 默认值(确保带 pathComm 前缀)
+	if (g_sys_db_path.empty() || g_sys_db_path == "data/system_metrics.db") {
+		g_sys_db_path = pathComm + "data/system_metrics.db";
+	}
+
+	// ✅ fix24-v29: 确保 data/ 目录存在(数据库统一放到 data/ 下)
+	{
+		std::string data_dir = pathComm + "data";
+		std::string cmd = "mkdir -p " + data_dir;
+		system(cmd.c_str());
+	}
+
+	// ✅ fix24-v29: 旧数据库自动迁移(upload_records.db → data/upload_records.db)
+	{
+		std::string old_db = pathComm + "upload_records.db";
+		std::string new_db = pathComm + "data/upload_records.db";
+		struct stat st_old, st_new;
+		if (stat(old_db.c_str(), &st_old) == 0 && stat(new_db.c_str(), &st_new) != 0) {
+			std::cout << "[数据库] 迁移旧数据库: " << old_db << " → " << new_db << std::endl;
+			std::string cp_cmd = "cp " + old_db + " " + new_db;
+			system(cp_cmd.c_str());
+			std::cout << "[数据库] 迁移完成,旧文件保留为备份" << std::endl;
+		}
+	}
+
+	if (init_database() != 0) {
+		std::cerr << "数据库初始化失败,程序退出" << std::endl;
+		return 1;
+	}
+/*
+	if (db_self_test() != 0) {
+		std::cerr << "数据库自测失败,程序退出" << std::endl;
+		close_database();
+		return 1;
+	}*/
+
+	// ✅ v43.2修复:在init_database释放g_db_mtx后再加载缓存,避免与increment_in/out_photo_count的锁序死锁
+	db_load_tw_cache();
+	load_recent_tbnums_from_db();
+	// ✅ v42新增:从数据库恢复交替锁定状态
+	load_station_cache_from_db();
+	//load_special_plates(pathComm + "config.txt");
+
+	if (hyperlpr_lib_init(model_path) != 0) {
+		std::cerr << "HyperLPR初始化失败,程序退出" << std::endl;
+		close_database();
+		return 1;
+	}
+
+	rstp_capture_info_init();
+
+	// ✅ fix24-v8: 共享摄像头模式检测
+	// 当进站和出站前向摄像头URL相同时,启用共享模式:
+	// 只创建一个摄像头连接,根据交替锁定状态自动判断车牌属于进站还是出站
+	if (!rtsp_url_front_out.empty() && rtsp_url_front_in == rtsp_url_front_out) {
+		g_shared_capture_mode = true;
+		std::cout << "[共享摄像头] 检测到进出站前向摄像头URL相同,启用共享模式" << std::endl;
+		std::cout << "[共享摄像头] 将根据交替锁定状态自动判断车牌方向(进站/出站)" << std::endl;
+	}
+
+	// ✅ v43修改:传入DecodeMode/DRM设备/RTSP传输协议
+	plate_rec_app.capture_front_in = new RTSPCapture(rtsp_url_front_in, g_hw_decode_mode, g_hw_decode_device, g_rtsp_transport);
+	if (!g_shared_capture_mode) {
+		plate_rec_app.capture_front_out = new RTSPCapture(rtsp_url_front_out, g_hw_decode_mode, g_hw_decode_device, g_rtsp_transport);
+	}
+	plate_rec_app.capture_side_in = new RTSPCapture(rtsp_url_side_in, g_hw_decode_mode, g_hw_decode_device, g_rtsp_transport);
+	plate_rec_app.capture_side_out = new RTSPCapture(rtsp_url_side_out, g_hw_decode_mode, g_hw_decode_device, g_rtsp_transport);
+
+	plate_rec_app.capture_front_in->start();
+	if (!g_shared_capture_mode) {
+		plate_rec_app.capture_front_out->start();
+	}
+	plate_rec_app.capture_side_in->start();
+	plate_rec_app.capture_side_out->start();
+
+	std::thread capture_thread(capture_do_work_thread);
+
+	// ✅ fix24 功能一:初始化登录认证系统
+	if (g_auth_enabled) {
+		// 创建认证数据库
+		g_auth_db = new auth::AuthDB();
+		std::string auth_db_path;
+		if (!g_auth_db_path.empty()) {
+			// 使用 config.ini [auth] db_path 配置的显式路径
+			auth_db_path = g_auth_db_path;
+		} else {
+			// 默认使用 data/auth.db
+			auth_db_path = pathComm + "data/auth.db";
+		}
+		// data/ 目录已在 init_database 前创建
+		// ✅ fix24-v29: 旧认证数据库自动迁移(upload_records_auth.db → data/auth.db)
+		{
+			std::string old_auth_db = pathComm + "upload_records_auth.db";
+			struct stat st_old_auth, st_new_auth;
+			if (stat(old_auth_db.c_str(), &st_old_auth) == 0 && stat(auth_db_path.c_str(), &st_new_auth) != 0) {
+				std::cout << "[auth] 迁移旧认证数据库: " << old_auth_db << " → " << auth_db_path << std::endl;
+				std::string cp_cmd = "cp " + old_auth_db + " " + auth_db_path;
+				system(cp_cmd.c_str());
+				std::cout << "[auth] 认证数据库迁移完成,旧文件保留为备份" << std::endl;
+			}
+		}
+		if (!g_auth_db->init(auth_db_path)) {
+			std::cerr << "[auth] 认证数据库初始化失败,认证系统禁用" << std::endl;
+			g_auth_enabled = false;
+			delete g_auth_db;
+			g_auth_db = nullptr;
+		} else {
+			// 检查是否有默认管理员,没有则创建
+			auth::User admin_user;
+			if (!g_auth_db->get_user_by_name("admin", admin_user)) {
+				std::string admin_hash = auth::bcrypt_hash("zhongjin188A");
+				if (!admin_hash.empty()) {
+					g_auth_db->create_user("admin", admin_hash, 2);  // role=2 superadmin
+					std::cout << "[auth] 默认管理员账户已创建 (admin/zhongjin188A)" << std::endl;
+				}
+			}
+			// 检查是否有普通用户 zhonjin,没有则创建
+			auth::User zhonjin_user;
+			if (!g_auth_db->get_user_by_name("zhonjin", zhonjin_user)) {
+				std::string zhonjin_hash = auth::bcrypt_hash("zhonjin");
+				if (!zhonjin_hash.empty()) {
+					g_auth_db->create_user("zhonjin", zhonjin_hash, 0);  // role=0 普通用户
+					std::cout << "[auth] 默认普通用户已创建 (zhonjin/zhonjin)" << std::endl;
+				}
+			}
+
+			// 创建会话管理器
+			g_session_mgr = new auth::SessionManager();
+			g_session_mgr->init(g_auth_db, 1800, 28800, 604800);  // idle=30min, absolute=8h, remember=7d
+
+			// 创建IP限流器
+			g_rate_limiter = new auth::RateLimiter(5, 300);  // 5次/5分钟
+
+			// 创建认证中间件
+			g_auth_middleware = new auth::AuthMiddleware();
+			g_auth_middleware->init(g_session_mgr, g_rate_limiter);
+
+			std::cout << "[auth] 认证系统初始化完成" << std::endl;
+		}
+	}
+
+	// ✅ fix24-v36: 初始化内存日志缓冲系统
+	if (g_log_enabled) {
+		MemoryLogBuffer::instance().init(
+			(size_t)g_log_buffer_size_mb * 1024 * 1024,  // buffer_size_mb → bytes
+			g_log_flush_time,
+			g_log_file,
+			g_log_retention_days,
+			g_log_redirect_stdout,
+			g_log_auto_flush_on_exit
+		);
+		MemoryLogBuffer::instance().start();
+		std::cout << "[log_mgr] ✅ 内存日志缓冲系统已启动" << std::endl;
+	} else {
+		std::cout << "[log_mgr] 内存日志模式禁用,使用传统直写模式" << std::endl;
+	}
+
+	// ✅ fix24-v38: 初始化系统监控历史数据记录(内存缓冲+每日定时写盘)
+	MetricsManager::instance().init(
+		g_sys_db_path,
+		g_sys_monitor_interval,
+		g_sys_flush_time,
+		g_sys_history_retention_days,
+		g_sys_aggregation_retention_days,
+		g_sys_temp_alert_threshold,
+		g_sys_cpu_alert_threshold
+	);
+	MetricsManager::instance().start();
+
+	start_web_server();
+
+	// ✅ fix20: 初始化称重系统 + MQTT客户端 + 异步worker
+	mqtt_client_init();
+	weight_scale_init();
+	start_weight_mqtt_worker();
+
+	time_t last_online_report = 0;
+	time_t last_cleanup = 0;
+	time_t last_db_health_check = 0;
+	time_t last_daily_cleanup = 0;
+	time_t last_file_cleanup = 0;  // ✅ fix18: 文件清理时间戳
+
+	std::cout << "✅ 车牌识别系统 v43.2 fix19 容量照片清理+代码日志清理+P0修复版 启动完成" << std::endl;
+	std::cout << "   项目名称: " << project_name << std::endl;
+	std::cout << "   工地编号: " << point_number << std::endl;
+	std::cout << "   出入口: " << throughway << std::endl;
+	std::cout << "   测试模式: " << (TestFlag == 1 ? "开启" : "关闭") << std::endl;
+	// ✅ fix24-v16: 使用可配置端口
+	std::cout << "   Web管理端口: " << g_web_server_port << std::endl;
+	std::cout << "   共享摄像头模式: " << (g_shared_capture_mode ? "启用(进出站共用同一摄像头,自动判断方向)" : "禁用(进出站使用独立摄像头)") << std::endl;
+	std::cout << "   ---- v43配置 ----" << std::endl;
+	std::cout << "   交替锁定(AlternatingMerge): " << (g_alternating_merge_enabled ? "启用" : "禁用") << std::endl;
+	std::cout << "   交替锁定间隔(in_out_interval): " << (g_in_out_interval_sec / 60) << " 分钟(" << g_in_out_interval_sec << "秒)" << std::endl;
+	std::cout << "   TIME_WINDOW: " << g_time_window_min << " 分钟(" << (g_time_window_min * 60) << "秒)" << std::endl;
+	if (g_alternating_merge_enabled) {
+		std::cout << "   [交替锁定] 进站→出站等待: " << g_in_out_interval_sec << "秒(" << (g_in_out_interval_sec/60) << "分钟)" << std::endl;
+		std::cout << "   [交替锁定] 出站→进站等待: " << (g_in_out_interval_sec + g_time_window_min * 60) << "秒(" << ((g_in_out_interval_sec + g_time_window_min * 60)/60) << "分钟)" << std::endl;
+		std::cout << "   [交替锁定] 一直等待: 对端操作完成前保持锁定,超2小时自动清零" << std::endl;
+		std::cout << "   [交替锁定] 出站无进站记录: 永远拦截,必须先进站" << std::endl;
+		std::cout << "   [交替锁定] TIME_WINDOW独立限流: 禁用(由交替锁定独占控制)" << std::endl;
+	} else {
+		std::cout << "   [仅TIME_WINDOW] 限流间隔: " << (g_time_window_min * 60) << "秒(" << g_time_window_min << "分钟)" << std::endl;
+	}
+	std::cout << "   解码模式: " << (g_hw_decode_mode == DecodeMode::AUTO ? "AUTO" : g_hw_decode_mode == DecodeMode::DRM ? "DRM硬解" : "SOFT软解") << std::endl;
+	std::cout << "   DRM设备: " << g_hw_decode_device << std::endl;
+	std::cout << "   RTSP传输: " << g_rtsp_transport << std::endl;
+	std::cout << "   ROI进站: " << g_roi_in.toString() << std::endl;
+	std::cout << "   ROI出站: " << g_roi_out.toString() << std::endl;
+	std::cout << "   ROI调试: " << (g_roi_debug_enabled ? "开启" : "关闭") << std::endl;
+	std::cout << "   紧急告警抑制(WarningSigns): " << (g_suppress_emergency_alert ? "启用(1=抑制)" : "禁用(0=正常发送)") << std::endl;
+	std::cout << "   照片容量阈值(PhotoMaxCapacityMB): " << g_photo_max_capacity_mb << " MB" << std::endl;
+	std::cout << "   日志保留天数(LogRetentionDays): " << g_log_retention_days << " 天" << std::endl;
+	std::cout << "   修复内容: fix19-容量照片清理+代码日志清理 + fix18-2小时超时清零+4路独立视频+ROI手动绘制+P0重复照片修复 + fix17~fix10 + ROI + FFmpeg硬解 + WarningSigns + Web增强重试" << std::endl;
+	std::cout << "========================================" << std::endl;
+
+	while (g_running) {
+		auto loop_start = std::chrono::steady_clock::now();
+
+		time_t now = time(NULL);
+		std::vector<std::shared_ptr<CarPlateInfo>> to_process;
+
+		// 优先处理独立重试队列(不触发照片保存)
+		{
+			std::lock_guard<std::mutex> lock(g_retry_queue_mtx);
+			while (!g_retry_queue.empty()) {
+				auto plate_info = g_retry_queue.front();
+				g_retry_queue.pop_front();
+				
+				if (plate_info->retry_count >= MAX_RETRY_COUNT) {
+					std::cerr << "[永久失败] " << plate_info->code << " 已重试" << MAX_RETRY_COUNT << "次,放弃" << std::endl;
+					send_permanent_failure_alert(plate_info->code,
+						plate_info->cap_info_copy->dtype == STATION_IN,
+						plate_info->retry_count, plate_info->db_id);
+					continue;
+				}
+				
+				// ✅ fix13: 预检is_blocked(),避免浪费retry_count
+				// 当is_blocked()为true时,上传必然返回BLOCKED,浪费重试次数
+				// 跳过本次重试,放回队尾,等is_blocked()过期后再重试
+				bool is_blocked_now = false;
+				if (plate_info->is_inbound) {
+					is_blocked_now = g_in_plate_status.is_blocked(plate_info->code);
+				} else {
+					is_blocked_now = g_out_plate_status.is_blocked(plate_info->code);
+				}
+				if (is_blocked_now) {
+					// 放回队尾,不递增retry_count,下次主循环再检查
+					g_retry_queue.push_back(plate_info);
+					// 只检查一次,避免阻塞主循环
+					break;
+				}
+				
+				plate_info->retry_count++;
+				std::cout << "[重试] " << plate_info->code << " 第" << plate_info->retry_count << "次重试" << std::endl;
+				
+				if (plate_info->db_id > 0) {
+					db_increment_retry_count(plate_info->db_id);
+				}
+				
+				// 启动上传线程
+				std::thread upload_thread([plate_info]() {
+					g_active_detach_threads++;
+					int result = plate_cap_info_upload(plate_info);
+					
+					if (result == static_cast<int>(ErrorCode::SUCCESS)) {
+						std::cout << "✅ " << plate_info->code << " 重试成功" << std::endl;
+						// ✅ v43修复Bug M-03:删除重试成功后的冗余锁定调用
+						// upload_in/out_photos成功后已调用lock_in/out_station,无需重复
+					} else if (result == static_cast<int>(ErrorCode::BUSINESS_ERROR)) {
+						// 业务错误不重试
+						std::cerr << "[业务拒绝] " << plate_info->code << " 不进行重试" << std::endl;
+					} else {
+						// 网络错误,重新加入重试队列
+						std::lock_guard<std::mutex> lock(g_retry_queue_mtx);
+						g_retry_queue.push_back(plate_info);
+					}
+					g_active_detach_threads--;
+				});
+				upload_thread.detach();
+			}
+		}
+		
+		// 收集需要处理的车牌(首次识别)
+		// ✅ P0修复:不再立即erase已处理的车牌,保留在map中防止同一辆车被当作新车处理
+		{
+			std::lock_guard<std::mutex> lock(plate_rec_app.map_mtx);
+			for (auto it = plate_rec_app.map.begin(); it != plate_rec_app.map.end();) {
+				auto& plate_info = it->second;
+				
+				// ✅ 核心修复:检查是否超过了TIME_WINDOW(基于上次创建工单时间)
+				if (plate_info->photo_saved) {
+					// ✅ fix18-P0: 上传进行中不检查peek,防止竞态条件导致重复保存照片
+					if (plate_info->upload_in_progress.load()) {
+						++it;
+						continue;
+					}
+
+					// ✅ fix15: anti-spin时长根据BLOCKED来源+进/出站方向选择
+					// 出站→进站等in_out_interval+TIME_WINDOW(600秒),进站→出站等in_out_interval(300秒)
+					if (plate_info->last_station_blocked_time > 0) {
+						int anti_spin_sec;
+						if (plate_info->blocked_by_station_lock) {
+							// ✅ fix24-v7: anti-spin改为30秒短防抖,不再使用完整等待时长
+							// 旧逻辑:anti-spin=in_out_interval+TIME_WINDOW*60(600秒),导致车辆到达后额外等待600秒
+							// 新逻辑:30秒短防抖,交替锁定的时序逻辑已正确处理等待时间
+							anti_spin_sec = 30;
+						} else {
+							anti_spin_sec = BLOCK_TIMEOUT_SEC;
+						}
+						if (difftime(now, plate_info->last_station_blocked_time) < anti_spin_sec) {
+							++it;
+							continue;
+						}
+					}
+
+					if (g_alternating_merge_enabled) {
+						// ✅ fix14: AlternatingMerge=1时禁用TIME_WINDOW,交替锁定独占时序控制
+						// 只检查交替锁定状态:peek允许→重置photo_saved,否则继续等待
+						// 时序完全由交替锁定决定:进站完成→等interval→出站→等interval→进站→...
+						if (peek_station_lock(plate_info->code, plate_info->is_inbound)) {
+							// ✅ fix24-P0: 上一次上传尚未完成时,禁止重置状态
+							// 否则上一轮上传完成后立刻被peek重新激活→同一帧多次保存+上传
+							if (plate_info->upload_in_progress.load()) {
+								++it;
+								continue;
+							}
+							// ✅ fix24-v6: 上传完成后冷却期,防止刚上传完就被peek重置导致同一辆车重复处理
+							// 冷却时长=in_out_interval_sec(与交替锁定间隔一致,确保对端操作有足够时间完成)
+							if (plate_info->upload_complete_time > 0 && 
+								difftime(now, plate_info->upload_complete_time) < g_in_out_interval_sec) {
+								++it;
+								continue;
+							}
+							// 交替锁定允许新操作(对端操作完成+等待interval已满),重置处理状态
+							std::cout << "[交替锁定重置] " << plate_info->code 
+							          << (plate_info->is_inbound ? "进站" : "出站") 
+							          << " 交替锁定允许新操作,重置处理状态" << std::endl;
+							plate_info->photo_saved = false;
+							plate_info->first_seen_time = now;
+							plate_info->count = 0;
+							plate_info->tb_num = "";
+							plate_info->db_id = 0;
+							plate_info->last_station_blocked_time = 0;
+							// ✅ fix14: 同步重置bill cache,确保新周期能创建新工单
+							reset_bill_cache_for_plate(plate_info->code, plate_info->is_inbound);
+						} else {
+							// 交替锁定仍阻止(对端操作未完成或等待interval未满),标记拦截
+							plate_info->last_station_blocked_time = now;
+							plate_info->blocked_by_station_lock = true;
+							plate_info->first_seen_time = now; // 刷新防止被PLATE_CLEANUP_TIMEOUT_SEC清理
+							// ✅ fix17: 交替锁定一直等待对端操作完成(但不超过24小时)
+							// 旧逻辑:超过600秒视为车辆已离开,清理map → 破坏"一直等待"
+							// 新逻辑:一直等待对端操作完成,超过24小时由cleanup_station_cache清零
+						}
+					} else {
+						// ✅ AlternatingMerge=0: TIME_WINDOW控制(原有逻辑不变)
+						time_t last_bill_time = 0;
+						{
+							bool is_inbound = plate_info->is_inbound;
+							auto& record_mtx = is_inbound ? g_in_record_mtx : g_out_record_mtx;
+							auto& upload_records = is_inbound ? g_in_upload_records : g_out_upload_records;
+							
+							std::lock_guard<std::mutex> lock(record_mtx);
+							auto rec_it = upload_records.find(plate_info->code);
+							if (rec_it != upload_records.end()) {
+								last_bill_time = rec_it->second.last_time > 0 ? rec_it->second.last_time : rec_it->second.create_time;
+							}
+						}
+						
+						if (last_bill_time == 0) {
+							last_bill_time = plate_info->first_seen_time;
+						}
+						
+						if (last_bill_time > 0 && difftime(now, last_bill_time) >= g_time_window_min * 60) {
+							// ✅ fix24-P0: 上传进行中禁止重置,防止重复保存
+							if (plate_info->upload_in_progress.load()) {
+								++it;
+								continue;
+							}
+							// ✅ fix24-v6: 上传完成后冷却期,防止刚上传完就被重置导致同一辆车重复处理
+							if (plate_info->upload_complete_time > 0 && 
+								difftime(now, plate_info->upload_complete_time) < g_in_out_interval_sec) {
+								++it;
+								continue;
+							}
+							if (difftime(now, plate_info->first_seen_time) > PLATE_CLEANUP_TIMEOUT_SEC) {
+								std::cout << "[清理] 周期重置但已超时: " << plate_info->code << std::endl;
+								it = plate_rec_app.map.erase(it);
+								continue;
+							}
+							
+							std::cout << "[周期重置] " << plate_info->code << " 距上次工单" 
+							          << (int)difftime(now, last_bill_time) << "秒超过" << g_time_window_min * 60 << "秒,重置处理状态" << std::endl;
+							plate_info->photo_saved = false;
+							plate_info->first_seen_time = now;
+							plate_info->count = 0;
+							plate_info->tb_num = "";
+							plate_info->db_id = 0;
+							plate_info->last_station_blocked_time = 0;
+						}
+					}
+					++it;
+					continue;
+				}
+
+				if (plate_info->count >= 2 && 
+					difftime(now, plate_info->first_seen_time) > 2) {
+					// ✅ fix18-P0: 上传进行中不处理,防止竞态
+					if (plate_info->upload_in_progress.load()) {
+						++it;
+						continue;
+					}
+					// ✅ fix11: 交替锁定拦截后跳过处理,避免CPU空转
+					// ✅ fix15: anti-spin时长根据BLOCKED来源+进/出站方向选择
+					// 出站→进站等in_out_interval+TIME_WINDOW(600秒),进站→出站等in_out_interval(300秒)
+					if (plate_info->last_station_blocked_time > 0) {
+						int anti_spin_sec;
+						if (plate_info->blocked_by_station_lock) {
+							// ✅ fix24-v7: anti-spin改为30秒短防抖
+							anti_spin_sec = 30;
+						} else {
+							anti_spin_sec = BLOCK_TIMEOUT_SEC;
+						}
+						if (difftime(now, plate_info->last_station_blocked_time) < anti_spin_sec) {
+							++it;
+							continue;
+						}
+					}
+					to_process.push_back(plate_info);
+					// ❌ Bug修复:不要在这里设置 photo_saved = true
+					// photo_saved 应该在照片真正保存成功后,由 plate_cap_info_save_file 设置
+					++it;
+				} else if (difftime(now, plate_info->first_seen_time) > PLATE_CLEANUP_TIMEOUT_SEC) {
+					// ✅ 修复:超时时直接清理,不再重置 photo_saved
+					// 修复前:重置 photo_saved = false 导致同一辆车被重新处理
+					// 修复后:直接erase,如果车辆再次出现会被识别线程重新创建
+					std::cout << "[清理] 超时车牌: " << plate_info->code << std::endl;
+					it = plate_rec_app.map.erase(it);
+				} else {
+					++it;
+				}
+			}
+		}
+
+		// 处理车牌
+		for (auto& plate_info : to_process) {
+			if (!g_running) break;
+			
+			// ✅ fix14: AlternatingMerge=1时交替锁定预检(TIME_WINDOW已禁用)
+			// 交替锁定独占时序控制,在保存照片前先预检
+			// 如果交替锁定会拦截则跳过(避免磁盘IO浪费+photo_count递增无上传)
+			if (g_alternating_merge_enabled && plate_info->retry_count == 0) {
+				if (!peek_station_lock(plate_info->code, plate_info->is_inbound)) {
+					plate_info->last_station_blocked_time = time(NULL);
+					plate_info->blocked_by_station_lock = true;
+					plate_info->photo_saved = false;
+					// ✅ fix24-v7: 显示剩余等待时间
+					int remaining = get_remaining_wait_time(plate_info->code, plate_info->is_inbound);
+					if (remaining < 0) {
+						std::cout << "[交替锁定预检] " << plate_info->code 
+						          << (plate_info->is_inbound ? "进站" : "出站") 
+						          << " 被拦截(需先完成对端操作),跳过照片保存" << std::endl;
+					} else {
+						std::cout << "[交替锁定预检] " << plate_info->code 
+						          << (plate_info->is_inbound ? "进站" : "出站") 
+						          << " 被拦截,剩余等待" << remaining << "秒"
+						          << "(" << (remaining/60) << "分" << (remaining%60) << "秒)"
+						          << ",跳过照片保存" << std::endl;
+					}
+					continue;
+				}
+			}
+			
+			plate_cap_info_save_file(plate_info);
+			if (plate_info->pic_path_front.empty() || plate_info->pic_path_side.empty()) {
+				std::cerr << "[跳过] 图片保存失败,车牌: " << plate_info->code << std::endl;
+				continue;
+			}
+
+			// ✅ fix18-P0: 设置上传进行中标志,防止主循环peek竞态
+			plate_info->upload_in_progress.store(true);
+
+			// 异步处理上传
+			std::thread upload_thread([plate_info]() {
+				g_active_detach_threads++;
+				int result = plate_cap_info_upload(plate_info);
+				
+				if (result == static_cast<int>(ErrorCode::SUCCESS)) {
+					std::cout << "✅ " << plate_info->code << " 处理完成" << std::endl;
+					// ✅ fix20: 上传成功后提交称重+MQTT任务
+					if (!plate_info->tb_num.empty()) {
+						int st = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN) ? 1 : 2;
+						enqueue_weight_mqtt_task(plate_info->code, st, plate_info->tb_num);
+					}
+				} else if (result == static_cast<int>(ErrorCode::PERMANENT_FAILURE)) {
+					std::cerr << "❌ " << plate_info->code << " 永久失败,已发送告警" << std::endl;
+				} else if (result == static_cast<int>(ErrorCode::BUSINESS_ERROR)) {
+					// ✅ P0修复:业务错误(-2数据重复、255进站不存在)不重试,直接放弃
+					std::cerr << "[业务拒绝] " << plate_info->code << " 错误码: " << result 
+							  << ",不进行重试" << std::endl;
+					// 照片已保存,永久失败只是上传失败,不影响照片保存状态
+					g_metrics.record_permanent_failure();
+					send_permanent_failure_alert(plate_info->code, 
+						plate_info->cap_info_copy->dtype == STATION_IN, 
+						plate_info->retry_count, plate_info->db_id);
+				} else if (result == static_cast<int>(ErrorCode::BLOCKED)) {
+					// ✅ fix18-P0: 被交替锁定拦截时不重置photo_saved,防止重复保存照片
+					// 旧逻辑:photo_saved=false → 主循环重新保存照片 → 竞态条件 → 每隔3秒重复保存
+					// 新逻辑:photo_saved保持true → 依赖anti-spin机制防止重复处理
+					plate_info->last_station_blocked_time = time(NULL);
+					// photo_saved 保持 true,不重置!
+					if (plate_info->blocked_by_station_lock) {
+						std::cout << "[交替锁定拦截] " << plate_info->code 
+						          << " 已标记拦截,等待交替锁定释放" << std::endl;
+					} else {
+						std::cout << "[上传失败拦截] " << plate_info->code 
+						          << " 连续上传失败被阻止," << BLOCK_TIMEOUT_SEC 
+						          << "秒后重试" << std::endl;
+					}
+				} else {
+					// ✅ P0修复:使用独立重试队列,不干扰主循环的照片保存逻辑
+					// 照片已保存,重试只是网络上传,不重新保存照片
+					std::lock_guard<std::mutex> lock(g_retry_queue_mtx);
+					g_retry_queue.push_back(plate_info);
+					std::cout << "[重试] " << plate_info->code << " 已加入重试队列" << std::endl;
+				}
+				
+				// ✅ fix18-P0: 上传完成,清除进行中标志(必须最后执行)
+				// ✅ fix24-v6: 记录上传完成时间,防止peek冷却期内重复处理
+				plate_info->upload_complete_time = time(NULL);
+				plate_info->upload_in_progress.store(false);
+				g_active_detach_threads--;
+			});
+			upload_thread.detach();
+		}
+
+		// 在线状态上报
+		if (difftime(now, last_online_report) >= INTERVAL_SECONDS) {
+			std::cout << "[在线状态] 上报4个摄像头状态..." << std::endl;
+			// v41.2修复: 使用批量接口,1次请求代替4次,减少网络压力
+			//int batch_result = lib_curl_online_status_batch_request();//bug注释掉
+			//if (batch_result != 0) {
+				// 降级:批量失败时尝试单独上报
+				lib_curl_online_status_request(&plate_rec_app.front_info_in);
+				lib_curl_online_status_request(&plate_rec_app.front_info_out);
+				lib_curl_online_status_request(&plate_rec_app.side_info_in);
+				lib_curl_online_status_request(&plate_rec_app.side_info_out);
+			//}
+			last_online_report = now;
+		}
+
+		// 状态清理
+		if (difftime(now, last_cleanup) >= STATUS_CLEANUP_INTERVAL_SEC) {
+			std::cout << "[状态清理] 清理过期状态..." << std::endl;
+			g_in_plate_status.cleanup_expired();
+			g_out_plate_status.cleanup_expired();
+			cleanup_plate_last_processed(true);
+			cleanup_plate_last_processed(false);
+			// ✅ v42新增:交替锁定缓存清理
+			cleanup_station_cache();
+			
+			{
+				std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+				cleanup_bill_cache_unlocked(true);
+			}
+			{
+				std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+				cleanup_bill_cache_unlocked(false);
+			}
+			
+			{
+				std::lock_guard<std::mutex> lock(g_in_record_mtx);
+				cleanup_expired_upload_records_unlocked(true);
+			}
+			{
+				std::lock_guard<std::mutex> lock(g_out_record_mtx);
+				cleanup_expired_upload_records_unlocked(false);
+			}
+			
+			last_cleanup = now;
+		}
+
+		// 数据库健康检查
+		if (difftime(now, last_db_health_check) >= DB_HEALTH_CHECK_INTERVAL_SEC) {
+			if (!check_db_health()) {
+				std::cerr << "[严重] 数据库健康检查失败,尝试重新连接..." << std::endl;
+				close_database();
+				if (init_database() != 0) {
+					std::cerr << "[致命] 数据库重连失败" << std::endl;
+				} else {
+					std::cout << "✅ 数据库重连成功" << std::endl;
+				}
+			}
+			last_db_health_check = now;
+		}
+
+		// 每日数据库清理和优化
+		struct tm tm_now;
+		localtime_r(&now, &tm_now);
+		if (tm_now.tm_hour == DAILY_CLEANUP_HOUR && 
+			tm_now.tm_min == DAILY_CLEANUP_MINUTE && 
+			difftime(now, last_daily_cleanup) >= 86400) {
+			std::cout << "[每日清理] 执行数据库每日清理..." << std::endl;
+			db_cleanup_old_records();
+			db_vacuum();
+			last_daily_cleanup = now;
+		}
+
+		// ✅ fix19重写:凌晨2点文件清理(照片+日志)
+		if (tm_now.tm_hour == FILE_CLEANUP_HOUR &&
+			tm_now.tm_min == FILE_CLEANUP_MINUTE &&
+			difftime(now, last_file_cleanup) >= 86400) {
+			std::cout << "[每日清理] 执行文件清理(照片+日志)..." << std::endl;
+			daily_file_cleanup();
+			// fix24 v38: 内存日志模式下,旧版copytruncate日志清理不再需要
+			// MemoryLogBuffer::cleanup_old_logs() 在23:20刷盘时已处理过期日志
+			if (!g_log_enabled) {
+				daily_log_cleanup();
+			} else {
+				std::cout << "[每日清理] 内存日志模式已启用,跳过旧版日志copytruncate" << std::endl;
+			}
+			last_file_cleanup = now;
+		}
+
+		// 计算主循环耗时
+		auto loop_end = std::chrono::steady_clock::now();
+		long long loop_duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(loop_end - loop_start).count();
+		g_metrics.record_main_loop_block(loop_duration_ms);
+
+		// 主循环休眠
+		std::this_thread::sleep_for(std::chrono::milliseconds(100));
+	}
+
+	// 优雅退出
+	std::cout << "\n开始优雅退出..." << std::endl;
+	g_running = false;
+
+	// 等待所有上传线程完成
+	std::cout << "等待上传线程完成..." << std::endl;
+	while (g_active_detach_threads > 0) {
+		std::this_thread::sleep_for(std::chrono::milliseconds(100));
+	}
+
+	// 停止服务
+	// ✅ fix24 功能一:清理认证系统
+	if (g_auth_middleware) { delete g_auth_middleware; g_auth_middleware = nullptr; }
+	if (g_rate_limiter) { delete g_rate_limiter; g_rate_limiter = nullptr; }
+	if (g_session_mgr) { delete g_session_mgr; g_session_mgr = nullptr; }
+	if (g_auth_db) { g_auth_db->close(); delete g_auth_db; g_auth_db = nullptr; }
+
+	stop_web_server();
+
+	// ✅ fix24-v36: 停止内存日志缓冲(恢复stdout + 最后一次刷盘)
+	if (g_log_enabled) {
+		MemoryLogBuffer::instance().stop();
+		std::cout << "[log_mgr] 内存日志缓冲已停止" << std::endl;
+	}
+
+	// ✅ fix24-v37: 停止系统监控历史数据采集
+	MetricsManager::instance().stop();
+
+	stop_weight_mqtt_worker();
+	weight_scale_close();
+	mqtt_client_close();
+	
+	if (capture_thread.joinable()) {
+		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;
+
+	hyperlpr_lib_deinit();
+	close_database();
+	lib_curl_deinit();
+
+	std::cout << "✅ 车牌识别系统已安全退出" << std::endl;
+	g_metrics.print_report();
+
+	return 0;
+}
+

+ 4 - 0
src/monitor.cpp

@@ -0,0 +1,4 @@
+/**
+ * monitor.cpp — 监控指标实现
+ */
+#include "monitor.h"

+ 4 - 0
src/monitor.h

@@ -0,0 +1,4 @@
+#ifndef MONITOR_H
+#define MONITOR_H
+#include "common.h"
+#endif

+ 217 - 0
src/mqtt_client.cpp

@@ -0,0 +1,217 @@
+/**
+ * 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;
+    }
+
+    // ✅ fix24-v22: publish()成功即视为成功,消息已交给mosquitto库投递
+    // 旧逻辑:等待ACK超时则返回false → worker重试3次 → broker收到3份相同消息(P1 Bug)
+    // 新逻辑:publish()返回rc=0即return true,ACK仅做日志记录
+    // 等待ACK(最多5秒,仅用于日志诊断)
+    for (int i = 0; i < 50 && !g_mqtt_client->acked.load() && g_running; i++)
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+    if (!g_mqtt_client->acked.load()) {
+        std::cout << "[MQTT] publish成功但ACK超时(5s),消息已由mosquitto库投递" << std::endl;
+    }
+
+    return true;  // publish()成功 = 消息已入队,由mosquitto库保证投递
+}
+
+void mqtt_client_close() {
+    if (g_mqtt_client) {
+        delete g_mqtt_client;
+        g_mqtt_client = nullptr;
+    }
+    mosqpp::lib_cleanup();
+    std::cout << "[MQTT] 已关闭" << std::endl;
+}
+int mqtt_get_status() {
+    if (!g_mqtt_enabled || !g_mqtt_client) return 0; // 未启用/未初始化
+    MqttState st = g_mqtt_client->current_state.load();
+    if (st == MqttState::STABLE) return 1;   // 已连接(稳定)
+    if (st == MqttState::CONNECTED) return 1; // 已连接
+    if (st == MqttState::CONNECTING) return 2; // 连接中
+    return 3; // 断开
+}

+ 36 - 0
src/mqtt_client.h

@@ -0,0 +1,36 @@
+/**
+ * mqtt_client.h — MQTT客户端模块(fix20完整实现)
+ * 从 plate_log2mqtt.cpp 完整迁移 F13-F15 功能
+ * 包括:mosquittopp连接管理、自动重连、消息发布、ACK确认
+ */
+#ifndef MQTT_CLIENT_H
+#define MQTT_CLIENT_H
+#include "common.h"
+#include <string>
+
+/**
+ * 初始化MQTT客户端连接
+ * @return true=连接成功, false=未启用或连接失败(后台会自动重连)
+ */
+bool mqtt_client_init();
+
+/**
+ * 发布消息到MQTT Broker
+ * @param topic 主题(为空则使用g_mqtt_topic)
+ * @param payload 消息内容
+ * @return true=发布并收到ACK, false=失败
+ */
+bool mqtt_client_publish(const std::string& topic, const std::string& payload);
+
+/**
+ * 关闭MQTT客户端连接
+ */
+void mqtt_client_close();
+
+/**
+ * 获取MQTT连接状态
+ * @return 0=未启用/未初始化, 1=已连接(稳定), 2=连接中, 3=断开
+ */
+int mqtt_get_status();
+
+#endif

+ 202 - 0
src/mqtt_client1.cpp

@@ -0,0 +1,202 @@
+/**
+ * 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;
+}

+ 758 - 0
src/network_client.cpp

@@ -0,0 +1,758 @@
+/**
+ * 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;
+			}
+		}
+	}
+
+	// fix24-v21: 使用照片抓拍时间(db_capture_time)替代当前时间,确保API上报时间与水印/DB一致
+	std::string capture_time_str;
+	if (plate_info->db_capture_time > 0) {
+		struct tm tm_buf;
+		struct tm* tm_ptr = localtime_r(&plate_info->db_capture_time, &tm_buf);
+		if (tm_ptr) {
+			char buf[64];
+			strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_ptr);
+			capture_time_str = buf;
+		} else {
+			capture_time_str = get_format_time();
+		}
+	} else {
+		capture_time_str = get_format_time();
+	}
+
+	if (g_wType == 1)
+	{
+		cJSON_AddStringToObject(json, "plateNumber", plate_info->code.c_str());
+		cJSON_AddStringToObject(json, "outWorkSiteDate", capture_time_str.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", capture_time_str.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;
+}

+ 24 - 0
src/network_client.h

@@ -0,0 +1,24 @@
+#ifndef NETWORK_CLIENT_H
+#define NETWORK_CLIENT_H
+#include "common.h"
+int lib_curl_init();
+void lib_curl_deinit();
+std::string get_cached_tenant_token();
+std::string get_tenant_token();
+int lib_curl_create_bill_request(std::shared_ptr<CarPlateInfo> plate_info, std::string& tb_num_out);
+bool lib_curl_image_upload_request(std::shared_ptr<CarPlateInfo> plate_info, GTYPE gtype);
+int lib_curl_online_status_request(CaptureInfo* cap_info);
+int lib_curl_online_status_batch_request();
+std::string json_create_bill_encode(std::shared_ptr<CarPlateInfo> plate_info);
+std::string json_online_status_encode(CaptureInfo* cap_info);
+std::string json_online_status_batch_encode();
+int parse_create_bill_response(const std::string& response, const std::string& plate, bool is_in, std::string& tb_num);
+int parse_online_status_response(std::string resp);
+int parse_upload_image_response(std::string resp);
+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 calculate_md5(const std::string& input);
+std::string get_format_time_s();
+size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, std::string* str);
+void InitCurlCommon(CURL* curl, std::string* response);
+#endif

+ 742 - 0
src/network_client1.cpp

@@ -0,0 +1,742 @@
+/**
+ * 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;
+}

+ 785 - 0
src/plate_recognizer.cpp

@@ -0,0 +1,785 @@
+/**
+ * plate_recognizer.cpp — 车牌识别实现
+ */
+#include "plate_recognizer.h"
+#include "rtsp_capture.h"
+#include "station_lock.h"
+#include "database.h"
+#include "network_client.h"
+#include "feishu_client.h"
+#include "web_server.h"
+#include "utils.h"
+#include "cvxFont.h"
+#include "hyper_lpr_sdk.h"
+#include <iostream>
+
+// 车牌类型映射表
+static const std::vector<std::string> TYPES = {
+	"蓝牌", "黄牌", "白牌单层", "绿牌", "黑牌港澳", "香港单层",
+	"香港双层", "澳门单层", "澳门双层", "黄牌双层"
+};
+
+void cleanup_plate_last_processed(bool is_in) {
+    auto& map = is_in ? g_in_plate_last_processed : g_out_plate_last_processed;
+    auto& mtx = is_in ? g_in_plate_last_processed_mtx : g_out_plate_last_processed_mtx;
+    
+    std::lock_guard<std::mutex> lock(mtx);
+    time_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+        std::chrono::system_clock::now().time_since_epoch()).count();
+    
+    std::vector<std::string> expired_keys;
+    for (const auto& pair : map) {
+        if (now_ms - pair.second > PLATE_DUPLICATE_INTERVAL_MS * 2) {
+            expired_keys.push_back(pair.first);
+        }
+    }
+    
+    for (const auto& key : expired_keys) {
+        map.erase(key);
+    }
+    
+    if (DEBUG_LOG && !expired_keys.empty()) {
+        std::cout << "[帧去重清理] " << (is_in ? "进站" : "出站") 
+                  << " 清理了 " << expired_keys.size() << " 条过期记录" << std::endl;
+    }
+}
+
+// ==================== 联单缓存结构 ====================
+
+int getCharBytes(unsigned char c) {
+	if ((c & 0x80) == 0) return 1;
+	else if ((c & 0xE0) == 0xC0) return 2;
+	else if ((c & 0xF0) == 0xE0) return 3;
+	else if ((c & 0xF8) == 0xF0) return 4;
+	return 1;
+}
+
+std::pair<std::string, std::string> splitStringInHalf(const std::string& str) {
+	std::string firstPart, secondPart;
+	size_t i = 0;
+	size_t one_line_max_char = 90;
+	while (i < str.length() && i < one_line_max_char) {
+		int bytes = getCharBytes((unsigned char)str[i]);
+		if (i + bytes > str.length()) break;
+		firstPart.append(str, i, bytes);
+		i += bytes;
+	}
+	secondPart = str.substr(i);
+	return std::make_pair(firstPart, secondPart);
+}
+
+cv::Mat create_text_watermark(const cv::Mat& src_image, std::string text)
+{
+	auto [firstHalf, secondHalf] = splitStringInHalf(text);
+	int width = src_image.cols;
+	if (width <= 0) width = 1280;
+	size_t font_size = 38;
+	cvx::CvxFont font(pathComm + "simsun.ttc");
+	cv::Mat watermark(160, width, CV_8UC3, cv::Scalar(0, 0, 0));
+	putText(watermark, firstHalf, cv::Point(40, 80), font, font_size, cv::Scalar(255, 255, 255));
+	putText(watermark, secondHalf, cv::Point(40, 140), font, font_size, cv::Scalar(255, 255, 255));
+	return watermark;
+}
+
+void add_watermask_to_src_image(cv::Mat& original, const cv::Mat& watermark, int margin) {
+	int originalHeight = original.rows;
+	int originalWidth = original.cols;
+	int watermarkHeight = watermark.rows;
+	int watermarkWidth = watermark.cols;
+	int newWidth = std::max(originalWidth, watermarkWidth);
+	int newHeight = originalHeight + watermarkHeight + margin;
+	cv::Mat result(newHeight, newWidth, original.type(), cv::Scalar(255, 255, 255));
+	cv::Rect roiOriginal(0, 0, originalWidth, originalHeight);
+	original.copyTo(result(roiOriginal));
+	int x = (newWidth - watermarkWidth) / 2;
+	if (x < 0) x = 0;
+	int y = originalHeight + margin;
+	int roi_w = std::min(watermarkWidth, newWidth - x);
+	int roi_h = std::min(watermarkHeight, newHeight - y);
+	if (roi_w > 0 && roi_h > 0) {
+		cv::Rect roiWatermark(x, y, roi_w, roi_h);
+		cv::Rect watermarkRoi(0, 0, roi_w, roi_h);
+		watermark(watermarkRoi).copyTo(result(roiWatermark));
+	}
+	original = result.clone();
+}
+
+
+int hyperlpr_lib_init(std::string model)
+{
+	HLPR_ContextConfiguration configuration = { 0 };
+	configuration.models_path = (char*)model.c_str();
+	configuration.max_num = 2;
+	configuration.det_level = DETECT_LEVEL_HIGH;
+	configuration.use_half = false;
+	configuration.nms_threshold = 0.5f;
+	configuration.rec_confidence_threshold = 0.6f;
+	configuration.box_conf_threshold = 0.30f;
+	configuration.threads = 1;
+	plate_rec_app.hlpr_ctx = HLPR_CreateContext(&configuration);
+	HREESULT ret = HLPR_ContextQueryStatus(plate_rec_app.hlpr_ctx);
+	if (ret != HResultCode::Ok) {
+		std::cerr << "HLPR_ContextQueryStatus error." << std::endl;
+		return -1;
+	}
+	return 0;
+}
+
+
+void hyperlpr_lib_deinit(void)
+{
+	HLPR_ReleaseContext(plate_rec_app.hlpr_ctx);
+	plate_rec_app.hlpr_ctx = NULL;
+	return;
+}
+
+
+std::string gtype_to_string(GTYPE gtype)
+{
+	if (gtype == POSITION_LO) return "低位车牌识别";
+	else if (gtype == POSITION_HI) return "高位抓拍";
+	return "";
+}
+
+std::string dtype_to_string(DTYPE dtype)
+{
+	if (dtype == STATION_IN) return "(入)";
+	else if (dtype == STATION_OUT) return "(出)";
+	return "";
+}
+
+std::string wtype_to_string(WTYPE wtype)
+{
+	if (wtype == BUILDING) return "工地";
+	else if (wtype == DOCK) return "码头";
+	else if (wtype == BACKFILL) return "回填点";
+	return "";
+}
+
+std::string capture_info_to_string(CaptureInfo* cap_info)
+{
+	if (!cap_info) return "";
+	// ✅ fix23: 使用抓拍时间而非当前时间
+	std::string time_str;
+	if (cap_info->capture_time > 0) {
+		struct tm tm_buf;
+		struct tm* tm_ptr = localtime_r(&cap_info->capture_time, &tm_buf);
+		if (tm_ptr) {
+			char buf[64];
+			strftime(buf, sizeof(buf), "%Y年%m月%d日%H:%M:%S", tm_ptr);
+			time_str = std::string(buf);
+		} else {
+			time_str = get_format_time_s();  // fallback
+		}
+	} else {
+		time_str = get_format_time_s();  // fallback
+	}
+	return wtype_to_string(cap_info->wtype) + ":" + project_name +
+		" 出入口:" + throughway + dtype_to_string(cap_info->dtype) + " 摄像头类型:" +
+		gtype_to_string(cap_info->gtype) + " 抓拍时间:" + time_str;
+}
+
+void capture_info_set(CaptureInfo* cap_info, GTYPE gtype, DTYPE dtype)
+{
+	cap_info->dtype = dtype;
+	cap_info->wtype = BUILDING;
+	cap_info->gtype = gtype;
+	cap_info->status = CAPTURE_NORMAL;
+	cap_info->capture_time = time(nullptr);  // ✅ fix23: 记录抓拍时间
+}
+
+int rstp_capture_info_init()
+{
+	capture_info_set(&plate_rec_app.front_info_in, POSITION_LO, STATION_IN);
+	capture_info_set(&plate_rec_app.front_info_out, POSITION_LO, STATION_OUT);
+	capture_info_set(&plate_rec_app.side_info_in, POSITION_HI, STATION_IN);
+	capture_info_set(&plate_rec_app.side_info_out, POSITION_HI, STATION_OUT);
+	return 0;
+}
+
+
+int hyperlpr_lib_update(P_HLPR_DataBuffer* buffer, cv::Mat image, cv::Mat image_full, cv::Mat image_side, CaptureInfo* cap_info)
+{
+	int result = 0;
+	HLPR_PlateResultList results = { 0 };
+	auto infer_start = std::chrono::steady_clock::now();
+	HLPR_ContextUpdateStream(plate_rec_app.hlpr_ctx, *buffer, &results);
+	auto infer_end = std::chrono::steady_clock::now();
+	int infer_ms = (int)std::chrono::duration_cast<std::chrono::milliseconds>(infer_end - infer_start).count();
+	g_metrics.record_inference_time(infer_ms);
+	if (DEBUG_LOG) {
+		std::cout << "[推理耗时] " << infer_ms << "ms, 检测到 " << results.plate_size << " 个车牌" << std::endl;
+	}
+	for (int i = 0; i < results.plate_size; ++i) {
+		if (std::isnan(results.plates[i].text_confidence))
+		{
+			std::cout << "skip confidence is NaN"
+				<< " plate info: " << results.plates[i].code << std::endl;
+			continue;
+		}
+		if (results.plates[i].text_confidence < g_PLATE_CONFIDENCE_THRESHOLD)
+		{
+			if (results.plates[i].text_confidence > g_PLATE_LOG_THRESHOLD)
+			{
+				std::cout << "skip confidence is " << results.plates[i].text_confidence
+					<< " plate info: " << results.plates[i].code << std::endl;
+			}
+			continue;
+		}
+		if (results.plates[i].type < 0 || results.plates[i].type >= (int)TYPES.size()) {
+			std::cerr << "skip unknown plate type: " << results.plates[i].type << std::endl;
+			continue;
+		}
+		// ✅ fix21: 基于UTF-8字符计数的车牌长度校验 + 类型自动纠正
+		std::string plate_type_str = TYPES[results.plates[i].type];
+		int char_count = count_utf8_chars(results.plates[i].code);
+		bool length_valid = true;
+		std::string corrected_type = plate_type_str;
+
+		// 7字符 = 标准车牌(蓝牌/黄牌/白牌单层等)
+		if (char_count == 7) {
+			if (plate_type_str == "绿牌" || plate_type_str == "黄牌双层") {
+				// 模型误判:7字符不可能是双层/绿牌,纠正为黄牌
+				corrected_type = "黄牌";
+				std::cout << "[车牌类型纠正] " << results.plates[i].code
+						  << " " << plate_type_str << "→" << corrected_type
+						  << " (字符数" << char_count << "与" << plate_type_str << "不匹配)" << std::endl;
+			}
+		}
+		// 8字符 = 新能源绿牌/黄牌双层
+		else if (char_count == 8) {
+			if (plate_type_str == "蓝牌" || plate_type_str == "黄牌" ||
+				plate_type_str == "白牌单层") {
+				// 模型误判:8字符不可能是传统单层牌,纠正为绿牌
+				corrected_type = "绿牌";
+				std::cout << "[车牌类型纠正] " << results.plates[i].code
+						  << " " << plate_type_str << "→" << corrected_type
+						  << " (字符数" << char_count << "与" << plate_type_str << "不匹配)" << std::endl;
+			}
+		}
+		// 其他字符数(6/9/10+) = 明显异常,丢弃
+		else {
+			length_valid = false;
+		}
+
+		if (!length_valid) {
+			std::cout << "[车牌长度校验] 丢弃: " << results.plates[i].code
+					  << " 类型:" << plate_type_str
+					  << " 字符数:" << char_count
+					  << " bytes:" << strlen(results.plates[i].code)
+					  << " (无法匹配任何标准车牌长度)" << std::endl;
+			continue;
+		}
+
+		// 应用纠正后的类型(通过修改type索引)
+		if (corrected_type != plate_type_str) {
+			for (int t = 0; t < (int)TYPES.size(); t++) {
+				if (TYPES[t] == corrected_type) {
+					results.plates[i].type = (HLPR_PlateType)t;
+					break;
+				}
+			}
+		}
+
+		// fix21: 测试车牌白名单交叉验证(仅记录,不阻断)
+		if (!g_special_plates.empty()) {
+			bool in_list = false;
+			{
+				std::lock_guard<std::mutex> lock(g_special_plates_mtx);
+				for (const auto& sp : g_special_plates) {
+					if (sp == results.plates[i].code) {
+						in_list = true;
+						break;
+					}
+				}
+			}
+			if (in_list && DEBUG_LOG) {
+				std::cout << "[测试车牌] 命中白名单: " << results.plates[i].code << std::endl;
+			}
+		}
+		if (DEBUG_LOG == 1)
+		{
+			std::cout << "plate info: " << results.plates[i].code << " confidence " << results.plates[i].text_confidence << std::endl;
+		}
+
+		// ✅ fix24-v8: 共享摄像头模式 — 根据交替锁定状态自动判断方向
+		// 当进出站共用同一个前向摄像头时,需要动态决定车牌属于进站还是出站
+		CaptureInfo* effective_cap_info = cap_info;
+		bool shared_mode_override = false;
+		if (g_shared_capture_mode) {
+			std::string plate_code = results.plates[i].code;
+			bool in_allowed = peek_station_lock(plate_code, true);
+			bool out_allowed = peek_station_lock(plate_code, false);
+			if (out_allowed && !in_allowed) {
+				// 当前应该出站:使用出站方向
+				effective_cap_info = &plate_rec_app.front_info_out;
+				shared_mode_override = true;
+				if (DEBUG_LOG) {
+					std::cout << "[共享摄像头] " << plate_code << " → 自动判定为出站" << std::endl;
+				}
+			} else if (in_allowed && !out_allowed) {
+				// 当前应该进站:使用进站方向
+				effective_cap_info = &plate_rec_app.front_info_in;
+				shared_mode_override = true;
+				if (DEBUG_LOG) {
+					std::cout << "[共享摄像头] " << plate_code << " → 自动判定为进站" << std::endl;
+				}
+			} else if (!in_allowed && !out_allowed) {
+				// 两个方向都不允许,跳过这个车牌
+				if (DEBUG_LOG) {
+					std::cout << "[共享摄像头] " << plate_code << " → 进出站均被锁定,跳过" << std::endl;
+				}
+				continue;
+			} else {
+				// 两个方向都允许(新车辆无记录),默认作为进站
+				effective_cap_info = &plate_rec_app.front_info_in;
+				shared_mode_override = true;
+				if (DEBUG_LOG) {
+					std::cout << "[共享摄像头] " << plate_code << " → 无历史记录,默认进站" << std::endl;
+				}
+			}
+		}
+
+		std::string map_key = dtype_to_string(effective_cap_info->dtype) + results.plates[i].code;
+		if (DEBUG_LOG == 1)
+		{
+			std::cout << "map_key: " << map_key << std::endl;
+		}
+		{
+			std::lock_guard<std::mutex> lock(plate_rec_app.map_mtx);
+			if (plate_rec_app.map.find(map_key) != plate_rec_app.map.end())
+			{
+				std::shared_ptr<CarPlateInfo> plate_info = plate_rec_app.map[map_key];
+				if (plate_info->confidence < results.plates[i].text_confidence)
+				{
+					plate_info->confidence = results.plates[i].text_confidence;
+					plate_info->type = TYPES[results.plates[i].type];
+					plate_info->image_front = image_full.clone();  // ✅ v43: 保存全帧用于上传
+					plate_info->image_side = image_side.clone();
+					// ✅ fix24-v11: 同步更新抓拍时间,确保水印时间与最新图像一致
+					if (plate_info->cap_info_copy) {
+						plate_info->cap_info_copy->capture_time = effective_cap_info->capture_time;
+					}
+				}
+				plate_info->count++;
+			}
+			else
+			{
+				auto plate_info = std::make_shared<CarPlateInfo>();
+				plate_info->confidence = results.plates[i].text_confidence;
+				plate_info->type = TYPES[results.plates[i].type];
+				plate_info->code = results.plates[i].code;
+				plate_info->image_front = image_full.clone();  // ✅ v43: 保存全帧用于上传
+				plate_info->image_side = image_side.clone();
+				plate_info->cap_info_copy.reset(new CaptureInfo(*effective_cap_info));
+				plate_info->retry_count = 0;
+				plate_info->count = 1;
+				plate_info->first_seen_time = time(NULL);
+                plate_info->db_id = 0; // 初始化数据库ID为0
+                // ✅ v41.1 新增:设置明确的方向标记
+                plate_info->is_inbound = (cap_info->dtype == STATION_IN);
+                plate_info->is_outbound = (cap_info->dtype == STATION_OUT);
+				plate_rec_app.map.insert({ map_key, plate_info });
+			}
+		}
+		result = 1;
+	}
+	return result;
+}
+
+// ==================== v41.1 新增:完全独立的进站处理函数 ====================
+
+void rstp_capture_do_work_in()
+{
+    cv::Mat image;
+    cv::Mat image_side;
+    
+    // 仅处理进站摄像头
+    image = plate_rec_app.capture_front_in->getFrame();
+    if (image.empty()) return;
+    
+    image_side = plate_rec_app.capture_side_in->getFrame();
+    if (image_side.empty()) return;
+    
+    plate_rec_app.capture_front_in->clearFrame();
+    plate_rec_app.capture_side_in->clearFrame();
+    
+    // ✅ v43新增:ROI裁剪(仅影响推理区域,上传照片仍用原始全帧)
+    cv::Mat image_full = image;  // 保存原始全帧引用
+    cv::Mat roi_image;
+    
+    if (g_roi_in.enabled && !g_roi_in.isFullFrame()) {
+        cv::Rect roi_rect = g_roi_in.getRect(image.cols, image.rows);
+        roi_image = image(roi_rect).clone();  // ✅ fix24: clone确保内存连续、stride正确
+        g_metrics.record_roi_crop(true);
+        if (g_roi_debug_enabled) {
+            std::cout << "[ROI-进站] 裁剪区域: " << roi_rect.x << "," << roi_rect.y 
+                      << " " << roi_rect.width << "x" << roi_rect.height 
+                      << " (原始: " << image.cols << "x" << image.rows << ")" << std::endl;
+        }
+    } else {
+        roi_image = image;  // 全帧模式,不裁剪
+        g_metrics.record_roi_fullframe(true);
+    }
+    
+    HLPR_ImageData data = { 0 };
+    data.data = roi_image.ptr<uint8_t>(0);  // ← ROI帧用于推理
+    data.width = roi_image.cols;
+    data.height = roi_image.rows;
+    data.format = STREAM_BGR;
+    data.rotation = CAMERA_ROTATION_0;
+    
+    // ✅ fix23: 更新抓拍时间戳
+    plate_rec_app.front_info_in.capture_time = time(nullptr);
+    
+    P_HLPR_DataBuffer buffer = HLPR_CreateDataBuffer(&data);
+    // ✅ v43变更:传入image_full用于保存,roi_image用于推理
+    hyperlpr_lib_update(&buffer, roi_image, image_full, image_side, &plate_rec_app.front_info_in);
+    HLPR_ReleaseDataBuffer(buffer);
+}
+
+// ==================== v41.1 新增:完全独立的出站处理函数 ====================
+
+void rstp_capture_do_work_out()
+{
+    // ✅ fix24-v8: 共享模式下出站由进站函数统一处理,此函数不应被调用
+    if (g_shared_capture_mode) return;
+
+    cv::Mat image;
+    cv::Mat image_side;
+    
+    // 仅处理出站摄像头
+    if (!plate_rec_app.capture_front_out) return;
+    image = plate_rec_app.capture_front_out->getFrame();
+    if (image.empty()) return;
+    
+    if (!plate_rec_app.capture_side_out) return;
+    image_side = plate_rec_app.capture_side_out->getFrame();
+    if (image_side.empty()) return;
+    
+    plate_rec_app.capture_front_out->clearFrame();
+    plate_rec_app.capture_side_out->clearFrame();
+    
+    // ✅ v43新增:ROI裁剪(仅影响推理区域,上传照片仍用原始全帧)
+    cv::Mat image_full = image;  // 保存原始全帧引用
+    cv::Mat roi_image;
+    
+    if (g_roi_out.enabled && !g_roi_out.isFullFrame()) {
+        cv::Rect roi_rect = g_roi_out.getRect(image.cols, image.rows);
+        roi_image = image(roi_rect).clone();  // ✅ fix24: clone确保内存连续、stride正确
+        g_metrics.record_roi_crop(false);
+        if (g_roi_debug_enabled) {
+            std::cout << "[ROI-出站] 裁剪区域: " << roi_rect.x << "," << roi_rect.y 
+                      << " " << roi_rect.width << "x" << roi_rect.height 
+                      << " (原始: " << image.cols << "x" << image.rows << ")" << std::endl;
+        }
+    } else {
+        roi_image = image;  // 全帧模式,不裁剪
+        g_metrics.record_roi_fullframe(false);
+    }
+    
+    HLPR_ImageData data = { 0 };
+    data.data = roi_image.ptr<uint8_t>(0);  // ← ROI帧用于推理
+    data.width = roi_image.cols;
+    data.height = roi_image.rows;
+    data.format = STREAM_BGR;
+    data.rotation = CAMERA_ROTATION_0;
+    
+    // ✅ fix23: 更新抓拍时间戳
+    plate_rec_app.front_info_out.capture_time = time(nullptr);
+    
+    P_HLPR_DataBuffer buffer = HLPR_CreateDataBuffer(&data);
+    // ✅ v43变更:传入image_full用于保存,roi_image用于推理
+    hyperlpr_lib_update(&buffer, roi_image, image_full, image_side, &plate_rec_app.front_info_out);
+    HLPR_ReleaseDataBuffer(buffer);
+}
+
+
+void capture_do_work_thread()
+{
+	while (g_running)
+	{
+        // ✅ v41.1 修复:使用完全独立的函数处理进站和出站
+        rstp_capture_do_work_in();
+        if (!g_running) break;
+        
+        // 增加100ms间隔,彻底消除时序竞争
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+        
+        // ✅ fix24-v8: 共享模式下出站由进站函数统一处理,不再单独调用出站处理
+        if (!g_shared_capture_mode) {
+            rstp_capture_do_work_out();
+        }
+        if (!g_running) break;
+        
+        std::this_thread::sleep_for(std::chrono::milliseconds(400));
+	}
+}
+
+
+void plate_cap_info_save_file(std::shared_ptr<CarPlateInfo> plate_info)
+{
+	CaptureInfo* plate_cap_info = plate_info->cap_info_copy.get();
+	if (!plate_cap_info) {
+		std::cerr << "plate_cap_info_save_file: cap_info为空" << std::endl;
+		return;
+	}
+
+	// ❌ Bug修复:移除 photo_saved 检查,避免在保存失败时误认为已保存
+	// photo_saved 应该在照片真正保存成功后设置
+
+    // ✅ v41.1 新增:运行时方向校验,防止数据错乱
+    bool is_in = (plate_cap_info->dtype == STATION_IN);
+    if ((is_in && !plate_info->is_inbound) || (!is_in && !plate_info->is_outbound)) {
+        std::cerr << "[严重错误] 方向不匹配,车牌: " << plate_info->code 
+                  << ",预期: " << (is_in ? "进站" : "出站")
+                  << ",实际: " << (plate_info->is_inbound ? "进站" : "出站") << std::endl;
+        plate_info->pic_path_front = "";
+        plate_info->pic_path_side = "";
+        return;
+    }
+
+	std::string cached_tb_num;
+	if (!create_or_get_bill_cache_for_photo(plate_info->code, is_in, cached_tb_num)) {
+		std::cout << "[诊断] " << plate_info->code << " " << (is_in ? "进站" : "出站")
+		          << " create_or_get_bill_cache_for_photo 返回 false,跳过照片保存" << std::endl;
+		plate_info->pic_path_front = "";
+		plate_info->pic_path_side = "";
+		return;
+	}
+
+	std::string output_path = pathComm + "PlateJPG/";
+	{
+		std::string dir_to_create = output_path;
+		if (!dir_to_create.empty() && dir_to_create.back() == '/') {
+			dir_to_create.pop_back();
+		}
+		size_t pos = 0;
+		while ((pos = dir_to_create.find('/', pos + 1)) != std::string::npos) {
+			std::string sub_dir = dir_to_create.substr(0, pos);
+			if (mkdir(sub_dir.c_str(), 0755) == -1) {
+				if (errno != EEXIST) { // 目录已存在不是错误
+					std::cerr << "[ERROR] 创建目录失败: " << sub_dir 
+					          << ",错误: " << strerror(errno) << std::endl;
+					g_metrics.record_dir_create_error();
+					plate_info->pic_path_front = "";
+					plate_info->pic_path_side = "";
+					return;
+				}
+			}
+		}
+		if (mkdir(dir_to_create.c_str(), 0755) == -1) {
+			if (errno != EEXIST) {
+				std::cerr << "[ERROR] 创建目录失败: " << dir_to_create 
+				          << ",错误: " << strerror(errno) << std::endl;
+				g_metrics.record_dir_create_error();
+				plate_info->pic_path_front = "";
+				plate_info->pic_path_side = "";
+				return;
+			}
+		}
+	}
+
+	// ✅ fix24-v15: 在保存照片时更新capture_time为当前时间,解决水印时间与实际保存时间不一致
+	// 根因:capture_time只在首次检测或置信度更高时更新,若车牌持续被检测但置信度未提升,
+	// capture_time会保持为第一次检测的时间,导致水印时间比实际保存时间早数分钟
+	plate_cap_info->capture_time = time(nullptr);
+	std::cout << "[DEBUG-v20] plate_cap_info->capture_time = " << plate_cap_info->capture_time << std::endl;
+	// ✅ fix24-v18: 固定db_capture_time,防止识别线程竞态覆盖导致DB时间不一致
+	plate_info->db_capture_time = plate_cap_info->capture_time;
+	std::cout << "[DEBUG-v20] plate_info->db_capture_time = " << plate_info->db_capture_time << std::endl;
+
+	char time_suffix[32] = { 0 };
+	// ✅ fix24-v13: 使用抓拍时刻作为文件名后缀,与水印时间一致
+	snprintf(time_suffix, sizeof(time_suffix), "%ld", plate_cap_info->capture_time > 0 ? plate_cap_info->capture_time : time(NULL));
+	std::string unique_suffix = std::string("_") + time_suffix;
+	CaptureInfo side_cap_info = *plate_cap_info;
+	side_cap_info.gtype = POSITION_HI;
+
+	if (plate_cap_info->dtype == STATION_IN)
+	{
+		// ✅ fix24-v11: clone后再加水印,保留原始图像不被修改,防止多次保存时水印叠加
+		cv::Mat front_copy = plate_info->image_front.clone();
+		cv::Mat side_copy = plate_info->image_side.clone();
+
+		cv::Mat image_water;
+		std::cout << "[DEBUG-v20] 水印使用 capture_time = " << plate_cap_info->capture_time << std::endl;
+		image_water = create_text_watermark(front_copy,
+			capture_info_to_string(plate_cap_info) +
+			" \n 车牌/颜色:" + plate_info->type + plate_info->code);
+		add_watermask_to_src_image(front_copy, image_water, 0);
+		std::string filename;
+		filename = output_path + plate_info->code + unique_suffix + "_front_in.jpg";
+		if (cv::imwrite(filename, front_copy)) {
+			plate_info->pic_path_front = plate_info->code + unique_suffix + "_front_in.jpg";
+		}
+		else {
+			std::cerr << "保存图片失败:" << filename << std::endl;
+			plate_info->pic_path_front = "";
+		}
+		image_water = create_text_watermark(side_copy,
+			capture_info_to_string(&side_cap_info));
+		add_watermask_to_src_image(side_copy, image_water, 0);
+		filename = output_path + plate_info->code + unique_suffix + "_side_in.jpg";
+		if (cv::imwrite(filename, side_copy)) {
+			plate_info->pic_path_side = plate_info->code + unique_suffix + "_side_in.jpg";
+			std::cout << plate_info->code << " 进站第" << (get_in_photo_count(plate_info->code) + 1) 
+					  << "组照片已保存到磁盘" << std::endl;
+		}
+		else {
+			std::cerr << "保存图片失败:" << filename << std::endl;
+			plate_info->pic_path_side = "";
+		}
+	}
+	else if (plate_cap_info->dtype == STATION_OUT)
+	{
+		// ✅ fix24-v11: clone后再加水印,保留原始图像不被修改,防止多次保存时水印叠加
+		cv::Mat front_copy = plate_info->image_front.clone();
+		cv::Mat side_copy = plate_info->image_side.clone();
+
+		cv::Mat image_water;
+		image_water = create_text_watermark(front_copy,
+			capture_info_to_string(plate_cap_info) +
+			" \n 车牌/颜色:" + plate_info->type + plate_info->code);
+		add_watermask_to_src_image(front_copy, image_water, 0);
+		std::string filename;
+		filename = output_path + plate_info->code + unique_suffix + "_front_out.jpg";
+		if (cv::imwrite(filename, front_copy)) {
+			plate_info->pic_path_front = plate_info->code + unique_suffix + "_front_out.jpg";
+		}
+		else {
+			std::cerr << "保存图片失败:" << filename << std::endl;
+			plate_info->pic_path_front = "";
+		}
+		image_water = create_text_watermark(side_copy,
+			capture_info_to_string(&side_cap_info));
+		add_watermask_to_src_image(side_copy, image_water, 0);
+		filename = output_path + plate_info->code + unique_suffix + "_side_out.jpg";
+		if (cv::imwrite(filename, side_copy)) {
+			plate_info->pic_path_side = plate_info->code + unique_suffix + "_side_out.jpg";
+			std::cout << plate_info->code << " 出站第" << (get_out_photo_count(plate_info->code) + 1) 
+					  << "组照片已保存到磁盘" << std::endl;
+		}
+		else {
+			std::cerr << "保存图片失败:" << filename << std::endl;
+			plate_info->pic_path_side = "";
+		}
+	}
+	// ✅ Bug修复:只有在照片真正保存成功后才设置 photo_saved = true
+	// 检查至少有一张照片保存成功
+	if (!plate_info->pic_path_front.empty() || !plate_info->pic_path_side.empty()) {
+		plate_info->photo_saved = true;
+		std::cout << "[DEBUG] " << plate_info->code << " 照片保存完成,photo_saved=true" << std::endl;
+	} else {
+		std::cout << "[DEBUG] " << plate_info->code << " 照片保存失败,未设置photo_saved" << std::endl;
+	}
+}
+
+
+// ==================== 进站/出站交替锁定核心函数(v42新增) ====================
+
+// 不获取锁的数据库保存版本(由调用方保证 g_station_cache_mtx + g_db_mtx)
+// ==================== 车牌信息上传(核心调度函数) ====================
+int plate_cap_info_upload(std::shared_ptr<CarPlateInfo> plate_info)
+{
+	int res = 0;
+	CaptureInfo* plate_cap_info = plate_info->cap_info_copy.get();
+	if (!plate_cap_info) {
+		std::cerr << "plate_cap_info_upload: cap_info为空" << std::endl;
+		return static_cast<int>(ErrorCode::INVALID_PARAM);
+	}
+	bool is_in = (plate_cap_info->dtype == STATION_IN);
+	std::string plate = plate_info->code;
+
+	if (is_in) {
+		if (g_in_plate_status.is_blocked(plate)) {
+			std::cerr << "车牌 " << plate << " 进站已被临时阻止,跳过上传" << std::endl;
+			plate_info->blocked_by_station_lock = false;  // ✅ fix13: 标记BLOCKED来源为is_blocked
+			return static_cast<int>(ErrorCode::BLOCKED);
+		}
+	} else {
+		if (g_out_plate_status.is_blocked(plate)) {
+			std::cerr << "车牌 " << plate << " 出站已被临时阻止,跳过上传" << std::endl;
+			plate_info->blocked_by_station_lock = false;  // ✅ fix13: 标记BLOCKED来源为is_blocked
+			return static_cast<int>(ErrorCode::BLOCKED);
+		}
+	}
+
+	// ✅ fix10: 原子锁定(检查+锁定一步完成,消除竞态条件)
+	// 先锁定再上传,上传失败则unlock_rollback回滚
+	// 重试请求(retry_count>0)跳过锁定检查,因为锁定已在首次调用时完成
+	bool station_locked = false;
+	if (g_alternating_merge_enabled && plate_info->retry_count == 0) {
+		if (is_in) {
+			station_locked = try_lock_in_station(plate);
+			if (!station_locked) {
+				std::cout << "[交替锁定] " << plate << " 进站被锁定,跳过" << std::endl;
+				plate_info->blocked_by_station_lock = true;  // ✅ fix13: 标记BLOCKED来源为交替锁定
+				return static_cast<int>(ErrorCode::BLOCKED);
+			}
+		} else {
+			station_locked = try_lock_out_station(plate);
+			if (!station_locked) {
+				std::cout << "[交替锁定] " << plate << " 出站被锁定,跳过" << std::endl;
+				plate_info->blocked_by_station_lock = true;  // ✅ fix13: 标记BLOCKED来源为交替锁定
+				return static_cast<int>(ErrorCode::BLOCKED);
+			}
+		}
+	}
+
+	// ✅ Bug修复:不在异步上传中重复调用 get_cached_*_bill,避免photo_count/tb_num被意外重置
+	// tb_num 在 save_file 阶段已确定,如果为空则直接尝试创建新工单
+	if (plate_info->tb_num.empty()) {
+		// 直接检查配额并创建工单
+		if (!check_bill_allowed(plate, is_in)) {
+			g_metrics.record_createbill(false, false, true);
+			// ✅ fix10: 创建工单失败,回滚交替锁定
+			if (station_locked) unlock_rollback_station(plate, is_in);
+			return static_cast<int>(ErrorCode::QUOTA_EXCEEDED);
+		}
+		res = lib_curl_create_bill_request(plate_info, plate_info->tb_num);
+		if (res != static_cast<int>(ErrorCode::SUCCESS)) {
+			std::cerr << "[创建工单失败] " << plate << " 错误码: " << res << std::endl;
+			// 只有网络错误才调用add_fail
+			if (res != static_cast<int>(ErrorCode::BUSINESS_ERROR) &&
+				res != static_cast<int>(ErrorCode::QUOTA_EXCEEDED)) {
+				(is_in ? g_in_plate_status : g_out_plate_status).add_fail(plate);
+			}
+			// ✅ fix10: 创建工单失败,回滚交替锁定
+			if (station_locked) unlock_rollback_station(plate, is_in);
+			return res;
+		}
+	} else {
+		std::cout << "[重试] " << plate << " 复用已有联单编号: " << plate_info->tb_num << std::endl;
+	}
+
+	bool is_special = false;
+	{
+		std::lock_guard<std::mutex> lock(g_special_plates_mtx);
+		is_special = std::find(g_special_plates.begin(), g_special_plates.end(), plate) != g_special_plates.end();
+	}
+
+	PhotoUploadResult result;
+	if (is_in) {
+		result = upload_in_photos(plate_info, plate, plate_info->tb_num,
+			project_name, point_number, throughway, is_special);
+	} else {
+		result = upload_out_photos(plate_info, plate, plate_info->tb_num,
+			project_name, point_number, throughway, is_special);
+	}
+
+	if (result.success_groups >= REQUIRED_SUCCESS_GROUPS) {
+		(is_in ? g_in_plate_status : g_out_plate_status).reset(plate);
+		g_last_data_activity_time = time(NULL);
+		g_metrics.record_station(is_in);
+		return static_cast<int>(ErrorCode::SUCCESS);
+	} else {
+		std::cerr << "[ERROR] " << plate << (is_in ? " 进站" : " 出站")
+			<< "照片上传失败,成功组数: " << result.success_groups << "/2" << std::endl;
+		// 照片上传失败调用add_fail
+		(is_in ? g_in_plate_status : g_out_plate_status).add_fail(plate);
+		// ✅ fix10: 照片上传失败,回滚交替锁定
+		if (station_locked) unlock_rollback_station(plate, is_in);
+		return static_cast<int>(ErrorCode::NETWORK_ERROR);
+	}
+}

+ 23 - 0
src/plate_recognizer.h

@@ -0,0 +1,23 @@
+#ifndef PLATE_RECOGNIZER_H
+#define PLATE_RECOGNIZER_H
+#include "common.h"
+int hyperlpr_lib_init(std::string model);
+void hyperlpr_lib_deinit(void);
+int hyperlpr_lib_update(P_HLPR_DataBuffer* buffer, cv::Mat image, cv::Mat image_full, cv::Mat image_side, CaptureInfo* cap_info);
+int rstp_capture_info_init();
+void rstp_capture_do_work_in();
+void rstp_capture_do_work_out();
+void capture_do_work_thread();
+void plate_cap_info_save_file(std::shared_ptr<CarPlateInfo> plate_info);
+int plate_cap_info_upload(std::shared_ptr<CarPlateInfo> plate_info);
+void cleanup_plate_last_processed(bool is_in);
+cv::Mat create_text_watermark(const cv::Mat& src_image, std::string text);
+void add_watermask_to_src_image(cv::Mat& original, const cv::Mat& watermark, int margin = 0);
+std::string gtype_to_string(GTYPE gtype);
+std::string dtype_to_string(DTYPE dtype);
+std::string wtype_to_string(WTYPE wtype);
+std::string capture_info_to_string(CaptureInfo* cap_info);
+void capture_info_set(CaptureInfo* cap_info, GTYPE gtype, DTYPE dtype);
+int getCharBytes(unsigned char c);
+std::pair<std::string, std::string> splitStringInHalf(const std::string& str);
+#endif

+ 250 - 0
src/rtsp_capture.cpp

@@ -0,0 +1,250 @@
+/**
+ * rtsp_capture.cpp — RTSP采集实现
+ * v43.2 模块化拆分 (编译修复版)
+ * 仅包含RTSPCapture类的方法实现
+ */
+#include "rtsp_capture.h"
+#include <iostream>
+
+RTSPCapture::RTSPCapture(const std::string& rtspUrl, DecodeMode mode,
+                         const std::string& drmDevice,
+                         const std::string& transport)
+    : rtspUrl_(rtspUrl), decodeMode_(mode), drmDevice_(drmDevice),
+      transport_(transport), fmtCtx_(nullptr), codecCtx_(nullptr),
+      hwDeviceCtx_(nullptr), swsCtx_(nullptr), videoStreamIndex_(-1),
+      isHardwareDecode_(false), running_(false) {}
+
+RTSPCapture::~RTSPCapture() {
+    stop();
+    cleanupFFmpeg();
+}
+
+void RTSPCapture::start() {
+    running_ = true;
+    captureThread_ = std::thread(&RTSPCapture::captureLoop, this);
+}
+
+void RTSPCapture::stop() {
+    running_ = false;
+    if (captureThread_.joinable()) captureThread_.join();
+}
+
+size_t RTSPCapture::FrameSize() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    return frameQueue_.size();
+}
+
+void RTSPCapture::clearFrame() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    while (!frameQueue_.empty()) frameQueue_.pop_front();
+}
+
+cv::Mat RTSPCapture::getFrame() {
+    cv::Mat frame;
+    std::unique_lock<std::mutex> lock(mutex_);
+    if (frameQueue_.empty()) return frame;
+    frame = frameQueue_.back();
+    frameQueue_.pop_back();
+    return frame;
+}
+
+// fix24 v35: 只查看最新帧但不从队列移除
+// 供MJPEG视频流使用,避免与识别线程竞争帧导致视频卡顿
+cv::Mat RTSPCapture::peekFrame() {
+    cv::Mat frame;
+    std::unique_lock<std::mutex> lock(mutex_);
+    if (frameQueue_.empty()) return frame;
+    frame = frameQueue_.back();  // 只复制,不移除
+    return frame;
+}
+
+bool RTSPCapture::initFFmpeg() {
+    AVDictionary* opts = nullptr;
+    av_dict_set(&opts, "rtsp_transport", transport_.c_str(), 0);
+    av_dict_set(&opts, "timeout", "5000000", 0);
+    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);
+
+    int ret = avformat_open_input(&fmtCtx_, rtspUrl_.c_str(), nullptr, &opts);
+    av_dict_free(&opts);
+    if (ret < 0) {
+        char errbuf[128];
+        av_strerror(ret, errbuf, sizeof(errbuf));
+        std::cerr << "[FFmpeg] 打开RTSP流失败: " << errbuf << std::endl;
+        return false;
+    }
+
+    ret = avformat_find_stream_info(fmtCtx_, nullptr);
+    if (ret < 0) {
+        std::cerr << "[FFmpeg] 获取流信息失败" << std::endl;
+        return false;
+    }
+
+    videoStreamIndex_ = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
+    if (videoStreamIndex_ < 0) {
+        std::cerr << "[FFmpeg] 未找到视频流" << std::endl;
+        return false;
+    }
+
+    AVStream* videoStream = fmtCtx_->streams[videoStreamIndex_];
+    AVCodecID codecId = videoStream->codecpar->codec_id;
+    bool isHEVC = (codecId == AV_CODEC_ID_HEVC || codecId == AV_CODEC_ID_H265);
+
+    isHardwareDecode_ = false;
+    if (decodeMode_ == DecodeMode::DRM || (decodeMode_ == DecodeMode::AUTO && isHEVC)) {
+        if (initDRMHwAccel()) {
+            isHardwareDecode_ = true;
+            std::cout << "[FFmpeg] 使用DRM硬件解码: " << (isHEVC ? "HEVC" : "H.264") << std::endl;
+        } else if (decodeMode_ == DecodeMode::DRM) {
+            std::cerr << "[FFmpeg] 强制DRM模式但初始化失败" << std::endl;
+            return false;
+        } else {
+            std::cout << "[FFmpeg] DRM不可用,回退软解码" << std::endl;
+        }
+    }
+
+    const AVCodec* codec = avcodec_find_decoder(codecId);
+    if (!codec) {
+        std::cerr << "[FFmpeg] 未找到解码器: " << avcodec_get_name(codecId) << std::endl;
+        return false;
+    }
+
+    codecCtx_ = avcodec_alloc_context3(codec);
+    if (!codecCtx_) return false;
+
+    ret = avcodec_parameters_to_context(codecCtx_, videoStream->codecpar);
+    if (ret < 0) return false;
+
+    if (isHardwareDecode_ && hwDeviceCtx_) {
+        codecCtx_->hw_device_ctx = av_buffer_ref(hwDeviceCtx_);
+    }
+
+    codecCtx_->thread_count = 4;
+    ret = avcodec_open2(codecCtx_, codec, nullptr);
+    if (ret < 0) {
+        char errbuf[128];
+        av_strerror(ret, errbuf, sizeof(errbuf));
+        std::cerr << "[FFmpeg] 打开解码器失败: " << errbuf << std::endl;
+        return false;
+    }
+
+    std::cout << "[FFmpeg] 初始化成功: " << avcodec_get_name(codecId)
+              << " " << codecCtx_->width << "x" << codecCtx_->height
+              << (isHardwareDecode_ ? " [DRM硬解]" : " [软解]") << std::endl;
+    return true;
+}
+
+bool RTSPCapture::initDRMHwAccel() {
+    AVHWDeviceType type = av_hwdevice_find_type_by_name("drm");
+    if (type == AV_HWDEVICE_TYPE_NONE) {
+        std::cerr << "[DRM] DRM硬件设备类型不可用" << std::endl;
+        return false;
+    }
+    int ret = av_hwdevice_ctx_create(&hwDeviceCtx_, type, drmDevice_.c_str(), nullptr, 0);
+    if (ret < 0) {
+        char errbuf[128];
+        av_strerror(ret, errbuf, sizeof(errbuf));
+        std::cerr << "[DRM] 创建硬件设备上下文失败: " << errbuf << std::endl;
+        return false;
+    }
+    std::cout << "[DRM] 硬件加速初始化成功: " << drmDevice_ << std::endl;
+    return true;
+}
+
+cv::Mat RTSPCapture::decodeFrameToBGR(AVFrame* frame) {
+    AVFrame* swFrame = nullptr;
+    bool needFree = false;
+
+    if (frame->format == AV_PIX_FMT_DRM_PRIME) {
+        swFrame = av_frame_alloc();
+        int ret = av_hwframe_transfer_data(swFrame, frame, 0);
+        if (ret < 0) {
+            av_frame_free(&swFrame);
+            return cv::Mat();
+        }
+        swFrame->width = frame->width;
+        swFrame->height = frame->height;
+        needFree = true;
+    } else {
+        swFrame = frame;
+    }
+
+    AVPixelFormat srcFmt = static_cast<AVPixelFormat>(swFrame->format);
+    AVPixelFormat dstFmt = AV_PIX_FMT_BGR24;
+
+    swsCtx_ = sws_getCachedContext(swsCtx_,
+        swFrame->width, swFrame->height, srcFmt,
+        swFrame->width, swFrame->height, dstFmt,
+        SWS_BILINEAR, nullptr, nullptr, nullptr);
+
+    if (!swsCtx_) {
+        if (needFree) av_frame_free(&swFrame);
+        return cv::Mat();
+    }
+
+    cv::Mat bgr(swFrame->height, swFrame->width, CV_8UC3);
+    uint8_t* dstSlice[1] = { bgr.data };
+    int dstStride[1] = { static_cast<int>(bgr.step) };
+
+    sws_scale(swsCtx_, swFrame->data, swFrame->linesize,
+        0, swFrame->height, dstSlice, dstStride);
+
+    if (needFree) av_frame_free(&swFrame);
+    return bgr;
+}
+
+void RTSPCapture::cleanupFFmpeg() {
+    if (swsCtx_) { sws_freeContext(swsCtx_); swsCtx_ = nullptr; }
+    if (codecCtx_) { avcodec_free_context(&codecCtx_); codecCtx_ = nullptr; }
+    if (fmtCtx_) { avformat_close_input(&fmtCtx_); fmtCtx_ = nullptr; }
+    if (hwDeviceCtx_) { av_buffer_unref(&hwDeviceCtx_); hwDeviceCtx_ = nullptr; }
+    videoStreamIndex_ = -1;
+    isHardwareDecode_ = false;
+}
+
+void RTSPCapture::captureLoop() {
+    while (running_) {
+        if (!fmtCtx_ && !initFFmpeg()) {
+            for (int i = 0; i < 30 && running_; ++i)
+                std::this_thread::sleep_for(std::chrono::milliseconds(100));
+            continue;
+        }
+
+        AVPacket* pkt = av_packet_alloc();
+        int ret = av_read_frame(fmtCtx_, pkt);
+
+        if (ret < 0) {
+            av_packet_free(&pkt);
+            cleanupFFmpeg();
+            for (int i = 0; i < 30 && running_; ++i)
+                std::this_thread::sleep_for(std::chrono::milliseconds(100));
+            continue;
+        }
+
+        if (pkt->stream_index != videoStreamIndex_) {
+            av_packet_free(&pkt);
+            continue;
+        }
+
+        ret = avcodec_send_packet(codecCtx_, pkt);
+        av_packet_free(&pkt);
+        if (ret < 0) continue;
+
+        AVFrame* frame = av_frame_alloc();
+        while (avcodec_receive_frame(codecCtx_, frame) == 0) {
+            cv::Mat bgr = decodeFrameToBGR(frame);
+            av_frame_unref(frame);
+
+            if (!bgr.empty()) {
+                std::lock_guard<std::mutex> lock(mutex_);
+                frameQueue_.push_back(std::move(bgr));
+                while (frameQueue_.size() > 10) frameQueue_.pop_front();
+                cond_.notify_one();
+            }
+        }
+        av_frame_free(&frame);
+    }
+    cleanupFFmpeg();
+}

+ 45 - 0
src/rtsp_capture.h

@@ -0,0 +1,45 @@
+#ifndef RTSP_CAPTURE_H
+#define RTSP_CAPTURE_H
+#include "common.h"
+
+class RTSPCapture {
+public:
+    RTSPCapture(const std::string& rtspUrl, DecodeMode mode = DecodeMode::AUTO,
+                const std::string& drmDevice = "/dev/dri/renderD128",
+                const std::string& transport = "tcp");
+    ~RTSPCapture();
+    void start();
+    void stop();
+    bool isRunning() const { return running_; }
+    size_t FrameSize();
+    void clearFrame();
+    cv::Mat getFrame();
+    // fix24 v35: peekFrame — 只读取最新帧不移除,供MJPEG流使用
+    // 避免与识别线程竞争帧队列
+    cv::Mat peekFrame();
+
+private:
+    bool initFFmpeg();
+    bool initDRMHwAccel();
+    cv::Mat decodeFrameToBGR(AVFrame* frame);
+    void cleanupFFmpeg();
+    void captureLoop();
+
+    std::string rtspUrl_;
+    DecodeMode decodeMode_;
+    std::string drmDevice_;
+    std::string transport_;
+    AVFormatContext* fmtCtx_ = nullptr;
+    AVCodecContext* codecCtx_ = nullptr;
+    AVBufferRef* hwDeviceCtx_ = nullptr;
+    SwsContext* swsCtx_ = nullptr;
+    int videoStreamIndex_ = -1;
+    bool isHardwareDecode_ = false;
+    bool running_ = false;
+    std::thread captureThread_;
+    std::deque<cv::Mat> frameQueue_;
+    std::mutex mutex_;
+    std::condition_variable cond_;
+};
+
+#endif

+ 184 - 0
src/ssh_manager.cpp

@@ -0,0 +1,184 @@
+/**
+ * ssh_manager.cpp - SSH 公钥管理实现
+ * fix24 v24: 读取/修改 authorized_keys 文件
+ */
+#include "ssh_manager.h"
+#include "common.h"
+
+#include <fstream>
+#include <sstream>
+#include <iostream>
+#include <algorithm>
+#include <cstring>
+#include <sys/stat.h>
+
+namespace ssh_mgr {
+
+// ========== 验证公钥格式 ==========
+bool validate_key_format(const std::string &key_line) {
+    // 格式: type base64_data [comment]
+    // type 必须是已知的 SSH 密钥类型
+    static const char* valid_types[] = {
+        "ssh-rsa", "ssh-dss", "ssh-ed25519",
+        "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521",
+        "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-nistp256@openssh.com",
+        nullptr
+    };
+
+    // 跳过空行和注释
+    if (key_line.empty() || key_line[0] == '#' || key_line[0] == '\n') return false;
+
+    // 检查类型
+    for (int i = 0; valid_types[i]; i++) {
+        if (key_line.find(valid_types[i]) == 0) {
+            // 检查后面是否有 base64 数据
+            size_t space_pos = key_line.find(' ');
+            if (space_pos == std::string::npos) return false;
+
+            // 检查 base64 部分至少有 20 个字符
+            size_t data_start = space_pos + 1;
+            size_t data_end = key_line.find(' ', data_start);
+            if (data_end == std::string::npos) data_end = key_line.size();
+            if (data_end - data_start < 20) return false;
+
+            return true;
+        }
+    }
+
+    return false;
+}
+
+// ========== 获取所有公钥列表 ==========
+std::vector<SSHKey> list_keys() {
+    std::vector<SSHKey> keys;
+
+    std::ifstream f(g_ssh_keys_path);
+    if (!f.is_open()) {
+        std::cerr << "[ssh_mgr] 无法打开 " << g_ssh_keys_path << std::endl;
+        return keys;
+    }
+
+    std::string line;
+    int index = 0;
+    while (std::getline(f, line)) {
+        // 跳过空行和注释
+        if (line.empty() || line[0] == '#') {
+            index++;
+            continue;
+        }
+
+        SSHKey key;
+        key.index = index;
+        key.raw_line = line;
+
+        // 解析 type base64 [comment]
+        std::istringstream iss(line);
+        iss >> key.type;
+
+        // base64 数据
+        size_t type_len = key.type.size();
+        size_t space1 = line.find(' ', type_len);
+        if (space1 != std::string::npos) {
+            size_t space2 = line.find(' ', space1 + 1);
+            if (space2 != std::string::npos) {
+                key.key_data = line.substr(space1 + 1, space2 - space1 - 1);
+                key.comment = line.substr(space2 + 1);
+            } else {
+                key.key_data = line.substr(space1 + 1);
+            }
+        }
+
+        keys.push_back(key);
+        index++;
+    }
+
+    return keys;
+}
+
+// ========== 添加公钥 ==========
+bool add_key(const std::string &key_line, std::string &error) {
+    // 验证格式
+    std::string trimmed = key_line;
+    // trim
+    while (!trimmed.empty() && (trimmed.back() == '\n' || trimmed.back() == '\r' || trimmed.back() == ' '))
+        trimmed.pop_back();
+    size_t start = trimmed.find_first_not_of(" \t");
+    if (start != std::string::npos) trimmed = trimmed.substr(start);
+
+    if (!validate_key_format(trimmed)) {
+        error = "无效的 SSH 公钥格式";
+        return false;
+    }
+
+    // 检查是否已存在相同的密钥
+    auto existing = list_keys();
+    for (const auto &k : existing) {
+        if (k.key_data == trimmed.substr(trimmed.find(' ') + 1, 
+                trimmed.find(' ', trimmed.find(' ') + 1) - trimmed.find(' ') - 1)) {
+            error = "该公钥已存在";
+            return false;
+        }
+    }
+
+    // 确保目录存在
+    std::string dir = g_ssh_keys_path.substr(0, g_ssh_keys_path.rfind('/'));
+    if (!dir.empty()) {
+        mkdir(dir.c_str(), 0700);
+    }
+
+    // 追加到文件
+    std::ofstream f(g_ssh_keys_path, std::ios::app);
+    if (!f.is_open()) {
+        error = "无法打开 " + g_ssh_keys_path;
+        return false;
+    }
+    f << trimmed << "\n";
+    f.close();
+
+    // 设置权限
+    chmod(g_ssh_keys_path.c_str(), 0600);
+
+    std::cout << "[ssh_mgr] 公钥已添加: " << trimmed.substr(0, 50) << "..." << std::endl;
+    return true;
+}
+
+// ========== 删除公钥 ==========
+bool delete_key(int index, std::string &error) {
+    std::ifstream f(g_ssh_keys_path);
+    if (!f.is_open()) {
+        error = "无法打开 " + g_ssh_keys_path;
+        return false;
+    }
+
+    std::vector<std::string> lines;
+    std::string line;
+    while (std::getline(f, line)) {
+        lines.push_back(line);
+    }
+    f.close();
+
+    if (index < 0 || index >= (int)lines.size()) {
+        error = "索引越界: " + std::to_string(index);
+        return false;
+    }
+
+    // 移除指定行
+    lines.erase(lines.begin() + index);
+
+    // 重写文件
+    std::ofstream out(g_ssh_keys_path);
+    if (!out.is_open()) {
+        error = "无法写入 " + g_ssh_keys_path;
+        return false;
+    }
+    for (const auto &l : lines) {
+        out << l << "\n";
+    }
+    out.close();
+    chmod(g_ssh_keys_path.c_str(), 0600);
+
+    std::cout << "[ssh_mgr] 公钥已删除 (index=" << index << ")" << std::endl;
+    return true;
+}
+
+}  // namespace ssh_mgr

+ 35 - 0
src/ssh_manager.h

@@ -0,0 +1,35 @@
+/**
+ * ssh_manager.h - SSH 公钥管理
+ * fix24 v24: authorized_keys 增删查
+ */
+#ifndef SSH_MANAGER_H
+#define SSH_MANAGER_H
+
+#include <string>
+#include <vector>
+
+struct SSHKey {
+    int index;              // 行号(用于删除)
+    std::string type;       // 密钥类型 (ssh-rsa, ssh-ed25519, etc.)
+    std::string key_data;   // 公钥数据(base64部分)
+    std::string comment;    // 注释(通常是 user@host)
+    std::string raw_line;   // 原始行内容
+};
+
+namespace ssh_mgr {
+
+// 获取所有公钥列表
+std::vector<SSHKey> list_keys();
+
+// 添加公钥
+bool add_key(const std::string &key_line, std::string &error);
+
+// 删除公钥(按索引)
+bool delete_key(int index, std::string &error);
+
+// 验证公钥格式
+bool validate_key_format(const std::string &key_line);
+
+}  // namespace ssh_mgr
+
+#endif  // SSH_MANAGER_H

+ 717 - 0
src/station_lock.cpp

@@ -0,0 +1,717 @@
+/**
+ * station_lock.cpp — 交替锁定实现
+ * 
+ * v43.2 fix17: 2小时超时清零(锁定记录超2小时自动清除)
+ * 
+ * fix17核心:交替锁定记录等待超过2小时(7200秒)后自动清零释放
+ * fix17修改:
+ *   1. cleanup_station_cache: 增加超过2小时的记录清除(内存+数据库)
+ *   2. load_station_cache_from_db: 重启恢复时跳过并删除超过2小时的过期记录
+ *   3. common.h: 新增STATION_LOCK_EXPIRE_SEC=7200常量
+ *   4. main.cpp: 启动信息更新为fix17+"2小时超时清零"
+ * 
+ * fix17业务规则:
+ *   进站完成后→等in_out_interval(300秒)→允许出站;如无出站→一直等待(但不超过2小时)
+ *   出站完成后→等in_out_interval+TIME_WINDOW(600秒)→允许进站;如无进站→一直等待(但不超过2小时)
+ *   出站无进站记录→永久拦截(不受2小时限制,必须先进站才能出站)
+ *   交替锁定记录等待超过2小时→自动清零释放(视为车辆已离开不再回来)
+ *   系统重启后:未超时保持锁定,超时清零
+ * 
+ * v43.2 fix15: 非对称等待时间+cleanup超时修正
+ * v43.2 fix14: AlternatingMerge=1禁用TIME_WINDOW独立限流+交替锁定独占时序
+ * v43.2 fix12: 修复 in_out_interval=TIME_WINDOW=5 时的交互Bug
+ * v43.2 fix11: 修复交替锁定"超时放行"导致连续进站P0 Bug
+ * 
+ * 交替锁定规则(AlternatingMerge=1, fix17版):
+ * 1. 进站完成后,状态变为OUTBOUND_ALLOWED,等待 in_out_interval(300秒) 后允许出站
+ * 2. 出站完成后,状态变为INBOUND_ALLOWED,等待 in_out_interval+TIME_WINDOW(600秒) 后允许进站
+ * 3. 同方向操作永远被拦截(进站后不能再进站,必须先出站)
+ * 4. 同方向操作永远被拦截(出站后不能再出站,必须先进站)
+ * 5. ✅ fix16: 出站无进站记录永远拦截,必须先进站才能出站(无超时放行)
+ * 6. ✅ fix17: 交替锁定记录等待超过2小时自动清零释放
+ * 7. ✅ fix15: 非对称等待时间,出站→进站比进站→出站多等TIME_WINDOW
+ * 8. ✅ fix14: AlternatingMerge=1时TIME_WINDOW独立限流禁用
+ * 
+ * 状态机(非对称等待+2小时超时清零):
+ *   INBOUND_ALLOWED  → 上次出站完成,等待 in_out_interval+TIME_WINDOW(600秒) 后允许进站;如无进站→一直等待(≤2小时)
+ *   OUTBOUND_ALLOWED → 上次进站完成,等待 in_out_interval(300秒) 后允许出站;如无出站→一直等待(≤2小时)
+ *   无记录出站 → 永远拦截(必须先进站,不受2小时限制)
+ *   ⚠️ 同方向操作永远被拦截(必须先完成对端操作)
+ *   ⚠️ 锁定记录等待超过2小时 → 自动清零释放(视为车辆已离开)
+ */
+#include "station_lock.h"
+#include "database.h"
+#include <iostream>
+
+static void _save_station_cache_to_db_unlocked(const std::string& plate, const PlateStationState& state) {
+    if (!g_db) return;
+    
+    const char* sql = "INSERT OR REPLACE INTO station_lock_cache "
+        "(plate_number, last_in_time, last_out_time, current_mode) VALUES (?, ?, ?, ?);";
+    sqlite3_stmt* stmt = nullptr;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+        sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_bind_int64(stmt, 2, state.last_in_time);
+        sqlite3_bind_int64(stmt, 3, state.last_out_time);
+        sqlite3_bind_int(stmt, 4, static_cast<int>(state.current_mode));
+        sqlite3_step(stmt);
+        sqlite3_finalize(stmt);
+    }
+}
+
+// 不获取锁的数据库删除版本
+static void _delete_station_cache_from_db_unlocked(const std::string& plate) {
+    if (!g_db) return;
+    
+    const char* sql = "DELETE FROM station_lock_cache WHERE plate_number=?;";
+    sqlite3_stmt* stmt = nullptr;
+    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+        sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+        sqlite3_step(stmt);
+        sqlite3_finalize(stmt);
+    }
+}
+
+// 从数据库加载交替锁定状态(重启恢复)
+void load_station_cache_from_db() {
+    if (!g_alternating_merge_enabled) return;
+    // 先在 g_db_mtx 保护下读取所有数据到临时容器
+    std::vector<std::tuple<std::string, time_t, time_t, StationMode>> temp_records;
+    
+    {
+        std::lock_guard<std::mutex> lock(g_db_mtx);
+        if (!g_db) return;
+        
+        const char* sql = "SELECT plate_number, last_in_time, last_out_time, current_mode FROM station_lock_cache;";
+        sqlite3_stmt* stmt;
+        
+        if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+            while (sqlite3_step(stmt) == SQLITE_ROW) {
+                std::string plate = (const char*)sqlite3_column_text(stmt, 0);
+                time_t last_in = sqlite3_column_int64(stmt, 1);
+                time_t last_out = sqlite3_column_int64(stmt, 2);
+                StationMode mode = static_cast<StationMode>(sqlite3_column_int(stmt, 3));
+                temp_records.emplace_back(plate, last_in, last_out, mode);
+            }
+            sqlite3_finalize(stmt);
+        }
+    }
+    
+    // ✅ fix17: 在 g_station_cache_mtx 保护下处理数据,超2小时的记录不恢复并删除
+    time_t now = time(NULL);
+    std::vector<std::string> expired_plates;
+    
+    {
+        std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+        
+        for (auto& rec : temp_records) {
+            const std::string& plate = std::get<0>(rec);
+            time_t last_in = std::get<1>(rec);
+            time_t last_out = std::get<2>(rec);
+            StationMode mode = std::get<3>(rec);
+            
+            // 计算最后活跃时间
+            time_t last_time = std::max(last_in, last_out);
+            if (last_time == 0) {
+                // last_in和last_out都为0的异常记录,仍恢复
+                PlateStationState state;
+                state.plate = plate;
+                state.last_in_time = last_in;
+                state.last_out_time = last_out;
+                state.current_mode = mode;
+                g_plate_station_cache[plate] = state;
+                std::cout << "[重启恢复] " << plate << " 异常记录(无时间戳),已恢复" << std::endl;
+                continue;
+            }
+            
+            // ✅ fix17: 超过2小时的记录不恢复,标记删除
+            int elapsed = static_cast<int>(difftime(now, last_time));
+            if (elapsed >= STATION_LOCK_EXPIRE_SEC) {
+                std::cout << "[重启清零] " << plate << " 等待已" << elapsed 
+                          << "秒(超过" << STATION_LOCK_EXPIRE_SEC << "秒),清零释放" << std::endl;
+                expired_plates.push_back(plate);
+                continue;
+            }
+            
+            // 未超时记录,恢复到内存
+            PlateStationState state;
+            state.plate = plate;
+            state.last_in_time = last_in;
+            state.last_out_time = last_out;
+            state.current_mode = mode;
+            g_plate_station_cache[plate] = state;
+            
+            // 计算当前状态下的等待剩余时间(日志报告)
+            if (mode == StationMode::OUTBOUND_ALLOWED) {
+                time_t ref = last_in;
+                int wait_remaining = g_in_out_interval_sec - static_cast<int>(difftime(now, ref));
+                if (wait_remaining > 0) {
+                    std::cout << "[重启恢复] " << plate << " 进站中,出站还需等待" 
+                              << wait_remaining << "秒" << std::endl;
+                } else {
+                    std::cout << "[重启恢复] " << plate << " 进站等待已满,可出站" << std::endl;
+                }
+            } else {
+                time_t ref = last_out;
+                int in_wait_sec = g_in_out_interval_sec + g_time_window_min * 60;
+                int wait_remaining = in_wait_sec - static_cast<int>(difftime(now, ref));
+                if (wait_remaining > 0) {
+                    std::cout << "[重启恢复] " << plate << " 出站中,进站还需等待" 
+                              << wait_remaining << "秒(共需" << in_wait_sec << "秒)" << std::endl;
+                } else {
+                    std::cout << "[重启恢复] " << plate << " 出站等待已满,可进站" << std::endl;
+                }
+            }
+        }
+    }
+    
+    // ✅ fix17: 从数据库中删除重启时发现的过期记录
+    if (!expired_plates.empty()) {
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        for (const auto& plate : expired_plates) {
+            if (g_db) {
+                const char* sql = "DELETE FROM station_lock_cache WHERE plate_number = ?;";
+                sqlite3_stmt* stmt;
+                if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+                    sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+                    sqlite3_step(stmt);
+                    sqlite3_finalize(stmt);
+                }
+            }
+        }
+        std::cout << "[重启清零] 清除 " << expired_plates.size() << " 条超过2小时的锁定记录" << std::endl;
+    }
+    
+    std::cout << "[配置] 从数据库恢复 " << g_plate_station_cache.size() 
+              << " 条站口锁定记录" << std::endl;
+}
+
+/**
+ * 原子操作:尝试进站锁定(检查+锁定一步完成,消除竞态条件)
+ * 
+ * 返回 true 表示锁定成功,可以继续上传;返回 false 表示被拦截
+ * 
+ * 逻辑(fix11: 严格交替,无同方向超时放行):
+ * - 无记录 → 允许(首次进站),立即锁定为 OUTBOUND_ALLOWED
+ * - OUTBOUND_ALLOWED(上次进站后)→ 永远拦截(必须先出站才能再进站)
+ *   ✅ fix11: 移除超时放行,严格交替。超时清理由cleanup_station_cache处理
+ * - INBOUND_ALLOWED(上次出站后)→ 必须等待 in_out_interval 秒后才允许进站
+ *   - 未到时间 → 拦截(强制等待间隔)
+ *   - 已到时间 → 允许进站,立即锁定为 OUTBOUND_ALLOWED
+ */
+bool try_lock_in_station(const std::string& plate) {
+    if (!g_alternating_merge_enabled) return true;
+
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    auto it = g_plate_station_cache.find(plate);
+    
+    // 无记录 → 可以进站(首次出现),立即锁定
+    if (it == g_plate_station_cache.end()) {
+        PlateStationState state;
+        state.plate = plate;
+        state.last_in_time = now;
+        state.last_out_time = 0;
+        state.current_mode = StationMode::OUTBOUND_ALLOWED;  // 进站完成→等待出站
+        g_plate_station_cache[plate] = state;
+        
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        _save_station_cache_to_db_unlocked(plate, state);
+        
+        g_metrics.record_station_lock(true);
+        std::cout << "[进站锁定] " << plate << " 首次进站,需等待" 
+                  << g_in_out_interval_sec << "秒后才允许出站" << std::endl;
+        return true;
+    }
+    
+    PlateStationState& state = it->second;
+    
+    // OUTBOUND_ALLOWED → 上次进站完成,必须先出站才能再进站(严格交替,无超时放行)
+    // ✅ fix11: 移除"超时放行",OUTBOUND_ALLOWED状态下进站必须被拦截
+    //   旧逻辑:进站后5分钟未出站→超时放行→允许重新进站 → 导致连续进站Bug
+    //   新逻辑:进站后必须出站,才能再进站。超时清理由cleanup_station_cache处理
+    if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+        time_t ref_time = state.last_in_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            std::cout << "[进站拦截] " << plate << " 上次进站后仅" << elapsed 
+                      << "秒,必须先出站才能再进站,还需等待出站" << std::endl;
+        } else {
+            std::cout << "[进站拦截] " << plate << " 当前为出站等待状态,必须先出站" << std::endl;
+        }
+        g_metrics.record_station_block(true);
+        return false;
+    }
+    
+    // INBOUND_ALLOWED → 上次出站后,需等待 in_out_interval+TIME_WINDOW 才允许进站(非对称间隔)
+    // ✅ fix15: 出站→进站等待 in_out_interval+TIME_WINDOW(如5+5=10分钟600秒)
+    //    进站→出站等待仅 in_out_interval(如5分钟300秒),非对称设计
+    if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+        time_t ref_time = state.last_out_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            int in_wait_sec = g_in_out_interval_sec + g_time_window_min * 60;  // 出站→进站: 300+300=600秒
+            if (elapsed >= in_wait_sec) {
+                // 等待时间已满,允许进站,立即锁定
+                state.last_in_time = now;
+                state.last_out_time = ref_time;  // 保留出站时间
+                state.current_mode = StationMode::OUTBOUND_ALLOWED;
+                
+                std::lock_guard<std::mutex> lock_db(g_db_mtx);
+                _save_station_cache_to_db_unlocked(plate, state);
+                
+                g_metrics.record_station_lock(true);
+                std::cout << "[进站放行] " << plate << " 出站后已等待" << elapsed 
+                          << "秒(需" << in_wait_sec << "秒),允许进站,已锁定" << std::endl;
+                return true;
+            }
+            std::cout << "[进站等待] " << plate << " 出站后需等待" << in_wait_sec 
+                      << "秒才能进站,还需等待"
+                      << (in_wait_sec - elapsed) << "秒" << std::endl;
+            g_metrics.record_station_block(true);
+            return false;
+        }
+        // last_out_time == 0 异常情况,允许进站,锁定
+        state.last_in_time = now;
+        state.current_mode = StationMode::OUTBOUND_ALLOWED;
+        
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        _save_station_cache_to_db_unlocked(plate, state);
+        
+        g_metrics.record_station_lock(true);
+        return true;
+    }
+    
+    return true;
+}
+
+/**
+ * 原子操作:尝试出站锁定(检查+锁定一步完成,消除竞态条件)
+ * 
+ * 返回 true 表示锁定成功,可以继续上传;返回 false 表示被拦截
+ * 
+ * 逻辑(fix16: 严格交替,出站无进站记录永远拦截):
+ * - 无记录 → 永远拦截(必须先进站才能出站,无超时放行)
+ *   ✅ fix16: 移除OUT_NO_RECORD_TIMEOUT_SEC超时放行,必须先进站
+ * - OUTBOUND_ALLOWED(上次进站后)→ 必须等待 in_out_interval 秒后才允许出站
+ *   - 未到时间 → 拦截(强制等待间隔)
+ *   - 已到时间 → 允许出站,立即锁定为 INBOUND_ALLOWED
+ * - INBOUND_ALLOWED(上次出站后)→ 永远拦截(必须先进站才能再出站)
+ */
+bool try_lock_out_station(const std::string& plate) {
+    if (!g_alternating_merge_enabled) return true;
+
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    auto it = g_plate_station_cache.find(plate);
+    
+    // ✅ fix16: 无记录 → 永远拦截(必须先进站才能出站,无超时放行)
+    // 旧逻辑:等待OUT_NO_RECORD_TIMEOUT_SEC(600秒)后允许出站 → 破坏"必须先进站"规则
+    // 新逻辑:出站无进站记录永远拦截,一直等待进站记录
+    if (it == g_plate_station_cache.end()) {
+        std::cout << "[出站拦截] " << plate << " 无进站记录,必须先进站才能出站" << std::endl;
+        g_metrics.record_station_block(false);
+        return false;
+    }
+    
+    PlateStationState& state = it->second;
+    
+    // OUTBOUND_ALLOWED → 上次进站后,需等待 in_out_interval 才允许出站(强制间隔)
+    if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+        time_t ref_time = state.last_in_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            if (elapsed >= g_in_out_interval_sec) {
+                // 等待时间已满,允许出站,立即锁定
+                state.last_out_time = now;
+                state.last_in_time = ref_time;  // 保留进站时间
+                state.current_mode = StationMode::INBOUND_ALLOWED;
+                
+                std::lock_guard<std::mutex> lock_db(g_db_mtx);
+                _save_station_cache_to_db_unlocked(plate, state);
+                
+                g_metrics.record_station_lock(false);
+                std::cout << "[出站放行] " << plate << " 进站后已等待" << elapsed 
+                          << "秒,允许出站,已锁定" << std::endl;
+                return true;
+            }
+            std::cout << "[出站等待] " << plate << " 进站后需等待" << g_in_out_interval_sec 
+                      << "秒才能出站,还需等待"
+                      << (g_in_out_interval_sec - elapsed) << "秒" << std::endl;
+            g_metrics.record_station_block(false);
+            return false;
+        }
+        // last_in_time == 0 异常情况,允许出站,锁定
+        state.last_out_time = now;
+        state.current_mode = StationMode::INBOUND_ALLOWED;
+        
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        _save_station_cache_to_db_unlocked(plate, state);
+        
+        g_metrics.record_station_lock(false);
+        return true;
+    }
+    
+    // INBOUND_ALLOWED → 上次出站完成,必须先进站才能再出站(严格交替,无超时放行)
+    // ✅ fix11: 移除"超时放行",INBOUND_ALLOWED状态下出站必须被拦截
+    //   旧逻辑:出站后5分钟未进站→超时放行→允许重新出站 → 导致连续出站
+    //   新逻辑:出站后必须进站,才能再出站。超时清理由cleanup_station_cache处理
+    if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+        time_t ref_time = state.last_out_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            std::cout << "[出站拦截] " << plate << " 上次出站后仅" << elapsed 
+                      << "秒,必须先进站才能再出站,还需等待进站" << std::endl;
+        } else {
+            std::cout << "[出站拦截] " << plate << " 当前为进站等待状态,必须先进站" << std::endl;
+        }
+        g_metrics.record_station_block(false);
+        return false;
+    }
+    
+    return false;
+}
+
+/**
+ * 交替锁定预检:检查交替锁定是否允许操作(不修改任何状态)
+ * 
+ * ✅ fix12新增:解决 in_out_interval=TIME_WINDOW=5 时的交互Bug
+ * 
+ * 问题:当 photo_saved=true 且 TIME_WINDOW(5分钟) 到期时,主循环直接重置 photo_saved=false,
+ * 但交替锁定仍会阻止同方向操作。导致照片保存→上传被拦截→photo_count递增无实际上传→重复浪费循环。
+ * 
+ * 修复:在保存照片前先调用此函数预检,如果交替锁定会拦截,跳过照片保存和计数器递增。
+ * 
+ * 与 try_lock_in/out_station 的区别:
+ * - try_lock: 原子操作(检查+锁定),修改状态,用于上传流程
+ * - peek: 只读查询(仅检查),不修改状态,用于保存照片前的预判
+ * 
+ * 返回 true 表示交替锁定允许该操作,false 表示会被拦截
+ */
+bool peek_station_lock(const std::string& plate, bool is_in) {
+    if (!g_alternating_merge_enabled) return true;
+
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    auto it = g_plate_station_cache.find(plate);
+    
+    // 无记录
+    if (it == g_plate_station_cache.end()) {
+        if (is_in) {
+            return true;  // 首次进站允许
+        } else {
+            // ✅ fix16: 出站无进站记录永远返回false(必须先进站才能出站)
+            return false;
+        }
+    }
+    
+    const PlateStationState& state = it->second;
+    
+    if (is_in) {
+        // OUTBOUND_ALLOWED → 上次进站完成,必须先出站才能再进站(严格交替)
+        if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+            std::cout << "[进站拦截] " << plate << " 必须先出站才能再进站,等待出站操作" << std::endl;
+            return false;
+        }
+        // INBOUND_ALLOWED → 上次出站后,需等待 in_out_interval+TIME_WINDOW 才允许进站(非对称)
+        // ✅ fix15: 出站→进站等待 in_out_interval+TIME_WINDOW(如5+5=10分钟600秒)
+        if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+            if (state.last_out_time > 0) {
+                int elapsed = static_cast<int>(difftime(now, state.last_out_time));
+                int in_wait_sec = g_in_out_interval_sec + g_time_window_min * 60;
+                if (elapsed >= in_wait_sec) {
+                    return true;
+                }
+                // ✅ fix24-v7: 输出剩余等待时间
+                int remaining = in_wait_sec - elapsed;
+                std::cout << "[进站等待] " << plate << " 出站后需等待" << in_wait_sec
+                          << "秒才能进站,已等待" << elapsed << "秒,还需等待"
+                          << remaining << "秒" << std::endl;
+                return false;
+            }
+            return true;  // last_out_time == 0 异常情况
+        }
+    } else {
+        // INBOUND_ALLOWED → 上次出站完成,必须先进站才能再出站(严格交替)
+        if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+            std::cout << "[出站拦截] " << plate << " 必须先进站才能再出站,等待进站操作" << std::endl;
+            return false;
+        }
+        // OUTBOUND_ALLOWED → 上次进站后,需等待 in_out_interval 才允许出站
+        if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+            if (state.last_in_time > 0) {
+                int elapsed = static_cast<int>(difftime(now, state.last_in_time));
+                if (elapsed >= g_in_out_interval_sec) {
+                    return true;
+                }
+                // ✅ fix24-v7: 输出剩余等待时间
+                int remaining = g_in_out_interval_sec - elapsed;
+                std::cout << "[出站等待] " << plate << " 进站后需等待" << g_in_out_interval_sec
+                          << "秒才能出站,已等待" << elapsed << "秒,还需等待"
+                          << remaining << "秒" << std::endl;
+                return false;
+            }
+            return true;  // last_in_time == 0 异常情况
+        }
+    }
+    
+    return true;
+}
+
+/**
+ * 解锁回滚(上传失败时调用,恢复锁定状态允许重试)
+ * 
+ * 进站失败 → 回滚:删除锁定记录,恢复到 INBOUND_ALLOWED(允许重新进站)
+ * 出站失败 → 回滚:恢复到 OUTBOUND_ALLOWED(允许重新出站)
+ */
+void unlock_rollback_station(const std::string& plate, bool was_in) {
+    if (!g_alternating_merge_enabled) return;
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    
+    auto it = g_plate_station_cache.find(plate);
+    if (it == g_plate_station_cache.end()) return;
+    
+    PlateStationState& state = it->second;
+    
+    if (was_in) {
+        // 进站失败回滚:恢复到 INBOUND_ALLOWED(上次出站完成的状态),允许重新进站
+        // 保留 last_out_time,清除 last_in_time
+        state.last_in_time = 0;
+        state.current_mode = StationMode::INBOUND_ALLOWED;
+        
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        _save_station_cache_to_db_unlocked(plate, state);
+        
+        std::cout << "[进站回滚] " << plate << " 进站失败,回滚锁定状态,允许重新进站" << std::endl;
+    } else {
+        // 出站失败回滚:恢复到 OUTBOUND_ALLOWED(上次进站完成的状态),允许重新出站
+        // 保留 last_in_time,清除 last_out_time
+        state.last_out_time = 0;
+        state.current_mode = StationMode::OUTBOUND_ALLOWED;
+        
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        _save_station_cache_to_db_unlocked(plate, state);
+        
+        std::cout << "[出站回滚] " << plate << " 出站失败,回滚锁定状态,允许重新出站" << std::endl;
+    }
+}
+
+/**
+ * 进站锁定确认(上传成功后调用,仅打印日志,锁定已在try_lock中完成)
+ */
+void lock_in_station(const std::string& plate) {
+    if (!g_alternating_merge_enabled) return;
+    // 锁定已在 try_lock_in_station 中完成,此处仅用于日志确认
+    std::cout << "[进站确认] " << plate << " 进站上传成功,锁定已生效" << std::endl;
+}
+
+/**
+ * 出站锁定确认(上传成功后调用,仅打印日志,锁定已在try_lock中完成)
+ */
+void lock_out_station(const std::string& plate) {
+    if (!g_alternating_merge_enabled) return;
+    // 锁定已在 try_lock_out_station 中完成,此处仅用于日志确认
+    std::cout << "[出站确认] " << plate << " 出站上传成功,锁定已生效" << std::endl;
+}
+
+// 交替锁定缓存清理(定时调用)
+// ✅ fix17: 超过2小时的记录清除(内存+数据库),未超时记录仅日志报告长等待
+// 交替锁定记录在对端操作完成前保持,超过2小时自动清零
+void cleanup_station_cache() {
+    if (!g_alternating_merge_enabled) return;
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    // ✅ fix17: 超过2小时的记录清除(内存+数据库),未超时记录仅日志报告长等待
+    std::vector<std::string> expired_plates;
+    
+    for (auto it = g_plate_station_cache.begin(); it != g_plate_station_cache.end(); ) {
+        PlateStationState& state = it->second;
+        time_t last_time = std::max(state.last_in_time, state.last_out_time);
+        
+        if (last_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, last_time));
+            
+            // ✅ fix17: 超过2小时(7200秒)的记录自动清零释放
+            if (elapsed >= STATION_LOCK_EXPIRE_SEC) {
+                std::cout << "[锁定清零] " << it->first << " 等待已" << elapsed 
+                          << "秒(超过" << STATION_LOCK_EXPIRE_SEC << "秒),自动清零释放" << std::endl;
+                expired_plates.push_back(it->first);
+                it = g_plate_station_cache.erase(it);
+                continue;
+            }
+            
+            // 超过30分钟的长等待记录,周期性报告
+            if (elapsed >= 1800 && elapsed % 1800 < STATUS_CLEANUP_INTERVAL_SEC) {
+                if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+                    std::cout << "[长等待] " << it->first << " 进站" << elapsed 
+                              << "秒仍未出站,继续等待(2小时后清零)" << std::endl;
+                } else {
+                    std::cout << "[长等待] " << it->first << " 出站" << elapsed 
+                              << "秒仍未进站,继续等待(2小时后清零)" << std::endl;
+                }
+            }
+        }
+        ++it;
+    }
+    
+    // 从数据库中删除已过期的记录
+    if (!expired_plates.empty()) {
+        std::lock_guard<std::mutex> lock_db(g_db_mtx);
+        for (const auto& plate : expired_plates) {
+            if (g_db) {
+                const char* sql = "DELETE FROM station_lock_cache WHERE plate_number = ?;";
+                sqlite3_stmt* stmt;
+                if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+                    sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+                    sqlite3_step(stmt);
+                    sqlite3_finalize(stmt);
+                }
+            }
+        }
+        std::cout << "[锁定清零] 清除 " << expired_plates.size() << " 条超过2小时的锁定记录" << std::endl;
+    }
+}
+
+// ==================== 交替锁定核心函数结束 ====================
+
+// ✅ fix24: 手动清除指定车牌的锁定记录
+bool clear_station_lock(const std::string& plate) {
+    if (!g_alternating_merge_enabled) return false;
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    
+    auto it = g_plate_station_cache.find(plate);
+    if (it == g_plate_station_cache.end()) {
+        std::cout << "[锁定清除] " << plate << " 无锁定记录" << std::endl;
+        return false;
+    }
+    
+    const PlateStationState& state = it->second;
+    std::string mode_str = (state.current_mode == StationMode::OUTBOUND_ALLOWED) ? "等待出站" : "等待进站";
+    std::cout << "[锁定清除] " << plate << " 原状态:" << mode_str
+              << " last_in=" << state.last_in_time << " last_out=" << state.last_out_time
+              << " → 已清除" << std::endl;
+    
+    g_plate_station_cache.erase(it);
+    
+    // 同步删除数据库记录
+    std::lock_guard<std::mutex> lock_db(g_db_mtx);
+    if (g_db) {
+        const char* sql = "DELETE FROM station_lock_cache WHERE plate_number = ?;";
+        sqlite3_stmt* stmt;
+        if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
+            sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
+            sqlite3_step(stmt);
+            sqlite3_finalize(stmt);
+        }
+    }
+    
+    return true;
+}
+
+// ✅ fix24-v7: 获取剩余等待时间
+int get_remaining_wait_time(const std::string& plate, bool is_in) {
+    if (!g_alternating_merge_enabled) return 0;
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    
+    auto it = g_plate_station_cache.find(plate);
+    if (it == g_plate_station_cache.end()) return 0;  // 无记录,可操作
+    
+    const PlateStationState& state = it->second;
+    time_t now = time(NULL);
+    
+    if (is_in) {
+        if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+            return -1;  // 严格拦截:必须先出站
+        }
+        if (state.current_mode == StationMode::INBOUND_ALLOWED && state.last_out_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, state.last_out_time));
+            int in_wait_sec = g_in_out_interval_sec + g_time_window_min * 60;
+            int remaining = in_wait_sec - elapsed;
+            return remaining > 0 ? remaining : 0;
+        }
+        return 0;
+    } else {
+        if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+            return -1;  // 严格拦截:必须先进站
+        }
+        if (state.current_mode == StationMode::OUTBOUND_ALLOWED && state.last_in_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, state.last_in_time));
+            int remaining = g_in_out_interval_sec - elapsed;
+            return remaining > 0 ? remaining : 0;
+        }
+        return 0;
+    }
+}
+
+// ==================== 旧函数兼容实现(已弃用,新代码应使用 try_lock 系列) ====================
+
+bool can_create_in_bill(const std::string& plate) {
+    // 旧接口:仅检查不锁定,存在竞态条件
+    // ⚠️ 此函数不再推荐使用,请使用 try_lock_in_station
+    // ✅ fix11: 与try_lock_in_station保持一致,OUTBOUND_ALLOWED时永远返回false
+    if (!g_alternating_merge_enabled) return true;
+
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    auto it = g_plate_station_cache.find(plate);
+    if (it == g_plate_station_cache.end()) return true;
+    
+    PlateStationState& state = it->second;
+    
+    // OUTBOUND_ALLOWED → 必须先出站才能再进站(严格交替)
+    if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+        return false;
+    }
+    
+    // INBOUND_ALLOWED → 需等待 in_out_interval+TIME_WINDOW 才允许进站(非对称)
+    // ✅ fix15: 出站→进站等待 in_out_interval+TIME_WINDOW(如5+5=10分钟600秒)
+    if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+        time_t ref_time = state.last_out_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            if (elapsed >= g_in_out_interval_sec + g_time_window_min * 60) return true;
+        }
+        return false;
+    }
+    
+    return true;
+}
+
+bool can_create_out_bill(const std::string& plate) {
+    // ⚠️ 此函数不再推荐使用,请使用 try_lock_out_station
+    // ✅ fix11: 与try_lock_out_station保持一致,INBOUND_ALLOWED时永远返回false
+    if (!g_alternating_merge_enabled) return true;
+
+    std::lock_guard<std::mutex> lock(g_station_cache_mtx);
+    time_t now = time(NULL);
+    
+    auto it = g_plate_station_cache.find(plate);
+    if (it == g_plate_station_cache.end()) {
+        // ✅ fix16: 出站无进站记录永远返回false(必须先进站才能出站)
+        return false;
+    }
+    
+    PlateStationState& state = it->second;
+    
+    // OUTBOUND_ALLOWED → 需等待 in_out_interval 才允许出站
+    if (state.current_mode == StationMode::OUTBOUND_ALLOWED) {
+        time_t ref_time = state.last_in_time;
+        if (ref_time > 0) {
+            int elapsed = static_cast<int>(difftime(now, ref_time));
+            if (elapsed >= g_in_out_interval_sec) return true;
+        }
+        return false;
+    }
+    
+    // INBOUND_ALLOWED → 必须先进站才能再出站(严格交替)
+    if (state.current_mode == StationMode::INBOUND_ALLOWED) {
+        return false;
+    }
+    
+    return true;
+}

+ 47 - 0
src/station_lock.h

@@ -0,0 +1,47 @@
+/**
+ * station_lock.h — 交替锁定接口
+ * v43.2 fix16: 严格交替锁定 — 一直等待,无超时清理/放行
+ * v43.2 fix11: 严格交替(移除同方向超时放行)+ 拦截防空转
+ * v43.2 fix12: 新增peek_station_lock预检函数,防止TIME_WINDOW与交替锁定交互Bug
+ */
+#ifndef STATION_LOCK_H
+#define STATION_LOCK_H
+
+#include "common.h"
+
+// 从数据库恢复交替锁定状态(重启恢复)
+void load_station_cache_from_db();
+
+// ✅ fix10: 原子操作:检查+锁定一步完成(消除竞态条件)
+// 返回 true 表示锁定成功,可以继续上传;返回 false 表示被拦截
+bool try_lock_in_station(const std::string& plate);
+bool try_lock_out_station(const std::string& plate);
+
+// ✅ fix12: 交替锁定预检(不修改状态,仅查询是否允许操作)
+// 用于在保存照片前判断是否会被交替锁定拦截,避免不必要的磁盘IO和计数器递增
+// 返回 true 表示交替锁定允许该操作,false 表示会被拦截
+bool peek_station_lock(const std::string& plate, bool is_in);
+
+// ✅ fix10: 解锁回滚(上传失败时调用,恢复锁定状态允许重试)
+void unlock_rollback_station(const std::string& plate, bool was_in);
+
+// 进站/出站锁定确认(上传成功后调用,锁定已在try_lock中完成,此处仅日志)
+void lock_in_station(const std::string& plate);
+void lock_out_station(const std::string& plate);
+
+// 交替锁定缓存清理(定时调用)
+void cleanup_station_cache();
+
+// ✅ fix24: 手动清除指定车牌的锁定记录
+// 用于处理出站未识别等场景导致的脏数据阻塞
+bool clear_station_lock(const std::string& plate);
+
+// ✅ fix24-v7: 获取剩余等待时间(秒)
+// 返回值:>0=还需等待的秒数,0=已满足条件可操作,-1=严格拦截(需对端先操作)
+int get_remaining_wait_time(const std::string& plate, bool is_in);
+
+// ⚠️ 以下旧函数保留兼容但已弃用,新代码应使用 try_lock 系列函数
+bool can_create_in_bill(const std::string& plate);
+bool can_create_out_bill(const std::string& plate);
+
+#endif // STATION_LOCK_H

+ 756 - 0
src/system_metrics_manager.cpp

@@ -0,0 +1,756 @@
+/**
+ * system_metrics_manager.cpp - 系统监控历史数据管理实现
+ * fix24-v38: 内存缓冲 + 每日定时写盘(减少SD卡写入)
+ */
+#include "system_metrics_manager.h"
+#include "common.h"
+
+#include <sqlite3.h>
+#include <chrono>
+#include <ctime>
+#include <sstream>
+#include <algorithm>
+#include <cmath>
+#include <cstring>
+#include <iostream>
+#include <unistd.h>
+#include <fstream>
+
+// ==================== 单例 ====================
+MetricsManager& MetricsManager::instance() {
+    static MetricsManager inst;
+    return inst;
+}
+
+// ==================== 初始化 ====================
+void MetricsManager::init(const std::string& db_path,
+                           int collection_interval_sec,
+                           const std::string& flush_time,
+                           int history_retention_days,
+                           int aggregation_retention_days,
+                           int temp_alert_threshold,
+                           int cpu_alert_threshold) {
+    db_path_ = db_path;
+    collection_interval_ = (collection_interval_sec > 0) ? collection_interval_sec : 2;
+    flush_time_ = flush_time.empty() ? "23:20" : flush_time;
+    history_retention_days_ = (history_retention_days > 0) ? history_retention_days : 7;
+    aggregation_retention_days_ = (aggregation_retention_days > 0) ? aggregation_retention_days : 365;
+    temp_alert_threshold_ = temp_alert_threshold;
+    cpu_alert_threshold_ = cpu_alert_threshold;
+
+    // 预分配内存缓冲区容量(一天的数据量,避免频繁realloc)
+    size_t estimated_daily = 86400 / collection_interval_ + 100;
+    {
+        std::lock_guard<std::mutex> lock(buffer_mutex_);
+        write_buffer_.reserve(estimated_daily);
+    }
+
+    std::cout << "[metrics_mgr] 初始化: db=" << db_path_
+              << " 采集间隔=" << collection_interval_ << "s"
+              << " 每日刷盘时间=" << flush_time_
+              << " 全量保留=" << history_retention_days_ << "天"
+              << " 聚合保留=" << aggregation_retention_days_ << "天"
+              << " 预分配缓冲=" << estimated_daily << "条"
+              << std::endl;
+}
+
+// ==================== 数据库初始化 ====================
+bool MetricsManager::init_database() {
+    sqlite3* db = nullptr;
+    int rc = sqlite3_open(db_path_.c_str(), &db);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[metrics_mgr] 无法打开数据库: " << db_path_
+                  << " 错误: " << sqlite3_errmsg(db) << std::endl;
+        if (db) sqlite3_close(db);
+        return false;
+    }
+
+    // 设置 busy timeout
+    sqlite3_busy_timeout(db, 5000);
+
+    // 创建表
+    const char* sql = R"SQL(
+        CREATE TABLE IF NOT EXISTS metrics_raw (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            timestamp INTEGER NOT NULL,
+            cpu_usage REAL,
+            mem_usage REAL,
+            disk_usage REAL,
+            cpu_temp REAL,
+            mem_used_mb INTEGER,
+            mem_total_mb INTEGER,
+            disk_used_gb INTEGER,
+            disk_total_gb INTEGER,
+            cpu_core_count INTEGER,
+            load_1min REAL,
+            load_5min REAL,
+            load_15min REAL,
+            proc_pid INTEGER,
+            proc_rss_mb INTEGER,
+            proc_cpu REAL
+        );
+
+        CREATE TABLE IF NOT EXISTS metrics_hourly (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            timestamp INTEGER NOT NULL UNIQUE,
+            cpu_avg REAL, cpu_max REAL,
+            mem_avg REAL, mem_max REAL,
+            disk_avg REAL, disk_max REAL,
+            temp_avg REAL, temp_max REAL,
+            load1_avg REAL, load1_max REAL,
+            sample_count INTEGER
+        );
+
+        CREATE TABLE IF NOT EXISTS metrics_daily (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            timestamp INTEGER NOT NULL UNIQUE,
+            cpu_avg REAL, cpu_max REAL,
+            mem_avg REAL, mem_max REAL,
+            disk_avg REAL, disk_max REAL,
+            temp_avg REAL, temp_max REAL,
+            load1_avg REAL, load1_max REAL,
+            sample_count INTEGER
+        );
+
+        CREATE INDEX IF NOT EXISTS idx_raw_ts ON metrics_raw(timestamp);
+        CREATE INDEX IF NOT EXISTS idx_hourly_ts ON metrics_hourly(timestamp);
+        CREATE INDEX IF NOT EXISTS idx_daily_ts ON metrics_daily(timestamp);
+    )SQL";
+
+    char* errmsg = nullptr;
+    rc = sqlite3_exec(db, sql, nullptr, nullptr, &errmsg);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[metrics_mgr] 建表失败: " << (errmsg ? errmsg : "unknown") << std::endl;
+        if (errmsg) sqlite3_free(errmsg);
+        sqlite3_close(db);
+        return false;
+    }
+
+    // 启用 WAL 模式以提升并发性能
+    sqlite3_exec(db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr);
+
+    metrics_db_ = db;
+    std::cout << "[metrics_mgr] 数据库初始化成功: " << db_path_ << std::endl;
+    return true;
+}
+
+// ==================== 启动 ====================
+bool MetricsManager::start() {
+    if (running_.load()) {
+        std::cerr << "[metrics_mgr] 已经在运行" << std::endl;
+        return true;
+    }
+
+    if (!init_database()) {
+        std::cerr << "[metrics_mgr] 数据库初始化失败,历史监控功能禁用" << std::endl;
+        return false;
+    }
+
+    running_.store(true);
+
+    collector_thread_ = std::thread(&MetricsManager::collection_thread_func, this);
+    flush_thread_ = std::thread(&MetricsManager::flush_thread_func, this);
+
+    std::cout << "[metrics_mgr] ✅ 采集线程(" << collection_interval_
+              << "s) + 每日刷盘线程(" << flush_time_ << ") 已启动" << std::endl;
+    return true;
+}
+
+// ==================== 停止 ====================
+void MetricsManager::stop() {
+    if (!running_.load()) return;
+    running_.store(false);
+
+    // 唤醒刷盘线程使其退出
+    flush_signal_cv_.notify_all();
+
+    if (collector_thread_.joinable()) collector_thread_.join();
+    if (flush_thread_.joinable()) flush_thread_.join();
+
+    // 最后一次刷盘(退出前保存所有缓冲数据)
+    if (!write_buffer_.empty()) {
+        do_flush();
+    }
+
+    if (metrics_db_) {
+        sqlite3_close(static_cast<sqlite3*>(metrics_db_));
+        metrics_db_ = nullptr;
+    }
+
+    std::cout << "[metrics_mgr] 已停止" << std::endl;
+}
+
+// ==================== CPU Delta 计算 ====================
+static bool read_cpu_times(long long& total, long long& idle) {
+    std::ifstream f("/proc/stat");
+    if (!f.is_open()) return false;
+    std::string line;
+    std::getline(f, line);
+    if (line.substr(0, 3) != "cpu") return false;
+
+    std::istringstream iss(line);
+    std::string label;
+    iss >> label;
+    long long user, nice, system, idle_val, iowait = 0, irq = 0, softirq = 0, steal = 0;
+    iss >> user >> nice >> system >> idle_val;
+    iss >> iowait >> irq >> softirq >> steal;
+
+    idle = idle_val + iowait;
+    total = user + nice + system + idle_val + iowait + irq + softirq + steal;
+    return true;
+}
+
+double MetricsManager::collect_cpu_delta() {
+    std::lock_guard<std::mutex> lock(cpu_delta_mutex_);
+    long long total, idle;
+    if (!read_cpu_times(total, idle)) return 0.0;
+
+    if (!cpu_delta_initialized_) {
+        prev_cpu_total_ = total;
+        prev_cpu_idle_ = idle;
+        cpu_delta_initialized_ = true;
+        return 0.0;  // 首次无法计算
+    }
+
+    long long total_diff = total - prev_cpu_total_;
+    long long idle_diff = idle - prev_cpu_idle_;
+    prev_cpu_total_ = total;
+    prev_cpu_idle_ = idle;
+
+    if (total_diff <= 0) return 0.0;
+    return (double)(total_diff - idle_diff) / total_diff * 100.0;
+}
+
+// ==================== 采集快照 ====================
+MetricsSnapshot MetricsManager::collect_snapshot() {
+    MetricsSnapshot s;
+    memset(&s, 0, sizeof(s));
+
+    s.timestamp = static_cast<int64_t>(time(nullptr));
+    s.cpu_usage = collect_cpu_delta();
+
+    // 内存
+    {
+        long avail_mb = 0;
+        double mem_pct = 0;
+        sysmon::get_memory_info(s.mem_total_mb, s.mem_used_mb, avail_mb, mem_pct);
+        s.mem_usage = mem_pct;
+    }
+
+    // 磁盘
+    long disk_used_gb = 0, disk_avail_gb = 0;
+    double disk_pct = 0;
+    sysmon::get_disk_info("/", s.disk_total_gb, disk_used_gb, disk_avail_gb, disk_pct);
+    s.disk_used_gb = disk_used_gb;
+    s.disk_usage = disk_pct;
+
+    // 温度
+    s.cpu_temp = sysmon::get_cpu_temperature();
+
+    // CPU核心数
+    s.cpu_core_count = sysconf(_SC_NPROCESSORS_ONLN);
+
+    // 负载
+    sysmon::get_load_avg(s.load_1min, s.load_5min, s.load_15min);
+
+    // 进程资源
+    s.proc_pid = getpid();
+    sysmon::get_process_resource(s.proc_pid, s.proc_rss_mb, s.proc_cpu);
+
+    return s;
+}
+
+// ==================== 采集线程 ====================
+void MetricsManager::collection_thread_func() {
+    std::cout << "[metrics_mgr] 采集线程启动" << std::endl;
+
+    // 首次采集初始化 CPU delta 基准
+    collect_snapshot();
+    // 等待一个间隔后再采集(让 delta 有意义)
+    for (int i = 0; i < collection_interval_ * 10 && running_.load(); i++) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    }
+
+    while (running_.load()) {
+        auto start = std::chrono::steady_clock::now();
+
+        MetricsSnapshot snap = collect_snapshot();
+
+        {
+            std::lock_guard<std::mutex> lock(buffer_mutex_);
+            write_buffer_.push_back(snap);
+
+            // 缓冲区保护:防止写入线程卡住时内存无限增长(最多缓冲 10 分钟数据)
+            size_t max_buffer = (600 / collection_interval_) + 10;
+            if (write_buffer_.size() > max_buffer) {
+                // 丢弃最旧的一半
+                size_t drop = write_buffer_.size() - max_buffer / 2;
+                write_buffer_.erase(write_buffer_.begin(), write_buffer_.begin() + drop);
+                std::cerr << "[metrics_mgr] 缓冲区溢出,丢弃 " << drop << " 条旧数据" << std::endl;
+            }
+        }
+
+        // 精确等待下一个间隔
+        auto elapsed = std::chrono::steady_clock::now() - start;
+        auto sleep_time = std::chrono::seconds(collection_interval_) - elapsed;
+        if (sleep_time > std::chrono::milliseconds(0)) {
+            // 分段 sleep 以便及时响应 stop
+            auto remaining = sleep_time;
+            while (remaining > std::chrono::milliseconds(200) && running_.load()) {
+                std::this_thread::sleep_for(std::chrono::milliseconds(200));
+                remaining -= std::chrono::milliseconds(200);
+            }
+            if (running_.load() && remaining > std::chrono::milliseconds(0)) {
+                std::this_thread::sleep_for(remaining);
+            }
+        }
+    }
+
+    std::cout << "[metrics_mgr] 采集线程退出" << std::endl;
+}
+
+// ==================== 写入线程 ====================
+void MetricsManager::flush_thread_func() {
+    std::cout << "[metrics_mgr] 刷盘线程启动 (每日 " << flush_time_ << " 写盘)" << std::endl;
+
+    while (running_.load()) {
+        int wait_seconds = seconds_until_next_flush();
+        // 分段等待,每5秒检查一次 running_ 标志
+        {
+            std::unique_lock<std::mutex> lock(flush_signal_mutex_);
+            flush_signal_cv_.wait_for(lock, std::chrono::seconds(wait_seconds), [this]{
+                return !running_.load();
+            });
+        }
+        if (!running_.load()) break;
+
+        // 到达刷盘时间,执行刷盘
+        do_flush();
+    }
+
+    std::cout << "[metrics_mgr] 刷盘线程退出" << std::endl;
+}
+
+// ==================== 执行刷盘 ====================
+void MetricsManager::do_flush() {
+    // 1. 取出缓冲区数据
+    std::vector<MetricsSnapshot> batch;
+    {
+        std::lock_guard<std::mutex> lock(buffer_mutex_);
+        batch.swap(write_buffer_);
+    }
+
+    if (batch.empty()) {
+        std::cout << "[metrics_mgr] 刷盘: 缓冲区为空,跳过" << std::endl;
+        return;
+    }
+
+    std::cout << "[metrics_mgr] 开始刷盘: " << batch.size() << " 条数据..." << std::endl;
+
+    // 2. 批量写入原始数据
+    if (batch_insert_raw(batch)) {
+        std::cout << "[metrics_mgr] 写入完成" << std::endl;
+    } else {
+        std::cerr << "[metrics_mgr] 写入失败!" << std::endl;
+    }
+
+    // 3. 执行聚合(小时 + 日)
+    aggregate_to_hourly();
+    aggregate_to_daily();
+
+    // 4. 清理过期数据
+    cleanup_expired();
+
+    std::cout << "[metrics_mgr] 刷盘全部完成" << std::endl;
+}
+
+// ==================== 解析刷盘时间 ====================
+bool MetricsManager::parse_flush_time(const std::string& time_str, int& hour, int& minute) {
+    if (time_str.size() < 4) return false;
+    size_t colon = time_str.find(':');
+    if (colon == std::string::npos || colon == 0 || colon >= time_str.size() - 1) return false;
+    try {
+        hour = std::stoi(time_str.substr(0, colon));
+        minute = std::stoi(time_str.substr(colon + 1));
+    } catch (...) {
+        return false;
+    }
+    return (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59);
+}
+
+// ==================== 计算距离下次刷盘的秒数 ====================
+int MetricsManager::seconds_until_next_flush() {
+    int hour = 23, minute = 20;
+    if (!parse_flush_time(flush_time_, hour, minute)) {
+        hour = 23; minute = 20; // 默认值
+    }
+
+    time_t now = time(nullptr);
+    struct tm tm_now;
+    localtime_r(&now, &tm_now);
+
+    // 计算今天的刷盘时间点
+    struct tm tm_flush = tm_now;
+    tm_flush.tm_hour = hour;
+    tm_flush.tm_min = minute;
+    tm_flush.tm_sec = 0;
+    time_t flush_today = mktime(&tm_flush);
+
+    int diff = (int)difftime(flush_today, now);
+    if (diff <= 0) {
+        // 今天的刷盘时间已过,等到明天
+        diff += 86400;
+    }
+    return diff;
+}
+
+// ==================== 批量写入 ====================
+bool MetricsManager::batch_insert_raw(const std::vector<MetricsSnapshot>& batch) {
+    if (batch.empty()) return true;
+
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return false;
+
+    sqlite3_stmt* stmt = nullptr;
+    const char* sql = "INSERT INTO metrics_raw "
+        "(timestamp,cpu_usage,mem_usage,disk_usage,cpu_temp,"
+        "mem_used_mb,mem_total_mb,disk_used_gb,disk_total_gb,"
+        "cpu_core_count,load_1min,load_5min,load_15min,"
+        "proc_pid,proc_rss_mb,proc_cpu) "
+        "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
+
+    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[metrics_mgr] prepare INSERT 失败: " << sqlite3_errmsg(db) << std::endl;
+        return false;
+    }
+
+    sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
+
+    for (const auto& s : batch) {
+        sqlite3_bind_int64(stmt, 1, s.timestamp);
+        sqlite3_bind_double(stmt, 2, s.cpu_usage);
+        sqlite3_bind_double(stmt, 3, s.mem_usage);
+        sqlite3_bind_double(stmt, 4, s.disk_usage);
+        sqlite3_bind_double(stmt, 5, s.cpu_temp);
+        sqlite3_bind_int64(stmt, 6, s.mem_used_mb);
+        sqlite3_bind_int64(stmt, 7, s.mem_total_mb);
+        sqlite3_bind_int64(stmt, 8, s.disk_used_gb);
+        sqlite3_bind_int64(stmt, 9, s.disk_total_gb);
+        sqlite3_bind_int(stmt, 10, s.cpu_core_count);
+        sqlite3_bind_double(stmt, 11, s.load_1min);
+        sqlite3_bind_double(stmt, 12, s.load_5min);
+        sqlite3_bind_double(stmt, 13, s.load_15min);
+        sqlite3_bind_int(stmt, 14, s.proc_pid);
+        sqlite3_bind_int64(stmt, 15, s.proc_rss_mb);
+        sqlite3_bind_double(stmt, 16, s.proc_cpu);
+
+        rc = sqlite3_step(stmt);
+        if (rc != SQLITE_DONE) {
+            std::cerr << "[metrics_mgr] INSERT step 失败: " << sqlite3_errmsg(db) << std::endl;
+        }
+        sqlite3_reset(stmt);
+    }
+
+    sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
+    sqlite3_finalize(stmt);
+
+    return true;
+}
+
+// ==================== 聚合到小时 ====================
+void MetricsManager::aggregate_to_hourly() {
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return;
+
+    // 聚合「已完成且尚未聚合」的整小时数据
+    // 条件:timestamp < 当前小时起始 AND timestamp > 已有hourly最大timestamp
+    const char* sql = R"SQL(
+        INSERT OR REPLACE INTO metrics_hourly
+        (timestamp, cpu_avg, cpu_max, mem_avg, mem_max, disk_avg, disk_max,
+         temp_avg, temp_max, load1_avg, load1_max, sample_count)
+        SELECT
+            (?1) * (timestamp / ?1) as hour_ts,
+            AVG(cpu_usage), MAX(cpu_usage),
+            AVG(mem_usage), MAX(mem_usage),
+            AVG(disk_usage), MAX(disk_usage),
+            AVG(CASE WHEN cpu_temp > 0 THEN cpu_temp END),
+            MAX(cpu_temp),
+            AVG(load_1min), MAX(load_1min),
+            COUNT(*)
+        FROM metrics_raw
+        WHERE timestamp < (?1) * ((strftime('%s','now') / ?1))
+        GROUP BY hour_ts
+        HAVING hour_ts > COALESCE((SELECT MAX(timestamp) FROM metrics_hourly), 0);
+    )SQL";
+
+    // 上面 SQL 中 ?1 = 3600,但 SQLite 对参数在 GROUP BY 中支持有限
+    // 改用拼接 SQL
+    int64_t now = static_cast<int64_t>(time(nullptr));
+    int64_t current_hour = (now / 3600) * 3600;
+
+    std::ostringstream oss;
+    oss << "INSERT OR REPLACE INTO metrics_hourly "
+        "(timestamp, cpu_avg, cpu_max, mem_avg, mem_max, disk_avg, disk_max, "
+        "temp_avg, temp_max, load1_avg, load1_max, sample_count) "
+        "SELECT "
+        "(timestamp / 3600) * 3600 AS hour_ts, "
+        "AVG(cpu_usage), MAX(cpu_usage), "
+        "AVG(mem_usage), MAX(mem_usage), "
+        "AVG(disk_usage), MAX(disk_usage), "
+        "AVG(CASE WHEN cpu_temp > 0 THEN cpu_temp END), "
+        "MAX(cpu_temp), "
+        "AVG(load_1min), MAX(load_1min), "
+        "COUNT(*) "
+        "FROM metrics_raw "
+        "WHERE timestamp < " << current_hour << " "
+        "GROUP BY hour_ts "
+        "HAVING hour_ts > COALESCE((SELECT MAX(timestamp) FROM metrics_hourly), 0)";
+
+    char* errmsg = nullptr;
+    int rc = sqlite3_exec(db, oss.str().c_str(), nullptr, nullptr, &errmsg);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[metrics_mgr] 小时聚合失败: " << (errmsg ? errmsg : "unknown") << std::endl;
+        if (errmsg) sqlite3_free(errmsg);
+    }
+}
+
+// ==================== 聚合到天 ====================
+void MetricsManager::aggregate_to_daily() {
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return;
+
+    int64_t now = static_cast<int64_t>(time(nullptr));
+    // 今天 0 点(UTC 简化处理,实际用 localtime 更准确,但对聚合影响不大)
+    time_t t = now;
+    struct tm tm_now;
+    localtime_r(&t, &tm_now);
+    tm_now.tm_hour = 0;
+    tm_now.tm_min = 0;
+    tm_now.tm_sec = 0;
+    int64_t today_start = static_cast<int64_t>(mktime(&tm_now));
+
+    std::ostringstream oss;
+    oss << "INSERT OR REPLACE INTO metrics_daily "
+        "(timestamp, cpu_avg, cpu_max, mem_avg, mem_max, disk_avg, disk_max, "
+        "temp_avg, temp_max, load1_avg, load1_max, sample_count) "
+        "SELECT "
+        "(timestamp / 86400) * 86400 AS day_ts, "
+        "AVG(cpu_avg), MAX(cpu_max), "
+        "AVG(mem_avg), MAX(mem_max), "
+        "AVG(disk_avg), MAX(disk_max), "
+        "AVG(temp_avg), MAX(temp_max), "
+        "AVG(load1_avg), MAX(load1_max), "
+        "SUM(sample_count) "
+        "FROM metrics_hourly "
+        "WHERE timestamp < " << today_start << " "
+        "GROUP BY day_ts "
+        "HAVING day_ts > COALESCE((SELECT MAX(timestamp) FROM metrics_daily), 0)";
+
+    char* errmsg = nullptr;
+    int rc = sqlite3_exec(db, oss.str().c_str(), nullptr, nullptr, &errmsg);
+    if (rc != SQLITE_OK) {
+        std::cerr << "[metrics_mgr] 日聚合失败: " << (errmsg ? errmsg : "unknown") << std::endl;
+        if (errmsg) sqlite3_free(errmsg);
+    }
+}
+
+// ==================== 清理过期数据 ====================
+void MetricsManager::cleanup_expired() {
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return;
+
+    int64_t now = static_cast<int64_t>(time(nullptr));
+
+    // 清理超过 history_retention_days 的原始数据
+    int64_t raw_cutoff = now - static_cast<int64_t>(history_retention_days_) * 86400;
+    std::ostringstream sql1;
+    sql1 << "DELETE FROM metrics_raw WHERE timestamp < " << raw_cutoff;
+    sqlite3_exec(db, sql1.str().c_str(), nullptr, nullptr, nullptr);
+
+    // 清理超过 aggregation_retention_days 的小时数据
+    int64_t hourly_cutoff = now - static_cast<int64_t>(aggregation_retention_days_) * 86400;
+    std::ostringstream sql2;
+    sql2 << "DELETE FROM metrics_hourly WHERE timestamp < " << hourly_cutoff;
+    sqlite3_exec(db, sql2.str().c_str(), nullptr, nullptr, nullptr);
+
+    // 清理超过 aggregation_retention_days 的日数据
+    std::ostringstream sql3;
+    sql3 << "DELETE FROM metrics_daily WHERE timestamp < " << hourly_cutoff;
+    sqlite3_exec(db, sql3.str().c_str(), nullptr, nullptr, nullptr);
+
+    // VACUUM 释放空间(可选,比较耗时,仅在大量删除后执行)
+    // sqlite3_exec(db, "VACUUM;", nullptr, nullptr, nullptr);
+
+    std::cout << "[metrics_mgr] 数据清理完成 (raw<" << raw_cutoff
+              << ", hourly/daily<" << hourly_cutoff << ")" << std::endl;
+}
+
+// ==================== 历史数据查询 ====================
+std::string MetricsManager::get_history_json(const std::string& range) {
+    int64_t now = static_cast<int64_t>(time(nullptr));
+    int64_t start_ts = 0;
+    std::string table = "metrics_raw";
+    bool use_raw = true;
+
+    if (range == "1h") {
+        start_ts = now - 3600;
+        use_raw = true;
+    } else if (range == "6h") {
+        start_ts = now - 6 * 3600;
+        use_raw = true;
+    } else if (range == "24h") {
+        start_ts = now - 24 * 3600;
+        use_raw = true;
+    } else if (range == "7d") {
+        start_ts = now - 7 * 86400;
+        use_raw = false;  // 用小时聚合
+        table = "metrics_hourly";
+    } else if (range == "30d") {
+        start_ts = now - 30 * 86400;
+        use_raw = false;
+        table = "metrics_daily";
+    } else {
+        // 默认 1h
+        start_ts = now - 3600;
+    }
+
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return "{\"error\":\"database not available\"}";
+
+    std::ostringstream sql;
+    if (use_raw) {
+        sql << "SELECT timestamp, cpu_usage, mem_usage, disk_usage, cpu_temp, "
+            << "load_1min, proc_rss_mb, proc_cpu "
+            << "FROM metrics_raw WHERE timestamp >= " << start_ts
+            << " ORDER BY timestamp ASC";
+    } else {
+        sql << "SELECT timestamp, cpu_avg, mem_avg, disk_avg, temp_avg, "
+            << "load1_avg, 0, 0 "
+            << "FROM " << table << " WHERE timestamp >= " << start_ts
+            << " ORDER BY timestamp ASC";
+    }
+
+    sqlite3_stmt* stmt = nullptr;
+    int rc = sqlite3_prepare_v2(db, sql.str().c_str(), -1, &stmt, nullptr);
+    if (rc != SQLITE_OK) {
+        return "{\"error\":\"query prepare failed\"}";
+    }
+
+    // 构建 JSON 响应
+    // 格式: {range, count, interval_hint, timestamps:[], cpu:[], mem:[], disk:[], temp:[], load:[], proc_mem:[], proc_cpu:[]}
+    std::ostringstream json;
+    json << "{\"range\":\"" << range << "\",";
+    json << "\"data\":{";
+    json << "\"timestamps\":[";
+
+    bool first = true;
+    std::ostringstream cpu_arr, mem_arr, disk_arr, temp_arr, load_arr, pmem_arr, pcpu_arr;
+
+    while (sqlite3_step(stmt) == SQLITE_ROW) {
+        if (!first) {
+            json << ",";
+            cpu_arr << ",";
+            mem_arr << ",";
+            disk_arr << ",";
+            temp_arr << ",";
+            load_arr << ",";
+            pmem_arr << ",";
+            pcpu_arr << ",";
+        }
+        first = false;
+
+        int64_t ts = sqlite3_column_int64(stmt, 0);
+        double cpu = sqlite3_column_double(stmt, 1);
+        double mem = sqlite3_column_double(stmt, 2);
+        double disk = sqlite3_column_double(stmt, 3);
+        double temp = sqlite3_column_double(stmt, 4);
+        double load = sqlite3_column_double(stmt, 5);
+        int64_t pmem = sqlite3_column_int64(stmt, 6);
+        double pcpu = sqlite3_column_double(stmt, 7);
+
+        json << ts;
+        cpu_arr << std::fixed;
+        cpu_arr.precision(1);
+        cpu_arr << cpu;
+        mem_arr << std::fixed; mem_arr.precision(1); mem_arr << mem;
+        disk_arr << std::fixed; disk_arr.precision(1); disk_arr << disk;
+        if (temp < -50) {
+            temp_arr << "null";
+        } else {
+            temp_arr << std::fixed; temp_arr.precision(1); temp_arr << temp;
+        }
+        load_arr << std::fixed; load_arr.precision(2); load_arr << load;
+        pmem_arr << pmem;
+        pcpu_arr << std::fixed; pcpu_arr.precision(1); pcpu_arr << pcpu;
+    }
+
+    json << "],";
+    json << "\"cpu\":[" << cpu_arr.str() << "],";
+    json << "\"mem\":[" << mem_arr.str() << "],";
+    json << "\"disk\":[" << disk_arr.str() << "],";
+    json << "\"temp\":[" << temp_arr.str() << "],";
+    json << "\"load\":[" << load_arr.str() << "],";
+    json << "\"proc_mem\":[" << pmem_arr.str() << "],";
+    json << "\"proc_cpu\":[" << pcpu_arr.str() << "]";
+    json << "}}";
+
+    sqlite3_finalize(stmt);
+    return json.str();
+}
+
+// ==================== 统计摘要 ====================
+std::string MetricsManager::get_stats_json() {
+    std::lock_guard<std::mutex> lock(db_mutex_);
+    sqlite3* db = static_cast<sqlite3*>(metrics_db_);
+    if (!db) return "{\"error\":\"database not available\"}";
+
+    std::ostringstream json;
+    json << "{";
+
+    // 各表记录数
+    auto count_table = [&](const char* table) -> int {
+        std::ostringstream sql;
+        sql << "SELECT COUNT(*) FROM " << table;
+        sqlite3_stmt* stmt = nullptr;
+        int count = 0;
+        if (sqlite3_prepare_v2(db, sql.str().c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
+            if (sqlite3_step(stmt) == SQLITE_ROW) {
+                count = sqlite3_column_int(stmt, 0);
+            }
+            sqlite3_finalize(stmt);
+        }
+        return count;
+    };
+
+    int raw_count = count_table("metrics_raw");
+    int hourly_count = count_table("metrics_hourly");
+    int daily_count = count_table("metrics_daily");
+
+    json << "\"raw_count\":" << raw_count << ",";
+    json << "\"hourly_count\":" << hourly_count << ",";
+    json << "\"daily_count\":" << daily_count << ",";
+    json << "\"running\":" << (running_.load() ? "true" : "false") << ",";
+    json << "\"buffer_size\":" << write_buffer_.size() << ",";
+    json << "\"collection_interval\":" << collection_interval_ << ",";
+    json << "\"flush_time\":\"" << flush_time_ << "\",";
+    json << "\"history_retention_days\":" << history_retention_days_ << ",";
+    json << "\"aggregation_retention_days\":" << aggregation_retention_days_;
+
+    json << "}";
+    return json.str();
+}
+
+// ==================== 工具函数 ====================
+int64_t MetricsManager::truncate_to_hour(int64_t ts) {
+    return (ts / 3600) * 3600;
+}
+
+int64_t MetricsManager::truncate_to_day(int64_t ts) {
+    return (ts / 86400) * 86400;
+}

+ 167 - 0
src/system_metrics_manager.h

@@ -0,0 +1,167 @@
+/**
+ * system_metrics_manager.h - 系统监控历史数据管理
+ * fix24-v37: 定时采集+批量写入SQLite+自动聚合+历史查询API
+ *
+ * 架构(v38: 内存缓冲 + 每日定时写盘,减少SD卡写入):
+ *   采集线程(每 monitor_interval 秒)→ 内存环形缓冲区
+ *   刷盘线程(每日 flush_time,默认23:20)→ SQLite metrics_raw 表
+ *   刷盘时同步执行聚合 → metrics_hourly / metrics_daily 表
+ *   Web API → 根据时间范围自动选择粒度返回数据
+ */
+#ifndef SYSTEM_METRICS_MANAGER_H
+#define SYSTEM_METRICS_MANAGER_H
+
+#include "system_monitor.h"
+#include <string>
+#include <vector>
+#include <deque>
+#include <thread>
+#include <mutex>
+#include <atomic>
+#include <cstdint>
+#include <condition_variable>
+
+// 单条监控指标快照
+struct MetricsSnapshot {
+    int64_t timestamp;         // Unix秒
+    double cpu_usage;          // CPU使用率 %
+    double mem_usage;          // 内存使用率 %
+    double disk_usage;         // 磁盘使用率 %
+    double cpu_temp;           // CPU温度 °C (-1=不可用)
+    int64_t mem_used_mb;
+    int64_t mem_total_mb;
+    int64_t disk_used_gb;
+    int64_t disk_total_gb;
+    int cpu_core_count;
+    double load_1min;
+    double load_5min;
+    double load_15min;
+    int proc_pid;
+    long proc_rss_mb;
+    double proc_cpu;
+};
+
+// 聚合数据行(hourly/daily共用结构)
+struct AggregatedMetrics {
+    int64_t timestamp;         // 聚合时间点(小时/天起始)
+    double cpu_avg;
+    double cpu_max;
+    double mem_avg;
+    double mem_max;
+    double disk_avg;
+    double disk_max;
+    double temp_avg;
+    double temp_max;
+    double load1_avg;
+    double load1_max;
+    int sample_count;          // 聚合了多少条原始数据
+};
+
+class MetricsManager {
+public:
+    static MetricsManager& instance();
+
+    // 初始化(在 parse_config_ini 之后调用)
+    void init(const std::string& db_path,
+              int collection_interval_sec,
+              const std::string& flush_time,
+              int history_retention_days,
+              int aggregation_retention_days,
+              int temp_alert_threshold,
+              int cpu_alert_threshold);
+
+    // 启动采集+写入线程
+    bool start();
+
+    // 停止所有线程(优雅退出)
+    void stop();
+
+    // 获取历史数据(Web API 调用)
+    // range: "1h","6h","24h","7d","30d"
+    // 自动选择粒度: <=24h用raw, <=7d用hourly, >7d用daily
+    std::string get_history_json(const std::string& range);
+
+    // 获取当前统计摘要
+    std::string get_stats_json();
+
+    bool is_running() const { return running_.load(); }
+
+private:
+    MetricsManager() = default;
+    ~MetricsManager() = default;
+    MetricsManager(const MetricsManager&) = delete;
+    MetricsManager& operator=(const MetricsManager&) = delete;
+
+    // 初始化数据库表结构
+    bool init_database();
+
+    // 采集线程:每 monitor_interval_ 秒采集一次,写入内存缓冲区
+    void collection_thread_func();
+
+    // 刷盘线程:每天 flush_time_ 时刻批量写入数据库 + 聚合 + 清理
+    void flush_thread_func();
+
+    // 执行一次完整的刷盘流程(写入+聚合+清理)
+    void do_flush();
+
+    // 使用delta法采集(不阻塞,与get_system_status的100ms采样不同)
+    MetricsSnapshot collect_snapshot();
+
+    // CPU delta 计算(读 /proc/stat 差值,不阻塞)
+    double collect_cpu_delta();
+
+    // 批量写入原始数据(事务)
+    bool batch_insert_raw(const std::vector<MetricsSnapshot>& batch);
+
+    // 聚合:原始→小时
+    void aggregate_to_hourly();
+
+    // 聚合:小时→天
+    void aggregate_to_daily();
+
+    // 清理过期数据
+    void cleanup_expired();
+
+    // 生成小时/天边界时间戳
+    static int64_t truncate_to_hour(int64_t ts);
+    static int64_t truncate_to_day(int64_t ts);
+
+    // 解析刷盘时间 "HH:MM"
+    static bool parse_flush_time(const std::string& time_str, int& hour, int& minute);
+
+    // 计算距离下次刷盘的秒数
+    int seconds_until_next_flush();
+
+    // ---- 成员变量 ----
+    std::string db_path_;
+    int collection_interval_ = 2;     // 采集间隔(秒)
+    std::string flush_time_ = "23:20";// 每日刷盘时间
+    int history_retention_days_ = 7;
+    int aggregation_retention_days_ = 365;
+    int temp_alert_threshold_ = 70;
+    int cpu_alert_threshold_ = 90;
+
+    std::atomic<bool> running_{false};
+    std::thread collector_thread_;
+    std::thread flush_thread_;
+
+    // 内存写入缓冲(全天数据,每日23:20一次性写入数据库)
+    std::vector<MetricsSnapshot> write_buffer_;
+    std::mutex buffer_mutex_;
+
+    // 刷盘触发信号(用于外部触发立即刷盘)
+    std::mutex flush_signal_mutex_;
+    std::condition_variable flush_signal_cv_;
+
+    // CPU delta 计算状态
+    std::mutex cpu_delta_mutex_;
+    long long prev_cpu_total_ = 0;
+    long long prev_cpu_idle_ = 0;
+    bool cpu_delta_initialized_ = false;
+
+    // SQLite 连接(独立于主业务数据库)
+    void* metrics_db_ = nullptr;  // sqlite3*
+    std::mutex db_mutex_;
+};
+
+#endif  // SYSTEM_METRICS_MANAGER_H

+ 274 - 0
src/system_monitor.cpp

@@ -0,0 +1,274 @@
+/**
+ * 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() {
+    // 尝试多个温度传感器路径(兼容 RPi4/Pi5、Ubuntu、Debian 等)
+    const char* paths[] = {
+        "/sys/class/thermal/thermal_zone0/temp",
+        "/sys/class/thermal/thermal_zone1/temp",
+        "/sys/class/hwmon/hwmon0/temp1_input",
+        "/sys/class/hwmon/hwmon1/temp1_input",
+        "/sys/class/hwmon/hwmon2/temp1_input",
+        "/sys/devices/virtual/thermal/thermal_zone0/temp",
+        "/sys/devices/platform/scb/fd5d0000.tmp/hwmon/hwmon0/temp1_input",
+        nullptr
+    };
+    for (int i = 0; paths[i]; i++) {
+        std::ifstream f(paths[i]);
+        if (f.is_open()) {
+            long long millideg = 0;
+            f >> millideg;
+            // 合理温度范围:-40°C ~ 125°C(原始值为毫度)
+            if (millideg > -40000 && millideg < 125000) {
+                return millideg / 1000.0;
+            }
+            // 如果值在合理范围但 > 1000,可能是已经以°C为单位
+            if (millideg > 1000 && millideg < 125) {
+                return millideg;
+            }
+        }
+    }
+    // 尝试用 vcgencmd(仅 Raspberry Pi OS)
+    {
+        FILE *fp = popen("vcgencmd measure_temp 2>/dev/null", "r");
+        if (fp) {
+            char buf[128] = {0};
+            if (fgets(buf, sizeof(buf) - 1, fp)) {
+                // 格式: temp=42.8'C
+                char *eq = strchr(buf, '=');
+                if (eq) {
+                    double temp = atof(eq + 1);
+                    if (temp > 0 && temp < 125) {
+                        pclose(fp);
+                        return temp;
+                    }
+                }
+            }
+            pclose(fp);
+        }
+    }
+    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{};  // ✅ fix24-v39: 使用值初始化替代 memset,避免破坏 std::string/vector 内部状态
+
+    // 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 > -50.0 && s.cpu_temp < 150.0);  // ✅ fix24-v39: 放宽判断,-50~150范围内均视为有效
+
+    // 磁盘(根分区)
+    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

+ 79 - 0
src/system_monitor.h

@@ -0,0 +1,79 @@
+/**
+ * system_monitor.h - 系统资源监控(CPU/内存/温度/磁盘)
+ * fix24 v24: Web管理功能批次
+ */
+#ifndef SYSTEM_MONITOR_H
+#define SYSTEM_MONITOR_H
+
+#include <string>
+#include <vector>
+
+struct SystemStatus {
+    // CPU
+    double cpu_usage_percent;        // CPU 使用率 (%)
+    int cpu_core_count;              // CPU 核心数
+    std::vector<double> cpu_per_core; // 每核使用率
+    double cpu_load_1min;            // 1分钟负载
+    double cpu_load_5min;            // 5分钟负载
+    double cpu_load_15min;           // 15分钟负载
+
+    // 内存
+    long mem_total_mb;               // 总内存 (MB)
+    long mem_used_mb;                // 已用内存 (MB)
+    long mem_available_mb;           // 可用内存 (MB)
+    double mem_usage_percent;        // 内存使用率 (%)
+
+    // 温度
+    double cpu_temp;                 // CPU 温度 (°C)
+    bool temp_available;             // 温度传感器是否可用
+
+    // 磁盘
+    long disk_total_gb;              // 磁盘总容量 (GB)
+    long disk_used_gb;               // 已用 (GB)
+    long disk_available_gb;          // 可用 (GB)
+    double disk_usage_percent;       // 磁盘使用率 (%)
+
+    // 系统信息
+    std::string hostname;
+    std::string kernel_version;
+    std::string uptime_string;
+    long uptime_seconds;
+
+    // 进程信息
+    int plate_rec_pid;
+    long plate_rec_rss_mb;           // PlateRecApp 内存占用 (MB)
+    double plate_rec_cpu_percent;    // PlateRecApp CPU 占用 (%)
+};
+
+namespace sysmon {
+
+// 获取系统状态快照
+SystemStatus get_system_status();
+
+// 获取 CPU 使用率(两次采样差值计算)
+double get_cpu_usage(int sample_ms = 200);
+
+// 获取 CPU 温度(从 /sys/class/thermal)
+double get_cpu_temperature();
+
+// 获取内存信息(从 /proc/meminfo)
+void get_memory_info(long &total_mb, long &used_mb, long &avail_mb, double &percent);
+
+// 获取磁盘信息
+void get_disk_info(const std::string &path, long &total_gb, long &used_gb, long &avail_gb, double &percent);
+
+// 获取系统启动时间
+std::string get_uptime_str(long &seconds);
+
+// 获取负载均值
+void get_load_avg(double &load1, double &load5, double &load15);
+
+// 获取当前进程资源占用
+void get_process_resource(int pid, long &rss_mb, double &cpu_percent);
+
+// 格式化文件大小
+std::string format_size(long mb);
+
+}  // namespace sysmon
+
+#endif  // SYSTEM_MONITOR_H

+ 247 - 0
src/system_monitor1.cpp

@@ -0,0 +1,247 @@
+/**
+ * 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

+ 1264 - 0
src/utils.cpp

@@ -0,0 +1,1264 @@
+/**
+ * utils.cpp — 工具函数实现
+ * v43.2 模块化拆分 (从原始源码重新提取)
+ */
+#include "utils.h"
+#include "database.h"
+#include "network_client.h"
+#include "station_lock.h"
+#include "log_manager.h"
+#include "ini.h"
+#include <iostream>
+
+void signal_handler(int signum) {
+	const char msg[] = "\n收到退出信号,正在安全退出...\n";
+	::write(STDOUT_FILENO, msg, sizeof(msg) - 1);
+	g_running = 0;
+	// ✅ fix24-v36: 信号处理时立即刷盘,防止丢失内存中的日志
+	MemoryLogBuffer::instance().signal_flush();
+}
+
+bool create_or_get_bill_cache_for_photo(const std::string& plate, bool is_in, std::string& cached_tb_num)
+{
+	time_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+		std::chrono::system_clock::now().time_since_epoch()).count();
+	
+    // 使用独立的帧去重Map
+    auto& map = is_in ? g_in_plate_last_processed : g_out_plate_last_processed;
+    auto& mtx = is_in ? g_in_plate_last_processed_mtx : g_out_plate_last_processed_mtx;
+    
+    {
+        // ✅ Bug修复:先完成帧去重检查,尽早释放帧去重锁
+        std::lock_guard<std::mutex> lock(mtx);
+        auto it = map.find(plate);
+        if (it != map.end()) {
+            if (now_ms - it->second < PLATE_DUPLICATE_INTERVAL_MS) {
+                if (DEBUG_LOG) {
+                    std::cout << "[INFO] " << plate << (is_in ? " 进站" : " 出站") 
+                              << " 2秒内已处理过,跳过重复存储" << std::endl;
+                }
+                return false;
+            }
+        }
+        map[plate] = now_ms;
+    } // ✅ 释放帧去重锁,避免深层嵌套加锁导致死锁
+
+	int photo_count = 0;
+	bool bill_created = false;
+	bool cache_hit = false;
+
+	if (is_in) {
+		cache_hit = get_cached_in_bill(plate, cached_tb_num, &photo_count, &bill_created);
+	}
+	else {
+		cache_hit = get_cached_out_bill(plate, cached_tb_num, &photo_count, &bill_created);
+	}
+
+	// ✅ 检查是否已达照片组上限
+	if (photo_count >= g_max_photo_groups) {
+		std::cout << "[INFO] " << plate << " " << (is_in ? "进站" : "出站")
+			<< "已成功上传" << photo_count << "组照片(上限" << g_max_photo_groups << "组),跳过保存" << std::endl;
+		return false;
+	}
+
+	// ✅ 严重Bug修复:超过TIME_WINDOW后,不再保存照片
+	if (!cache_hit) {
+		// 检查该车牌是否在当前时间窗口内已被处理过
+		if (!check_bill_allowed(plate, is_in)) {
+			std::cout << "[拦截] " << plate << " " << (is_in ? "进站" : "出站")
+				<< " TIME_WINDOW(" << g_time_window_min << "分钟)内已处理过,禁止保存照片" << std::endl;
+			return false;
+		}
+		std::cout << "[INFO] " << plate << " " << (is_in ? "进站" : "出站")
+			<< " 首次识别,允许保存照片" << std::endl;
+	}
+
+	// ✅ P0修复:立即递增计数器,防止外层重试时重复保存同一组照片
+	if (is_in) {
+		increment_in_photo_count(plate);
+	}
+	else {
+		increment_out_photo_count(plate);
+	}
+	std::cout << "[DEBUG] " << plate << " " << (is_in ? "进站" : "出站")
+		<< "允许保存照片,计数器已递增" << std::endl;
+
+	return true;
+}
+
+// ✅ 修复:update_bill_cache_tb_num不再自动创建新缓存,避免重置photo_count
+void update_bill_cache_tb_num(const std::string& plate, bool is_in, const std::string& tb_num)
+{
+	if (is_in) {
+		std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+		auto it = g_in_bill_cache.find(plate);
+		if (it != g_in_bill_cache.end()) {
+			it->second.tb_num = tb_num;
+			it->second.bill_created = true;
+			std::cout << "[DEBUG] " << plate << " 进站更新联单编号: " << tb_num << std::endl;
+		} else {
+            // ✅ 修复:缓存不存在时不创建新条目,避免重置photo_count
+            // 让create_or_get_bill_cache_for_photo在下次保存照片时自然创建
+            std::cout << "[DEBUG] " << plate << " 进站缓存不存在,跳过更新联单编号" << std::endl;
+        }
+	}
+	else {
+		std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+		auto it = g_out_bill_cache.find(plate);
+		if (it != g_out_bill_cache.end()) {
+			it->second.tb_num = tb_num;
+			it->second.bill_created = true;
+			std::cout << "[DEBUG] " << plate << " 出站更新联单编号: " << tb_num << std::endl;
+		} else {
+            // ✅ 修复:缓存不存在时不创建新条目,避免重置photo_count
+            std::cout << "[DEBUG] " << plate << " 出站缓存不存在,跳过更新联单编号" << std::endl;
+        }
+	}
+}
+void load_special_plates(const std::string& config_path) {
+	if (config_path.empty()) return;
+	char config_path_buf[PATH_MAX] = { 0 };
+	strncpy(config_path_buf, config_path.c_str(), PATH_MAX - 1);
+
+	std::vector<std::string> cars;
+	auto plate_handler = [](void* user, const char* section, const char* name, const char* value) -> int {
+		if (!user) return 1;
+		if (strcmp(section, "PlateRec") == 0) {
+			if (strncmp(name, "car[", 4) == 0) {
+				if (value && value[0] != '\0') {
+					std::vector<std::string>* vec = reinterpret_cast<std::vector<std::string>*>(user);
+					vec->emplace_back(value);
+				}
+			}
+		}
+		return 1;
+		};
+
+	int err = ini_parse(config_path_buf, plate_handler, &cars);
+	if (err < 0) {
+		std::cerr << "无法打开特殊车牌配置: " << config_path_buf << std::endl;
+		return;
+	}
+
+	std::lock_guard<std::mutex> lock(g_special_plates_mtx);
+	g_special_plates.clear();
+	for (const auto& p : cars) {
+		g_special_plates.push_back(p);
+		std::cout << "加载特殊车牌: " << p << std::endl;
+	}
+}
+
+bool check_bill_allowed_unlocked(const std::string& plate, bool is_in) {
+	// ✅ fix14: AlternatingMerge=1时禁用TIME_WINDOW,交替锁定独占时序控制
+	// 交替锁定已保证严格交替(进站→出站→进站),无需TIME_WINDOW额外限流
+	if (g_alternating_merge_enabled) {
+		return true;
+	}
+	
+	time_t now = time(NULL);
+	auto& records = is_in ? g_in_upload_records : g_out_upload_records;
+	
+	auto it = records.find(plate);
+	
+	if (it == records.end()) {
+		// ✅ 核心修复:如果内存中没有记录,直接允许创建
+		// 因为 _db_load_tw_cache_unlocked 已经清理了过期的数据库记录
+		// 所以这里没有记录意味着要么是新车,要么是旧记录已过期被清理
+		std::cout << "[CHECK] " << plate << (is_in ? "进站" : "出站") << " 内存中无记录,允许创建新工单" << std::endl;
+		return true;
+	}
+	
+	auto& record = it->second;
+	time_t record_time = record.last_time > 0 ? record.last_time : record.create_time;
+	
+	// ✅ 核心修复:超过5分钟后,重置计数器,开启新的时间窗口周期
+	if (record_time > 0 && difftime(now, record_time) >= g_time_window_min * 60) {
+		std::cout << "[TIME_WINDOW重置] " << plate << (is_in ? "进站" : "出站")
+			<< " 已超过" << g_time_window_min << "分钟窗口,重置配额,开启新周期" << std::endl;
+		record.bill_count = 0;
+		record.last_time = now;  // ✅ 更新为当前时间,用于下次重启判断
+		record.create_time = now;
+		record.photo_success_count = 0;
+		record.feishu_sent = false;
+		record.generation++;
+		
+		// ✅ TIME_WINDOW持久化:重置后保存到数据库
+		int photo_count = 0;
+		if (is_in) {
+			auto bill_it = g_in_bill_cache.find(plate);
+			photo_count = (bill_it != g_in_bill_cache.end()) ? bill_it->second.photo_count : 0;
+		} else {
+			auto bill_it = g_out_bill_cache.find(plate);
+			photo_count = (bill_it != g_out_bill_cache.end()) ? bill_it->second.photo_count : 0;
+		}
+		db_save_tw_cache(plate, is_in, record.create_time, record.last_time, record.bill_count, photo_count, record.tb_num, record.generation);
+		
+		return true; // 允许创建新工单
+	}
+	
+	if (record.bill_count >= MAX_BILL_COUNT_PER_WINDOW) {
+		std::cerr << plate << (is_in ? "进站" : "出站")
+			<< " " << g_time_window_min << "分钟内创建工单次数已达上限" << std::endl;
+		g_metrics.time_window_hits++;
+		return false;
+	}
+	return true;
+}
+
+void increment_bill_count(const std::string& plate, bool is_in) {
+    std::lock_guard<std::mutex> lock(is_in ? g_in_record_mtx : g_out_record_mtx);
+    auto& records = is_in ? g_in_upload_records : g_out_upload_records;
+    
+    auto it = records.find(plate);
+    time_t now = time(NULL);
+    
+    if (it != records.end()) {
+        it->second.bill_count++;
+        it->second.last_time = now;
+        std::cout << "[配额更新] " << plate << (is_in ? "进站" : "出站") 
+                  << " 工单计数: " << it->second.bill_count << "/" << MAX_BILL_COUNT_PER_WINDOW << std::endl;
+    } else {
+        // ❌ Bug修复:如果记录不存在,创建新记录
+        SceneUploadRecord new_record;
+        new_record.bill_count = 1;
+        new_record.last_time = now;
+        new_record.create_time = now;
+        records[plate] = new_record;
+        std::cout << "[配额更新] " << plate << (is_in ? "进站" : "出站") 
+                  << " 创建新记录,工单计数: 1/" << MAX_BILL_COUNT_PER_WINDOW << std::endl;
+    }
+    
+    // ✅ TIME_WINDOW持久化:保存到数据库
+    auto& record = records[plate];
+    int photo_count = 0;
+    if (is_in) {
+        auto bill_it = g_in_bill_cache.find(plate);
+        photo_count = (bill_it != g_in_bill_cache.end()) ? bill_it->second.photo_count : 0;
+    } else {
+        auto bill_it = g_out_bill_cache.find(plate);
+        photo_count = (bill_it != g_out_bill_cache.end()) ? bill_it->second.photo_count : 0;
+    }
+    db_save_tw_cache(plate, is_in, record.create_time, record.last_time, record.bill_count, photo_count, record.tb_num, record.generation);
+}
+
+void cleanup_expired_upload_records_unlocked(bool is_in) {
+	time_t now = time(NULL);
+	const int RECORD_EXPIRE_TIME = g_time_window_min * 60;
+	auto& records = is_in ? g_in_upload_records : g_out_upload_records;
+	
+	// ✅ P1修复:不删除过期记录,让check_bill_allowed检查时间窗口
+	// 与 get_cached_in_bill/get_cached_out_bill 的逻辑保持一致
+	for (auto it = records.begin(); it != records.end(); ) {
+		time_t last_active = std::max(it->second.last_time, it->second.create_time);
+		if (difftime(now, last_active) >= 24 * 60 * 60) {
+			// ✅ v42 Bug#6修复:超过24小时的记录执行erase,防止内存无限增长
+			// TIME_WINDOW内的记录不删除,由check_bill_allowed处理过期重置
+			it = records.erase(it);
+		}
+		else if (difftime(now, last_active) >= RECORD_EXPIRE_TIME) {
+			// 超过TIME_WINDOW但不到24小时的记录,保留但重置计数
+			it->second.bill_count = 0;
+			it->second.photo_success_count = 0;
+			it->second.feishu_sent = false;
+			++it;
+		}
+		else {
+			++it;
+		}
+	}
+}
+
+// ==================== 联单缓存函数实现 ====================
+bool get_cached_in_bill(const std::string& plate, std::string& tb_num, int* photo_count, bool* bill_created) {
+	std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+	auto it = g_in_bill_cache.find(plate);
+	if (it != g_in_bill_cache.end()) {
+		time_t now = time(NULL);
+		// ✅ Bug修复:统一使用 < 判断未过期,>= 判断已过期
+		if ((now - it->second.create_time) < g_time_window_min * 60) {
+			tb_num = it->second.tb_num;
+			it->second.hit_count++;
+			if (photo_count != nullptr) *photo_count = it->second.photo_count;
+			if (bill_created != nullptr) *bill_created = it->second.bill_created;
+			g_metrics.record_cache_hit(true, true);
+			return true;
+		}
+		else {
+			// ✅ 核心修复:超过5分钟后,重置照片计数器,开启新周期
+			// ✅ 同时同步更新 g_in_upload_records,保持状态一致
+			{
+				std::lock_guard<std::mutex> lock_rec(g_in_record_mtx);
+				auto rec_it = g_in_upload_records.find(plate);
+				if (rec_it != g_in_upload_records.end()) {
+					rec_it->second.last_time = now;
+					rec_it->second.create_time = now;
+					rec_it->second.bill_count = 0;  // ✅ Bug修复:重置 bill_count,允许创建新工单
+				}
+			}
+			it->second.photo_count = 0;
+			it->second.tb_num = "";
+			it->second.bill_created = false;
+			it->second.create_time = now;
+			if (photo_count != nullptr) *photo_count = 0;
+			if (bill_created != nullptr) *bill_created = false;
+			g_metrics.record_cache_hit(true, false);
+			return false;
+		}
+	}
+	g_metrics.record_cache_hit(true, false);
+	return false;
+}
+
+void cache_in_bill(const std::string& plate, const std::string& tb_num) {
+	std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+	auto it = g_in_bill_cache.find(plate);
+	if (it != g_in_bill_cache.end()) {
+		it->second.tb_num = tb_num;
+		it->second.bill_created = true;
+		std::cout << "[DEBUG] " << plate << " 进站更新联单: " << tb_num << std::endl;
+	}
+	else {
+		g_in_bill_cache[plate] = { tb_num, time(NULL), 0, 0, true };
+		std::cout << "[DEBUG] " << plate << " 进站创建缓存: " << tb_num << std::endl;
+	}
+}
+
+void increment_in_photo_count(const std::string& plate) {
+	std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+	auto it = g_in_bill_cache.find(plate);
+	time_t now = time(NULL);
+	
+	if (it != g_in_bill_cache.end()) {
+		if (it->second.photo_count < g_max_photo_groups) {
+			it->second.photo_count++;
+			std::cout << "[DEBUG] 进站照片计数: " << plate << " -> "
+				<< it->second.photo_count << "/" << g_max_photo_groups << std::endl;
+		}
+	} else {
+		// ✅ P0修复:创建缓存条目并设置create_time,用于时间窗口检查
+		g_in_bill_cache[plate] = { "", now, 0, 1, false };
+		std::cout << "[DEBUG] 进站创建缓存: " << plate << " photo_count=1" << std::endl;
+	}
+	
+	// ✅ TIME_WINDOW持久化:保存到数据库
+	auto bill_it = g_in_bill_cache.find(plate);
+	if (bill_it != g_in_bill_cache.end()) {
+		// ✅ v43.2修复:访问g_in_upload_records时加锁,避免数据竞争
+		std::lock_guard<std::mutex> lock_rec(g_in_record_mtx);
+		auto& records = g_in_upload_records;
+		auto rec_it = records.find(plate);
+		int bill_count = (rec_it != records.end()) ? rec_it->second.bill_count : 0;
+		time_t create_time = (rec_it != records.end()) ? rec_it->second.create_time : now;
+		time_t last_time = (rec_it != records.end() && rec_it->second.last_time > 0) ? rec_it->second.last_time : now;
+		std::string tb_num = (rec_it != records.end()) ? rec_it->second.tb_num : "";
+		int generation = (rec_it != records.end() && rec_it->second.generation > 0) ? rec_it->second.generation : 1;
+		
+		if (rec_it == records.end()) {
+			SceneUploadRecord new_rec;
+			new_rec.tb_num = "";
+			new_rec.bill_created = false;
+			new_rec.create_time = now;
+			new_rec.last_time = now;
+			new_rec.bill_count = 0;
+			new_rec.generation = 1;
+			records[plate] = new_rec;
+			std::cout << "[DEBUG] 进站同步创建 g_in_upload_records: " << plate << " last_time=" << now << std::endl;
+		}
+		// ✅ v43.2修复:先收集参数再释放锁,避免g_in_bill_cache_mtx+g_in_record_mtx+g_db_mtx三重嵌套
+		db_save_tw_cache(plate, true, create_time, last_time, bill_count, bill_it->second.photo_count, tb_num, generation);
+	}
+}
+
+int get_in_photo_count(const std::string& plate) {
+	std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+	auto it = g_in_bill_cache.find(plate);
+	return it != g_in_bill_cache.end() ? it->second.photo_count : 0;
+}
+
+bool get_cached_out_bill(const std::string& plate, std::string& tb_num, int* photo_count, bool* bill_created) {
+	std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+	auto it = g_out_bill_cache.find(plate);
+	if (it != g_out_bill_cache.end()) {
+		time_t now = time(NULL);
+		// ✅ Bug修复:统一使用 < 判断未过期,>= 判断已过期
+		if ((now - it->second.create_time) < g_time_window_min * 60) {
+			tb_num = it->second.tb_num;
+			it->second.hit_count++;
+			if (photo_count != nullptr) *photo_count = it->second.photo_count;
+			if (bill_created != nullptr) *bill_created = it->second.bill_created;
+			g_metrics.record_cache_hit(false, true);
+			return true;
+		}
+		else {
+			// ✅ 核心修复:超过5分钟后,重置照片计数器,开启新周期
+			// ✅ 同时同步更新 g_out_upload_records,保持状态一致
+			{
+				std::lock_guard<std::mutex> lock_rec(g_out_record_mtx);
+				auto rec_it = g_out_upload_records.find(plate);
+				if (rec_it != g_out_upload_records.end()) {
+					rec_it->second.last_time = now;
+					rec_it->second.create_time = now;
+					rec_it->second.bill_count = 0;  // ✅ Bug修复:重置 bill_count,允许创建新工单
+				}
+			}
+			it->second.photo_count = 0;
+			it->second.tb_num = "";
+			it->second.bill_created = false;
+			it->second.create_time = now;
+			if (photo_count != nullptr) *photo_count = 0;
+			if (bill_created != nullptr) *bill_created = false;
+			g_metrics.record_cache_hit(false, false);
+			return false;
+		}
+	}
+	g_metrics.record_cache_hit(false, false);
+	return false;
+}
+
+void cache_out_bill(const std::string& plate, const std::string& tb_num) {
+	std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+	auto it = g_out_bill_cache.find(plate);
+	if (it != g_out_bill_cache.end()) {
+		it->second.tb_num = tb_num;
+		it->second.bill_created = true;
+		std::cout << "[DEBUG] " << plate << " 出站更新联单: " << tb_num << std::endl;
+	}
+	else {
+		g_out_bill_cache[plate] = { tb_num, time(NULL), 0, 0, true };
+		std::cout << "[DEBUG] " << plate << " 出站创建缓存: " << tb_num << std::endl;
+	}
+}
+
+void increment_out_photo_count(const std::string& plate) {
+	std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+	auto it = g_out_bill_cache.find(plate);
+	time_t now = time(NULL);
+	
+	if (it != g_out_bill_cache.end()) {
+		if (it->second.photo_count < g_max_photo_groups) {
+			it->second.photo_count++;
+			std::cout << "[DEBUG] 出站照片计数: " << plate << " -> "
+				<< it->second.photo_count << "/" << g_max_photo_groups << std::endl;
+		}
+	} else {
+		// ✅ P0修复:创建缓存条目并设置create_time,用于时间窗口检查
+		g_out_bill_cache[plate] = { "", now, 0, 1, false };
+		std::cout << "[DEBUG] 出站创建缓存: " << plate << " photo_count=1" << std::endl;
+	}
+	
+	// ✅ TIME_WINDOW持久化:保存到数据库
+	auto bill_it = g_out_bill_cache.find(plate);
+	if (bill_it != g_out_bill_cache.end()) {
+		// ✅ v43.2修复:访问g_out_upload_records时加锁,避免数据竞争
+		std::lock_guard<std::mutex> lock_rec(g_out_record_mtx);
+		auto& records = g_out_upload_records;
+		auto rec_it = records.find(plate);
+		int bill_count = (rec_it != records.end()) ? rec_it->second.bill_count : 0;
+		time_t create_time = (rec_it != records.end()) ? rec_it->second.create_time : now;
+		time_t last_time = (rec_it != records.end() && rec_it->second.last_time > 0) ? rec_it->second.last_time : now;
+		std::string tb_num = (rec_it != records.end()) ? rec_it->second.tb_num : "";
+		int generation = (rec_it != records.end() && rec_it->second.generation > 0) ? rec_it->second.generation : 1;
+		
+		if (rec_it == records.end()) {
+			SceneUploadRecord new_rec;
+			new_rec.tb_num = "";
+			new_rec.bill_created = false;
+			new_rec.create_time = now;
+			new_rec.last_time = now;
+			new_rec.bill_count = 0;
+			new_rec.generation = 1;
+			records[plate] = new_rec;
+			std::cout << "[DEBUG] 出站同步创建 g_out_upload_records: " << plate << " last_time=" << now << std::endl;
+		}
+		// ✅ v43.2修复:先收集参数再释放锁
+		db_save_tw_cache(plate, false, create_time, last_time, bill_count, bill_it->second.photo_count, tb_num, generation);
+	}
+}
+
+int get_out_photo_count(const std::string& plate) {
+	std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+	auto it = g_out_bill_cache.find(plate);
+	return it != g_out_bill_cache.end() ? it->second.photo_count : 0;
+}
+
+void cleanup_bill_cache_unlocked(bool is_in) {
+	// ❌ Bug修复:不删除过期缓存,让 get_cached_in_bill/out_bill 自然处理过期重置
+	// 与 cleanup_expired_upload_records_unlocked 保持一致
+	// 仅清理长时间无活动的孤立记录(如超过24小时)
+	time_t now = time(NULL);
+	const int LONG_TIMEOUT = 24 * 60 * 60; // 24小时
+	if (is_in) {
+		for (auto it = g_in_bill_cache.begin(); it != g_in_bill_cache.end(); ) {
+			if ((now - it->second.create_time) > LONG_TIMEOUT) {
+				std::cout << "[清理] 进站缓存过期24小时: " << it->first << std::endl;
+				it = g_in_bill_cache.erase(it);
+			}
+			else {
+				++it;
+			}
+		}
+	}
+	else {
+		for (auto it = g_out_bill_cache.begin(); it != g_out_bill_cache.end(); ) {
+			if ((now - it->second.create_time) > LONG_TIMEOUT) {
+				std::cout << "[清理] 出站缓存过期24小时: " << it->first << std::endl;
+				it = g_out_bill_cache.erase(it);
+			}
+			else {
+				++it;
+			}
+		}
+	}
+}
+
+UploadError classify_upload_error(CURLcode res, int http_code) {
+	if (res == CURLE_OPERATION_TIMEDOUT) return UploadError::NETWORK_TIMEOUT;
+	if (res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST) return UploadError::NETWORK_CONNECTION_FAILED;
+	if (http_code >= 500) return UploadError::SERVER_ERROR_5XX;
+	if (http_code == 401 || http_code == 403) return UploadError::AUTH_FAILED;
+	if (http_code >= 400 && http_code < 500) return UploadError::SERVER_ERROR_4XX;
+	if (res != CURLE_OK) return UploadError::UNKNOWN_ERROR;
+	return UploadError::SUCCESS;
+}
+
+RecoveryStrategy get_recovery_strategy(UploadError error) {
+	switch (error) {
+	case UploadError::NETWORK_TIMEOUT: return { 2, 1000, true };
+	case UploadError::NETWORK_CONNECTION_FAILED: return { 2, 2000, true };
+	case UploadError::SERVER_ERROR_5XX: return { 1, 1500, true };
+	default: return { 0, 0, false };
+	}
+}
+
+bool check_bill_allowed(const std::string& plate, bool is_in) {
+	// ✅ Bug修复:按固定顺序获取锁,避免嵌套加锁导致死锁
+	// 顺序:先 g_bill_cache_mtx,后 g_record_mtx
+	if (is_in) {
+		std::lock_guard<std::mutex> lock_bill(g_in_bill_cache_mtx);
+		std::lock_guard<std::mutex> lock_record(g_in_record_mtx);
+		cleanup_expired_upload_records_unlocked(is_in);
+		return check_bill_allowed_unlocked(plate, is_in);
+	} else {
+		std::lock_guard<std::mutex> lock_bill(g_out_bill_cache_mtx);
+		std::lock_guard<std::mutex> lock_record(g_out_record_mtx);
+		cleanup_expired_upload_records_unlocked(is_in);
+		return check_bill_allowed_unlocked(plate, is_in);
+	}
+}
+
+RetryResult enhanced_retry_record(int db_id) {
+    RetryResult result;
+    
+    // 1. 从数据库读取记录
+    DbRecord rec = db_get_record(db_id);
+    if (rec.id == 0) {
+        result.message = "记录不存在: ID=" + std::to_string(db_id);
+        return result;
+    }
+    
+    bool is_in = (rec.station_type == 1);
+    
+    // 2. 检查联单编号,如果没有则重新获取
+    if (rec.tb_num.empty() || rec.tb_num == "0") {
+        // 需要重新创建工单获取联单编号
+        if (!check_bill_allowed(rec.plate_number, is_in)) {
+            result.message = "时间窗口内工单数量已达上限";
+            return result;
+        }
+        
+        // 创建工单请求
+        auto plate_info = std::make_shared<CarPlateInfo>();
+        plate_info->code = rec.plate_number;
+        plate_info->is_inbound = is_in;
+        plate_info->is_outbound = !is_in;
+        plate_info->tb_num = "";
+        
+        CaptureInfo cap_info;
+        cap_info.dtype = is_in ? STATION_IN : STATION_OUT;
+        cap_info.gtype = POSITION_LO;
+        plate_info->cap_info_copy = std::make_unique<CaptureInfo>(cap_info);
+        
+        std::string new_tb_num;
+        int res = lib_curl_create_bill_request(plate_info, new_tb_num);
+        if (res != 0) {
+            result.message = "创建工单失败: 错误码=" + std::to_string(res);
+            db_increment_retry_count(db_id);
+            return result;
+        }
+        
+        // 更新数据库tb_num
+        db_update_tb_num(db_id, new_tb_num);
+        result.tb_num = new_tb_num;
+        std::cout << "[Web重试] " << rec.plate_number << " 获取联单编号: " 
+                  << new_tb_num << std::endl;
+    }
+    
+    // 3. 重新上传失败的照片
+    // 读取最新记录(tb_num可能已更新)
+    rec = db_get_record(db_id);
+    
+    // 3.1 上传低位照片(如果失败)
+    if (rec.lo_upload_status != 1 && !rec.lo_photo_path.empty()) {
+        auto plate_info = std::make_shared<CarPlateInfo>();
+        plate_info->code = rec.plate_number;
+        plate_info->tb_num = rec.tb_num;
+        plate_info->pic_path_front = rec.lo_photo_path;
+        plate_info->is_inbound = (rec.station_type == 1);
+        plate_info->is_outbound = (rec.station_type != 1);
+        
+        CaptureInfo cap_info;
+        cap_info.dtype = (rec.station_type == 1) ? STATION_IN : STATION_OUT;
+        cap_info.gtype = POSITION_LO;
+        plate_info->cap_info_copy = std::make_unique<CaptureInfo>(cap_info);
+        
+        bool lo_res = lib_curl_image_upload_request(plate_info, POSITION_LO);
+        if (lo_res) {
+            db_update_upload_status_side(db_id, "lo", 1);
+            result.uploaded_photos++;
+            std::cout << "[Web重试] " << rec.plate_number 
+                      << " 低位照片上传成功" << std::endl;
+        } else {
+            std::cerr << "[Web重试] " << rec.plate_number 
+                      << " 低位照片上传失败" << std::endl;
+        }
+    }
+    
+    // 3.2 上传高位照片(如果失败)
+    if (rec.hi_upload_status != 1 && !rec.hi_photo_path.empty()) {
+        auto plate_info = std::make_shared<CarPlateInfo>();
+        plate_info->code = rec.plate_number;
+        plate_info->tb_num = rec.tb_num;
+        plate_info->pic_path_side = rec.hi_photo_path;
+        plate_info->is_inbound = (rec.station_type == 1);
+        plate_info->is_outbound = (rec.station_type != 1);
+        
+        CaptureInfo cap_info;
+        cap_info.dtype = (rec.station_type == 1) ? STATION_IN : STATION_OUT;
+        cap_info.gtype = POSITION_HI;
+        plate_info->cap_info_copy = std::make_unique<CaptureInfo>(cap_info);
+        
+        bool hi_res = lib_curl_image_upload_request(plate_info, POSITION_HI);
+        if (hi_res) {
+            db_update_upload_status_side(db_id, "hi", 1);
+            result.uploaded_photos++;
+            std::cout << "[Web重试] " << rec.plate_number 
+                      << " 高位照片上传成功" << std::endl;
+        } else {
+            std::cerr << "[Web重试] " << rec.plate_number 
+                      << " 高位照片上传失败" << std::endl;
+        }
+    }
+    
+    // 4. 更新重试次数
+    db_increment_retry_count(db_id);
+    
+    // 5. 判断最终结果
+    rec = db_get_record(db_id);  // 重新读取
+    if (rec.lo_upload_status == 1 && rec.hi_upload_status == 1 && !rec.tb_num.empty() && rec.tb_num != "0") {
+        result.success = true;
+        result.message = "重试成功: 联单编号=" + rec.tb_num 
+                       + " 上传照片=" + std::to_string(result.uploaded_photos) + "张";
+    } else {
+        result.message = "部分成功: 低位=" + std::to_string(rec.lo_upload_status) 
+                       + " 高位=" + std::to_string(rec.hi_upload_status)
+                       + " 联单=" + (rec.tb_num.empty() ? "无" : rec.tb_num)
+                       + " 上传照片=" + std::to_string(result.uploaded_photos) + "张";
+    }
+    
+    return result;
+}
+
+std::string get_format_time(void)
+{
+	char date[32];
+	struct timeval tv;
+	gettimeofday(&tv, NULL);
+	struct tm tm_buf;
+	struct tm* now = localtime_r(&tv.tv_sec, &tm_buf);
+	if (!now) {
+		return std::string("1970-01-01 00:00:00");
+	}
+	strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", now);
+	return std::string(date);
+}
+
+std::string get_tenant_token() {
+	std::string token;
+	cJSON* root = cJSON_CreateObject();
+	cJSON_AddStringToObject(root, "app_id", g_feishu_app_id.c_str());
+	cJSON_AddStringToObject(root, "app_secret", g_feishu_app_secret.c_str());
+	char* post_data = cJSON_PrintUnformatted(root);
+
+	const int MAX_RETRY = 4;  // 初始请求 + 3次重试 = 4次请求
+	for (int retry = 0; retry < MAX_RETRY; retry++) {
+		if (retry > 0) {
+			int backoff_seconds = (1 << retry);
+			std::cerr << "[飞书token] 重试第" << 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) continue;
+
+		std::string response;
+		struct curl_slist* headers = curl_slist_append(NULL, "Content-Type: application/json");
+		InitCurlCommon(curl, &response);
+		curl_easy_setopt(curl, CURLOPT_URL, "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal");
+		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+		curl_easy_setopt(curl, CURLOPT_POST, 1L);
+		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+		CURLcode res = curl_easy_perform(curl);
+		if (res == CURLE_OK) {
+			cJSON* resp_json = cJSON_Parse(response.c_str());
+			if (resp_json) {
+				std::cout << "飞书token接口响应:token长度=" << (response.length() > 50 ? response.substr(0, 20) + "..." : "***") << std::endl;
+				cJSON* token_obj = cJSON_GetObjectItemCaseSensitive(resp_json, "tenant_access_token");
+				cJSON* code_obj = cJSON_GetObjectItemCaseSensitive(resp_json, "code");
+				if (code_obj && code_obj->valueint != 0) {
+					std::cerr << "飞书token接口返回错误码:" << code_obj->valueint;
+					cJSON* msg_obj = cJSON_GetObjectItem(resp_json, "msg");
+					if (msg_obj && cJSON_IsString(msg_obj)) {
+						std::cerr << ",msg:" << msg_obj->valuestring;
+					}
+					std::cerr << std::endl;
+				}
+				if (token_obj && cJSON_IsString(token_obj)) {
+					token = token_obj->valuestring;
+				}
+				cJSON_Delete(resp_json);
+			}
+			curl_slist_free_all(headers);
+			curl_easy_cleanup(curl);
+			break;
+		}
+
+		std::cerr << "[飞书token] CURL请求失败:" << curl_easy_strerror(res) << std::endl;
+		curl_slist_free_all(headers);
+		curl_easy_cleanup(curl);
+		if (res != CURLE_OPERATION_TIMEDOUT && res != CURLE_COULDNT_CONNECT) {
+			break;
+		}
+	}
+
+	cJSON_Delete(root);
+	free(post_data);
+	return token;
+}
+
+int read_cpu_serial(char* serial, size_t max_len) {
+	FILE* fp = fopen("/proc/cpuinfo", "r");
+	if (!fp) {
+		perror("无法打开/proc/cpuinfo");
+		return -1;
+	}
+
+	char line[256];
+	while (fgets(line, sizeof(line), fp)) {
+		if (strncmp(line, "Serial", 6) == 0) {
+			char* colon = strchr(line, ':');
+			if (colon) {
+				char* trimmed = a_trim(serial, colon + 1);
+				// 确保以00000000开头(树莓派格式)
+				if (strlen(trimmed) >= 8) {
+					fclose(fp);
+					return 0;
+				}
+			}
+		}
+	}
+
+	fclose(fp);
+	// 如果读取失败,使用备用方法生成唯一ID
+	snprintf(serial, max_len, "00000000%08x", (unsigned int)time(NULL));
+	return 0;
+}
+
+int read_config_md5(const char* filename, char* md5_value, size_t max_len) {
+	FILE* fp = fopen(filename, "r");
+	if (!fp) {
+		perror("无法打开 config.txt");
+		return -1;
+	}
+
+	char line[256];
+	while (fgets(line, sizeof(line), fp)) {
+		char* trimmed = a_trim(line, line);
+		if (strlen(trimmed) == 32) {
+			strncpy(md5_value, trimmed, max_len - 1);
+			md5_value[max_len - 1] = '\0';
+			fclose(fp);
+			return 0;
+		}
+	}
+
+	fclose(fp);
+	return -1;
+}
+/*
+// MD5 工具函数(完整实现)
+void md5_hexEx1(const char* input, char* output) {
+	unsigned char digest[16];
+	MD5_CTX ctx;
+	MD5_Init(&ctx);
+	MD5_Update(&ctx, input, strlen(input));
+	MD5_Final(digest, &ctx);
+
+	for (int i = 0; i < 16; i++) {
+		sprintf(output + i * 2, "%02x", digest[i]);
+	}
+	output[32] = '\0';
+}
+
+void md5_tripleEx1(const char* input, char* output) {
+	char md5_1[33], md5_2[33];
+	md5_hexEx1(input, md5_1);
+	md5_hexEx1(md5_1, md5_2);
+	md5_hexEx1(md5_2, output);
+}
+*/
+// 字符串处理函数完整实现
+char* l_trim(char* szOutput, const char* szInput) {
+	const char* p = szInput;
+	while (*p != '\0' && isspace((unsigned char)*p)) {
+		p++;
+	}
+	strcpy(szOutput, p);
+	return szOutput;
+}
+
+char* r_trim(char* szOutput, const char* szInput) {
+	strcpy(szOutput, szInput);
+	char* p = szOutput + strlen(szOutput) - 1;
+	while (p >= szOutput && isspace((unsigned char)*p)) {
+		*p = '\0';
+		p--;
+	}
+	return szOutput;
+}
+
+char* a_trim(char* szOutput, const char* szInput) {
+	char szTemp[KEYVALLEN] = { 0 };
+	l_trim(szTemp, szInput);
+	r_trim(szOutput, szTemp);
+	return szOutput;
+}
+/*
+int GetProfileString(char* profile, char* AppName, char* KeyName, char* KeyVal) {
+	FILE* fp;
+	char szLine[KEYVALLEN] = { 0 };
+	char szAppName[KEYVALLEN] = { 0 };
+	char szKeyName[KEYVALLEN] = { 0 };
+	char* cTemp;
+	int iAppNameLen = strlen(AppName);
+	int iKeyNameLen = strlen(KeyName);
+
+	sprintf(szAppName, "[%s]", AppName);
+	if ((fp = fopen(profile, "r")) == NULL) {
+		return -1;
+	}
+
+	while (!feof(fp)) {
+		if (fgets(szLine, KEYVALLEN, fp) == NULL) {
+			break;
+		}
+
+		if (strncmp(szLine, szAppName, iAppNameLen) == 0) {
+			while (!feof(fp)) {
+				if (fgets(szLine, KEYVALLEN, fp) == NULL) {
+					break;
+				}
+				if (*szLine == '[') {
+					break;
+				}
+				if (*szLine == ';' || *szLine == '#') {
+					continue;
+				}
+				if ((cTemp = strchr(szLine, '=')) != NULL) {
+					*cTemp = '\0';
+					l_trim(szKeyName, szLine);
+					if (strcmp(szKeyName, KeyName) == 0) {
+						a_trim(KeyVal, cTemp + 1);
+						fclose(fp);
+						return 0;
+					}
+				}
+			}
+			break;
+		}
+	}
+
+	fclose(fp);
+	return -1;
+}
+*/
+size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, std::string* str) {
+	size_t realsize = size * nmemb;
+	try {
+		str->append((char*)contents, realsize);
+	} catch (const std::bad_alloc& e) {
+		std::cerr << "WriteMemoryCallback: 内存分配失败: " << e.what() << std::endl;
+		return 0;
+	}
+	return realsize;
+}
+
+
+// ✅ fix14新增: 重置指定车牌的bill cache,用于交替锁定新周期开始时同步清理
+// 当AlternatingMerge=1且交替锁定允许新操作时,调用此函数重置bill cache
+// 确保photo_count归零,允许新周期保存照片和创建新工单
+void reset_bill_cache_for_plate(const std::string& plate, bool is_in) {
+	time_t now = time(NULL);
+	if (is_in) {
+		std::lock_guard<std::mutex> lock(g_in_bill_cache_mtx);
+		auto it = g_in_bill_cache.find(plate);
+		if (it != g_in_bill_cache.end()) {
+			it->second.photo_count = 0;
+			it->second.tb_num = "";
+			it->second.bill_created = false;
+			it->second.create_time = now;
+			it->second.hit_count = 0;
+			std::cout << "[bill cache重置] " << plate << " 进站 photo_count→0" << std::endl;
+		}
+	} else {
+		std::lock_guard<std::mutex> lock(g_out_bill_cache_mtx);
+		auto it = g_out_bill_cache.find(plate);
+		if (it != g_out_bill_cache.end()) {
+			it->second.photo_count = 0;
+			it->second.tb_num = "";
+			it->second.bill_created = false;
+			it->second.create_time = now;
+			it->second.hit_count = 0;
+			std::cout << "[bill cache重置] " << plate << " 出站 photo_count→0" << std::endl;
+		}
+	}
+	
+	// 同步重置upload record的bill_count,允许创建新工单
+	{
+		auto& mtx = is_in ? g_in_record_mtx : g_out_record_mtx;
+		auto& records = is_in ? g_in_upload_records : g_out_upload_records;
+		std::lock_guard<std::mutex> lock(mtx);
+		auto rec_it = records.find(plate);
+		if (rec_it != records.end()) {
+			rec_it->second.bill_count = 0;
+			rec_it->second.last_time = now;
+			rec_it->second.create_time = now;
+			rec_it->second.photo_success_count = 0;
+			rec_it->second.feishu_sent = false;
+			rec_it->second.generation++;
+		}
+	}
+}
+
+// ==================== 每日文件清理(fix19重写:容量+天数) ====================
+#include <dirent.h>
+#include <set>
+#include <algorithm>
+
+/**
+ * 递归收集目录下所有.jpg文件的信息
+ * 返回: vector<pair<完整路径, pair<修改时间, 文件大小>>>
+ */
+static void collect_jpg_files(const std::string& dir_path,
+                               std::vector<std::pair<std::string, std::pair<time_t, long long>>>& files) {
+    DIR* dir = opendir(dir_path.c_str());
+    if (!dir) return;
+
+    struct dirent* entry;
+    while ((entry = readdir(dir)) != nullptr) {
+        std::string name = entry->d_name;
+        if (name == "." || name == "..") continue;
+
+        std::string full_path = dir_path + name;
+
+        if (entry->d_type == DT_DIR) {
+            std::string sub_path = full_path + "/";
+            collect_jpg_files(sub_path, files);
+        }
+        else if (entry->d_type == DT_REG) {
+            if (name.size() < 4) continue;
+            std::string ext = name.substr(name.size() - 4);
+            for (char& c : ext) c = tolower(c);
+            if (ext != ".jpg") continue;
+
+            struct stat st;
+            if (stat(full_path.c_str(), &st) == 0) {
+                files.emplace_back(full_path, std::make_pair(st.st_mtime, st.st_size));
+            }
+        }
+    }
+    closedir(dir);
+}
+
+/**
+ * 递归计算目录下.jpg文件总大小
+ */
+static long long calc_dir_jpg_size(const std::string& dir_path) {
+    std::vector<std::pair<std::string, std::pair<time_t, long long>>> files;
+    collect_jpg_files(dir_path, files);
+    long long total = 0;
+    for (const auto& f : files) {
+        total += f.second.second;
+    }
+    return total;
+}
+
+/**
+ * 尝试删除空子目录
+ */
+static void remove_empty_dirs(const std::string& dir_path) {
+    DIR* dir = opendir(dir_path.c_str());
+    if (!dir) return;
+
+    struct dirent* entry;
+    while ((entry = readdir(dir)) != nullptr) {
+        std::string name = entry->d_name;
+        if (name == "." || name == "..") continue;
+
+        std::string full_path = dir_path + name;
+
+        if (entry->d_type == DT_DIR) {
+            std::string sub_path = full_path + "/";
+            remove_empty_dirs(sub_path);
+            rmdir(full_path.c_str());  // 仅删除空目录
+        }
+    }
+    closedir(dir);
+}
+
+/**
+ * 每日照片清理 — 容量阈值模式(fix19重写)
+ * 
+ * 策略:PlateJPG/目录下.jpg文件总大小超过 g_photo_max_capacity_mb MB时,
+ * 按修改时间从旧到新删除,直到总大小降至阈值以下。
+ * 默认阈值1GB(1024MB),可通过config.ini的PhotoMaxCapacityMB配置。
+ */
+void daily_file_cleanup() {
+    std::string photo_dir = pathComm + "PlateJPG/";
+    long long threshold_bytes = g_photo_max_capacity_mb * 1024LL * 1024LL;
+
+    // 收集所有.jpg文件信息
+    std::vector<std::pair<std::string, std::pair<time_t, long long>>> files;
+    collect_jpg_files(photo_dir, files);
+
+    // 计算总大小
+    long long total_bytes = 0;
+    for (const auto& f : files) {
+        total_bytes += f.second.second;
+    }
+
+    std::cout << "[照片清理] PlateJPG/ 当前 " << (total_bytes / 1024 / 1024) << " MB / "
+              << g_photo_max_capacity_mb << " MB 阈值,共 " << files.size() << " 个文件" << std::endl;
+
+    // 未超阈值,无需清理
+    if (total_bytes <= threshold_bytes) {
+        std::cout << "[照片清理] 容量未超阈值,无需清理" << std::endl;
+        return;
+    }
+
+    // 按修改时间排序(旧的排前面)
+    std::sort(files.begin(), files.end(),
+        [](const auto& a, const auto& b) {
+            return a.second.first < b.second.first;
+        });
+
+    // 从最旧的文件开始删除,直到总大小降至阈值以下
+    int deleted_count = 0;
+    long long freed_bytes = 0;
+    for (const auto& f : files) {
+        if (total_bytes <= threshold_bytes) break;
+        if (unlink(f.first.c_str()) == 0) {
+            total_bytes -= f.second.second;
+            freed_bytes += f.second.second;
+            deleted_count++;
+        }
+    }
+
+    // 清理空子目录
+    remove_empty_dirs(photo_dir);
+
+    std::cout << "[照片清理] 删除 " << deleted_count << " 张照片(按时间从旧到新),释放 "
+              << (freed_bytes / 1024 / 1024) << " MB,当前 "
+              << (total_bytes / 1024 / 1024) << " MB" << std::endl;
+}
+
+/**
+ * 每日日志清理 — 天数模式(fix19新增)
+ * 
+ * 策略:
+ * 1. 扫描PlateRecApp.log-*和PlateRecApp.log-*.gz文件,删除超过g_log_retention_days天的归档日志
+ * 2. 对当前PlateRecApp.log执行copytruncate(复制→清空),防止日志无限增长
+ *    copytruncate: 复制当前日志→PlateRecApp.log-YYYYMMDD,然后清空原文件
+ *    程序stdout重定向到该文件,清空后新写入从头开始(O_APPEND模式)
+ * 默认30天,可通过config.ini的LogRetentionDays配置。
+ */
+void daily_log_cleanup() {
+    std::string log_dir = pathComm;
+    time_t now = time(NULL);
+    time_t cutoff = now - (time_t)g_log_retention_days * 86400;
+    struct tm tm_now;
+    localtime_r(&now, &tm_now);
+
+    int deleted_count = 0;
+    long long freed_bytes = 0;
+
+    // 1. 扫描并删除过期的归档日志文件(PlateRecApp.log-YYYYMMDD 或 .log-YYYYMMDD.gz)
+    DIR* dir = opendir(log_dir.c_str());
+    if (dir) {
+        struct dirent* entry;
+        while ((entry = readdir(dir)) != nullptr) {
+            std::string name = entry->d_name;
+            if (name == "." || name == "..") continue;
+
+            // 匹配 PlateRecApp.log- 开头的文件
+            if (name.find("PlateRecApp.log-") != 0) continue;
+            if (entry->d_type != DT_REG) continue;
+
+            std::string full_path = log_dir + name;
+            struct stat st;
+            if (stat(full_path.c_str(), &st) == 0 && st.st_mtime < cutoff) {
+                freed_bytes += st.st_size;
+                unlink(full_path.c_str());
+                deleted_count++;
+                std::cout << "[日志清理] 删除过期归档: " << name << std::endl;
+            }
+        }
+        closedir(dir);
+    }
+
+    // 2. copytruncate当前日志文件
+    std::string log_path = log_dir + "PlateRecApp.log";
+    struct stat log_st;
+    if (stat(log_path.c_str(), &log_st) == 0 && log_st.st_size > 0) {
+        // 只在日志文件>1MB时执行copytruncate,避免空文件浪费
+        if (log_st.st_size > 1024 * 1024) {
+            // 生成日期后缀
+            char date_suffix[16];
+            strftime(date_suffix, sizeof(date_suffix), "%Y%m%d", &tm_now);
+            std::string backup_path = log_path + "-" + date_suffix;
+
+            // 检查备份文件是否已存在(避免同一天重复copytruncate覆盖)
+            struct stat backup_st;
+            if (stat(backup_path.c_str(), &backup_st) != 0) {
+                // 备份文件不存在,执行copytruncate
+                // 步骤1: 复制当前日志到备份文件
+                std::ifstream src(log_path, std::ios::binary);
+                std::ofstream dst(backup_path, std::ios::binary);
+                if (src.is_open() && dst.is_open()) {
+                    dst << src.rdbuf();
+                    src.close();
+                    dst.close();
+
+                    // 步骤2: 清空当前日志文件(truncate)
+                    if (truncate(log_path.c_str(), 0) == 0) {
+                        std::cout << "[日志清理] copytruncate: " << log_path 
+                                  << " → " << backup_path 
+                                  << " (" << (log_st.st_size / 1024 / 1024) << " MB)" << std::endl;
+                    } else {
+                        std::cerr << "[日志清理] truncate失败: " << strerror(errno) << std::endl;
+                        // truncate失败,删除备份避免重复
+                        unlink(backup_path.c_str());
+                    }
+                } else {
+                    std::cerr << "[日志清理] 复制日志文件失败" << std::endl;
+                    if (dst.is_open()) dst.close();
+                    if (src.is_open()) src.close();
+                }
+            } else {
+                std::cout << "[日志清理] 今日备份已存在,跳过copytruncate" << std::endl;
+            }
+        } else {
+            std::cout << "[日志清理] 当前日志仅 " << (log_st.st_size / 1024) 
+                      << " KB,无需轮转" << std::endl;
+        }
+    }
+
+    if (deleted_count > 0) {
+        std::cout << "[日志清理] 删除 " << deleted_count << " 个过期归档(超过"
+                  << g_log_retention_days << "天),释放 "
+                  << (freed_bytes / 1024 / 1024) << " MB" << std::endl;
+    } else {
+        std::cout << "[日志清理] 无过期归档需要清理" << std::endl;
+    }
+}
+
+// ==================== ROI配置写回config.ini(fix18新增) ====================
+
+bool save_roi_to_config(const std::string& config_path, const std::string& side,
+                         int x, int y, int w, int h, bool enabled) {
+    if (config_path.empty()) {
+        std::cerr << "[ROI保存] config.ini路径为空,无法保存" << std::endl;
+        return false;
+    }
+    
+    std::ifstream ifs(config_path);
+    if (!ifs.is_open()) {
+        std::cerr << "[ROI保存] 无法打开配置文件: " << config_path << std::endl;
+        return false;
+    }
+    std::vector<std::string> lines;
+    std::string line;
+    while (std::getline(ifs, line)) {
+        lines.push_back(line);
+    }
+    ifs.close();
+    
+    std::string prefix = "roi_" + side + "_";
+    std::map<std::string, std::string> roi_values = {
+        {prefix + "x", std::to_string(x)},
+        {prefix + "y", std::to_string(y)},
+        {prefix + "w", std::to_string(w)},
+        {prefix + "h", std::to_string(h)},
+        {prefix + "enabled", std::to_string(enabled ? 1 : 0)}
+    };
+    
+    std::set<std::string> updated_keys;
+    for (auto& l : lines) {
+        std::string trimmed = l;
+        size_t start = trimmed.find_first_not_of(" \t");
+        if (start != std::string::npos) trimmed = trimmed.substr(start);
+        
+        for (auto& kv : roi_values) {
+            if (trimmed.find(kv.first + "=") == 0 || trimmed.find(kv.first + " =") == 0) {
+                l = kv.first + "=" + kv.second;
+                updated_keys.insert(kv.first);
+                break;
+            }
+        }
+    }
+    
+    for (auto& kv : roi_values) {
+        if (updated_keys.find(kv.first) == updated_keys.end()) {
+            lines.push_back(kv.first + "=" + kv.second);
+        }
+    }
+    
+    std::ofstream ofs(config_path);
+    if (!ofs.is_open()) {
+        std::cerr << "[ROI保存] 无法写入配置文件: " << config_path << std::endl;
+        return false;
+    }
+    for (const auto& l : lines) {
+        ofs << l << "\n";
+    }
+    ofs.close();
+    
+    std::cout << "[ROI保存] " << side << " ROI配置已写回config.ini: x=" << x 
+              << " y=" << y << " w=" << w << " h=" << h 
+              << " enabled=" << (enabled ? 1 : 0) << std::endl;
+    return true;
+}
+
+// fix21: 计算UTF-8字符串的字符数(中文字符3字节,英文/数字1字节)
+int count_utf8_chars(const char* str) {
+    if (!str) return 0;
+    int count = 0;
+    while (*str) {
+        unsigned char c = (unsigned char)*str;
+        if ((c & 0xC0) != 0x80) {  // 不是continuation byte
+            count++;
+        }
+        str++;
+    }
+    return count;
+}

+ 37 - 0
src/utils.h

@@ -0,0 +1,37 @@
+#ifndef UTILS_H
+#define UTILS_H
+#include "common.h"
+std::string get_format_time(void);
+bool get_cached_in_bill(const std::string& plate, std::string& tb_num, int* photo_count = nullptr, bool* bill_created = nullptr);
+void cache_in_bill(const std::string& plate, const std::string& tb_num);
+void increment_in_photo_count(const std::string& plate);
+int get_in_photo_count(const std::string& plate);
+bool get_cached_out_bill(const std::string& plate, std::string& tb_num, int* photo_count = nullptr, bool* bill_created = nullptr);
+void cache_out_bill(const std::string& plate, const std::string& tb_num);
+void increment_out_photo_count(const std::string& plate);
+int get_out_photo_count(const std::string& plate);
+void cleanup_bill_cache_unlocked(bool is_in);
+bool create_or_get_bill_cache_for_photo(const std::string& plate, bool is_in, std::string& tb_num);
+void update_bill_cache_tb_num(const std::string& plate, bool is_in, const std::string& tb_num);
+void load_special_plates(const std::string& config_path);
+bool check_bill_allowed(const std::string& plate, bool is_in);
+bool check_bill_allowed_unlocked(const std::string& plate, bool is_in);
+void increment_bill_count(const std::string& plate, bool is_in);
+void cleanup_expired_upload_records_unlocked(bool is_in);
+void reset_bill_cache_for_plate(const std::string& plate, bool is_in);
+UploadError classify_upload_error(CURLcode res, int http_code);
+RecoveryStrategy get_recovery_strategy(UploadError error);
+RetryResult enhanced_retry_record(int db_id);
+char* l_trim(char* szOutput, const char* szInput);
+char* r_trim(char* szOutput, const char* szInput);
+char* a_trim(char* szOutput, const char* szInput);
+int GetProfileString(char* profile, char* AppName, char* KeyName, char* KeyVal);
+int read_cpu_serial(char* serial, size_t max_len);
+int read_config_md5(const char* filename, char* md5_value, size_t max_len);
+void signal_handler(int signum);
+void daily_file_cleanup();
+void daily_log_cleanup();
+bool save_roi_to_config(const std::string& config_path, const std::string& side, int x, int y, int w, int h, bool enabled);
+// fix21: UTF-8字符计数(用于车牌长度校验)
+int count_utf8_chars(const char* str);
+#endif

+ 2174 - 0
src/web_server.cpp

@@ -0,0 +1,2174 @@
+/**
+ * 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"
+#include "auth_api.h"
+#include "system_monitor.h"
+#include "system_metrics_manager.h"
+#include "frpc_manager.h"
+#include "ssh_manager.h"
+#include "log_manager.h"
+#include "mqtt_client.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();
+}
+
+// fix24 v35: peek版本 — 不消费帧,供MJPEG流使用
+// 避免与识别线程竞争帧导致视频显示慢
+static cv::Mat peek_frame_by_stream(const std::string& stream) {
+    if (stream == "in-low") {
+        return plate_rec_app.capture_front_in->peekFrame();
+    } else if (stream == "in-high") {
+        return plate_rec_app.capture_side_in->peekFrame();
+    } else if (stream == "out-low") {
+        if (plate_rec_app.capture_front_out) {
+            return plate_rec_app.capture_front_out->peekFrame();
+        }
+        return plate_rec_app.capture_front_in->peekFrame();
+    } else if (stream == "out-high") {
+        return plate_rec_app.capture_side_out->peekFrame();
+    }
+    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;
+
+	// ✅ fix24 功能一:登录认证系统 - 注册认证中间件和API路由
+	if (g_auth_enabled) {
+		// 预路由认证中间件
+		svr.set_pre_routing_handler(g_auth_middleware->get_pre_routing_handler());
+		// 后路由安全头
+		svr.set_post_routing_handler(g_auth_middleware->get_post_routing_handler());
+		// 认证API路由
+		auth::register_auth_routes(svr);
+		// 登录页面
+		svr.Get("/login", [](const httplib::Request& req, httplib::Response& res) {
+			// 从 web 目录读取 login.html
+			std::ifstream f(pathComm + "assets/web/login.html");
+			if (f.is_open()) {
+				std::string html((std::istreambuf_iterator<char>(f)),
+				                  std::istreambuf_iterator<char>());
+				res.set_content(html, "text/html; charset=utf-8");
+			} else {
+				res.status = 404;
+				res.set_content("Login page not found", "text/plain");
+			}
+		});
+		std::cout << "[auth] 认证系统已启用" << std::endl;
+	}
+
+	// ==================== fix24 v24: 系统监控 API ====================
+	svr.Get("/api/system/status", [](const httplib::Request& req, httplib::Response& res) {
+		SystemStatus s = sysmon::get_system_status();
+		std::ostringstream json;
+		json << "{";
+		json << "\"cpu\":{\"usage\":" << std::fixed << std::setprecision(1) << s.cpu_usage_percent
+		     << ",\"cores\":" << s.cpu_core_count
+		     << ",\"load1\":" << s.cpu_load_1min << ",\"load5\":" << s.cpu_load_5min << ",\"load15\":" << s.cpu_load_15min << "},";
+		json << "\"memory\":{\"total\":" << s.mem_total_mb << ",\"used\":" << s.mem_used_mb
+		     << ",\"available\":" << s.mem_available_mb << ",\"percent\":" << std::fixed << std::setprecision(1) << s.mem_usage_percent << "},";
+		json << "\"temperature\":{\"value\":" << std::fixed << std::setprecision(1) << s.cpu_temp
+		     << ",\"available\":" << (s.temp_available ? "true" : "false")
+		     << ",\"alert_threshold\":" << g_sys_temp_alert_threshold << "},";
+		json << "\"disk\":{\"total\":" << s.disk_total_gb << ",\"used\":" << s.disk_used_gb
+		     << ",\"available\":" << s.disk_available_gb << ",\"percent\":" << std::fixed << std::setprecision(1) << s.disk_usage_percent << "},";
+		json << "\"system\":{\"hostname\":\"" << s.hostname << "\",\"kernel\":\"" << s.kernel_version
+		     << "\",\"uptime\":\"" << s.uptime_string << "\",\"uptime_seconds\":" << s.uptime_seconds << "},";
+		json << "\"process\":{\"pid\":" << s.plate_rec_pid << ",\"rss_mb\":" << s.plate_rec_rss_mb << "},";
+		json << "\"thresholds\":{\"cpu_alert\":" << g_sys_cpu_alert_threshold << ",\"temp_alert\":" << g_sys_temp_alert_threshold << "}";
+		json << "}";
+		res.set_content(json.str(), "application/json; charset=utf-8");
+	});
+
+	// 系统监控页面
+	svr.Get("/system", [](const httplib::Request& req, httplib::Response& res) {
+		std::string html_path = pathComm + "assets/web/system.html";
+		std::ifstream f(html_path);
+		if (f.is_open()) {
+			std::string html((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
+			res.set_content(html, "text/html; charset=utf-8");
+		} else {
+			res.status = 404;
+			res.set_content("System page not found", "text/plain");
+		}
+	});
+
+	// ==================== fix24 v24: frpc 管理 API ====================
+	svr.Get("/api/frpc/status", [](const httplib::Request& req, httplib::Response& res) {
+		FrpcStatus st = frpc_mgr::get_status();
+		std::ostringstream json;
+		json << "{\"running\":" << (st.running ? "true" : "false");
+		json << ",\"tunnels\":[";
+		for (size_t i = 0; i < st.tunnels.size(); i++) {
+			if (i > 0) json << ",";
+			json << "{\"name\":\"" << st.tunnels[i].name
+			     << "\",\"type\":\"" << st.tunnels[i].type
+			     << "\",\"local_addr\":\"" << st.tunnels[i].local_addr
+			     << "\",\"remote_addr\":\"" << st.tunnels[i].remote_addr
+			     << "\",\"status\":\"" << st.tunnels[i].status << "\"}";
+		}
+		json << "],\"error\":\"" << st.error << "\"}";
+		res.set_content(json.str(), "application/json; charset=utf-8");
+	});
+
+	svr.Get("/api/frpc/config", [](const httplib::Request& req, httplib::Response& res) {
+		std::string config = frpc_mgr::read_config();
+		if (config.empty()) {
+			res.status = 404;
+			res.set_content("{\"error\":\"无法读取 frpc 配置文件\"}", "application/json");
+		} else {
+			res.set_content(config, "text/plain; charset=utf-8");
+		}
+	});
+
+	svr.Post("/api/frpc/config", [](const httplib::Request& req, httplib::Response& res) {
+		std::string error;
+		if (frpc_mgr::save_config(req.body, error)) {
+			res.set_content("{\"success\":true,\"message\":\"配置已保存\"}", "application/json");
+		} else {
+			res.status = 500;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	svr.Post("/api/frpc/restart", [](const httplib::Request& req, httplib::Response& res) {
+		std::string error;
+		if (frpc_mgr::restart_service(error)) {
+			res.set_content("{\"success\":true,\"message\":\"frpc 重启成功\"}", "application/json");
+		} else {
+			res.status = 500;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	// frpc 管理页面
+	svr.Get("/frpc", [](const httplib::Request& req, httplib::Response& res) {
+		std::string html_path = pathComm + "assets/web/frpc.html";
+		std::ifstream f(html_path);
+		if (f.is_open()) {
+			std::string html((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
+			res.set_content(html, "text/html; charset=utf-8");
+		} else {
+			res.status = 404;
+			res.set_content("frpc page not found", "text/plain");
+		}
+	});
+
+	// ==================== fix24 v24: SSH Keys 管理 API ====================
+	svr.Get("/api/ssh/keys", [](const httplib::Request& req, httplib::Response& res) {
+		auto keys = ssh_mgr::list_keys();
+		std::ostringstream json;
+		json << "{\"keys\":[";
+		for (size_t i = 0; i < keys.size(); i++) {
+			if (i > 0) json << ",";
+			json << "{\"index\":" << keys[i].index
+			     << ",\"type\":\"" << keys[i].type
+			     << "\",\"comment\":\"" << keys[i].comment
+			     << "\",\"key_preview\":\"" << (keys[i].key_data.size() > 20 ? keys[i].key_data.substr(0, 20) + "..." : keys[i].key_data) << "\"}";
+		}
+		json << "]}";
+		res.set_content(json.str(), "application/json; charset=utf-8");
+	});
+
+	svr.Post("/api/ssh/keys", [](const httplib::Request& req, httplib::Response& res) {
+		// 解析 JSON body: {"key": "ssh-rsa AAAA... user@host"}
+		std::string error;
+		// 简单提取 key 字段
+		size_t key_pos = req.body.find("\"key\"");
+		if (key_pos == std::string::npos) {
+			res.status = 400;
+			res.set_content("{\"success\":false,\"error\":\"缺少 key 字段\"}", "application/json");
+			return;
+		}
+		size_t colon = req.body.find(':', key_pos);
+		size_t q1 = req.body.find('"', colon + 1);
+		size_t q2 = req.body.rfind('"');
+		if (q1 == std::string::npos || q2 == std::string::npos || q2 <= q1) {
+			res.status = 400;
+			res.set_content("{\"success\":false,\"error\":\"key 值解析失败\"}", "application/json");
+			return;
+		}
+		std::string key_value = req.body.substr(q1 + 1, q2 - q1 - 1);
+
+		if (ssh_mgr::add_key(key_value, error)) {
+			res.set_content("{\"success\":true,\"message\":\"公钥已添加\"}", "application/json");
+		} else {
+			res.status = 400;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	svr.Delete("/api/ssh/keys", [](const httplib::Request& req, httplib::Response& res) {
+		// 解析 JSON body: {"index": 0}
+		size_t idx_pos = req.body.find("\"index\"");
+		if (idx_pos == std::string::npos) {
+			res.status = 400;
+			res.set_content("{\"success\":false,\"error\":\"缺少 index 字段\"}", "application/json");
+			return;
+		}
+		size_t colon = req.body.find(':', idx_pos);
+		int index = atoi(req.body.c_str() + colon + 1);
+
+		std::string error;
+		if (ssh_mgr::delete_key(index, error)) {
+			res.set_content("{\"success\":true,\"message\":\"公钥已删除\"}", "application/json");
+		} else {
+			res.status = 400;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	// SSH Keys 管理页面
+	svr.Get("/ssh-keys", [](const httplib::Request& req, httplib::Response& res) {
+		std::string html_path = pathComm + "assets/web/ssh_keys.html";
+		std::ifstream f(html_path);
+		if (f.is_open()) {
+			std::string html((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
+			res.set_content(html, "text/html; charset=utf-8");
+		} else {
+			res.status = 404;
+			res.set_content("SSH Keys page not found", "text/plain");
+		}
+	});
+
+	// ==================== fix24 v24/v36: 日志管理 API ====================
+	svr.Get("/api/logs/stats", [](const httplib::Request& req, httplib::Response& res) {
+		LogStatus st = log_mgr::get_log_status();
+		std::ostringstream json;
+		json << "{\"file\":\"" << st.log_file_path << "\""
+		     << ",\"size_mb\":" << st.file_size_mb
+		     << ",\"buffer_mb\":" << st.buffer_size_mb
+		     << ",\"buffer_percent\":" << std::fixed << std::setprecision(1) << st.buffer_usage_percent
+		     << ",\"line_count\":" << st.line_count
+		     << ",\"flush_time\":\"" << st.flush_time << "\""
+		     << ",\"retention_days\":" << st.retention_days
+		     << ",\"last_modified\":\"" << st.last_modified << "\""
+		     << ",\"redirect\":" << (st.redirect_enabled ? "true" : "false")
+		     // ✅ fix24-v36: 内存日志缓冲新增字段
+		     << ",\"memory_enabled\":" << (st.memory_enabled ? "true" : "false")
+		     << ",\"memory_usage_bytes\":" << st.memory_usage_bytes
+		     << ",\"memory_usage_mb\":" << std::fixed << std::setprecision(2)
+		     << ((double)st.memory_usage_bytes / (1024.0 * 1024.0))
+		     << ",\"last_flush_time\":\"" << st.last_flush_time << "\""
+		     << ",\"total_flush_count\":" << st.total_flush_count
+		     << "}";
+		res.set_content(json.str(), "application/json; charset=utf-8");
+	});
+
+	svr.Post("/api/logs/flush", [](const httplib::Request& req, httplib::Response& res) {
+		std::string error;
+		if (log_mgr::flush_log(error)) {
+			res.set_content("{\"success\":true,\"message\":\"日志已刷新\"}", "application/json");
+		} else {
+			res.status = 500;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	svr.Post("/api/logs/cleanup", [](const httplib::Request& req, httplib::Response& res) {
+		std::string error;
+		if (log_mgr::cleanup_old_logs(error)) {
+			res.set_content("{\"success\":true,\"message\":\"过期日志已清理\"}", "application/json");
+		} else {
+			res.status = 500;
+			res.set_content("{\"success\":false,\"error\":\"" + error + "\"}", "application/json");
+		}
+	});
+
+	svr.Get("/api/logs/tail", [](const httplib::Request& req, httplib::Response& res) {
+		int lines = 200;
+		if (req.has_param("lines")) {
+			lines = std::min(atoi(req.get_param_value("lines").c_str()), 1000);
+		}
+		std::string content = log_mgr::tail_log(lines);
+		res.set_content(content, "text/plain; charset=utf-8");
+	});
+
+	// 日志管理页面
+	svr.Get("/logs", [](const httplib::Request& req, httplib::Response& res) {
+		std::string html_path = pathComm + "assets/web/logs.html";
+		std::ifstream f(html_path);
+		if (f.is_open()) {
+			std::string html((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
+			res.set_content(html, "text/html; charset=utf-8");
+		} else {
+			res.status = 404;
+			res.set_content("Logs page not found", "text/plain");
+		}
+	});
+
+	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");
+		});
+
+	// ==================== fix24-v37: 系统监控历史数据 API ====================
+	svr.Get("/api/monitor/history", [](const httplib::Request& req, httplib::Response& res) {
+		std::string range = "1h";
+		if (req.has_param("range")) {
+			range = req.get_param_value("range");
+		}
+		std::string json = MetricsManager::instance().get_history_json(range);
+		res.set_content(json, "application/json; charset=utf-8");
+	});
+
+	svr.Get("/api/monitor/stats", [](const httplib::Request& req, httplib::Response& res) {
+		std::string json = MetricsManager::instance().get_stats_json();
+		res.set_content(json, "application/json; charset=utf-8");
+	});
+
+	// ==================== fix24 v26: 连接状态 API ====================
+	svr.Get("/api/connections", [](const httplib::Request& req, httplib::Response& res) {
+		std::ostringstream j;
+		j << "{";
+
+		// --- RTSP 摄像头 ---
+		j << "\"rtsp\":[";
+		auto check_rtsp = [](const std::string& name, const std::string& url, RTSPCapture* cap) {
+			std::ostringstream o;
+			bool configured = !url.empty();
+			bool running = (cap && cap->isRunning());
+			o << "{\"name\":\"" << name << "\""
+			  << ",\"url\":\"" << (configured ? url.substr(0, 40) + "..." : "") << "\""
+			  << ",\"configured\":" << (configured ? "true" : "false")
+			  << ",\"running\":" << (running ? "true" : "false")
+			  << "}";
+			return o.str();
+		};
+		j << check_rtsp("前置进站", rtsp_url_front_in, plate_rec_app.capture_front_in);
+		j << "," << check_rtsp("前置出站", rtsp_url_front_out, plate_rec_app.capture_front_out);
+		j << "," << check_rtsp("侧面进站", rtsp_url_side_in, plate_rec_app.capture_side_in);
+		j << "," << check_rtsp("侧面出站", rtsp_url_side_out, plate_rec_app.capture_side_out);
+		j << "],";
+
+		// --- MQTT ---
+		int mqtt_st = mqtt_get_status();
+		const char* mqtt_state_str[] = {"未启用", "已连接", "连接中", "已断开"};
+		j << "\"mqtt\":{\"enabled\":" << (g_mqtt_enabled ? "true" : "false")
+		  << ",\"status\":" << mqtt_st
+		  << ",\"state\":\"" << mqtt_state_str[mqtt_st] << "\""
+		  << ",\"host\":\"" << g_mqtt_host << ":" << g_mqtt_port << "\""
+		  << ",\"topic\":\"" << g_mqtt_topic << "\"},";
+
+		// --- 数据库 ---
+		j << "\"database\":{\"connected\":" << (g_db ? "true" : "false")
+		  << ",\"path\":\"" << g_db_path << "\"},";
+
+		// --- frpc ---
+		auto frpc_st = frpc_mgr::get_status();
+		j << "\"frpc\":{\"running\":" << (frpc_st.running ? "true" : "false")
+		  << ",\"tunnels\":" << frpc_st.tunnels.size()
+		  << ",\"version\":\"" << frpc_st.version << "\""
+		  << ",\"server\":\"" << g_frpc_admin_addr << ":" << g_frpc_admin_port << "\"},";
+
+		// --- Web 服务器 ---
+		j << "\"web\":{\"port\":" << g_web_server_port << ",\"https\":" << (g_ssl_enabled ? "true" : "false") << "},";
+
+		// --- 认证 ---
+		j << "\"auth\":{\"enabled\":" << (g_auth_enabled ? "true" : "false") << "}";
+
+		j << "}";
+		res.set_content(j.str(), "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 {
+				// fix24 v35: 使用peekFrame代替getFrame,不与识别线程竞争帧
+				cv::Mat frame = peek_frame_by_stream(stream);
+				if (frame.empty()) {
+					std::this_thread::sleep_for(std::chrono::milliseconds(100));
+					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编码 — fix24 v35: 质量从70降到55,加快传输速度
+				std::vector<uchar> buf;
+				cv::imencode(".jpg", frame, buf, {cv::IMWRITE_JPEG_QUALITY, 55});
+
+				// 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("/help", [](const httplib::Request& req, httplib::Response& res) {
+		std::string html_path = pathComm + "assets/web/help.html";
+		std::ifstream f(html_path);
+		if (f.is_open()) {
+			res.set_content(std::string((std::istreambuf_iterator<char>(f)),
+				std::istreambuf_iterator<char>()), "text/html; charset=utf-8");
+		} else {
+			res.set_content("<html><body><h2>帮助页面不存在</h2><p>请将 help.html 复制到 assets/web/ 目录</p></body></html>", "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)}}
+@media(max-width:768px){
+.navbar{flex-wrap:wrap;gap:8px;padding:10px 14px}
+.navbar .brand{font-size:14px;width:100%}
+.navbar a{font-size:12px;padding:4px 8px}
+.container{padding:0 10px;margin:10px auto}
+.status-bar{flex-direction:column;gap:8px}
+.status-bar .path{font-size:12px}
+.field{flex-direction:column;align-items:flex-start;gap:4px;padding:8px 12px}
+.field-label{min-width:auto;font-size:12px}
+.field-input input,.field-input textarea{font-size:12px}
+.section-title{font-size:13px;padding:8px 12px}
+.btn{padding:6px 14px;font-size:13px}
+}
+@media(max-width:480px){
+.navbar .brand{font-size:13px}
+.navbar a{font-size:11px;padding:3px 6px}
+}
+</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>
+  <a href="/help" id="nav-help" style="display:none;">帮助</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','X-CSRF-Token':document.cookie.match(/csrf_token=([^;]+)/)?.[1]||''},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);
+}
+
+// 角色检查:根据权限显示导航链接
+fetch('/api/auth/status',{credentials:'same-origin'}).then(r=>r.json()).then(d=>{
+if(d.authenticated){
+if(d.role>=1){var c=document.getElementById('nav-config');if(c)c.style.display='';}
+if(d.role>=2){var h=document.getElementById('nav-help');if(h)h.style.display='';}
+}}).catch(()=>{});
+
+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}
+@media(max-width:768px){
+.navbar{flex-wrap:wrap;gap:8px;padding:10px 14px}
+.navbar .brand{font-size:14px;width:100%}
+.navbar a{font-size:12px;padding:4px 8px}
+.container{padding:0 10px;margin:10px auto}
+.card-title{flex-direction:column;align-items:flex-start;gap:6px;font-size:13px;padding:8px 12px}
+table{font-size:12px;display:block;overflow-x:auto;white-space:nowrap}
+th,td{padding:6px 8px}
+.btn{padding:4px 10px;font-size:11px}
+}
+@media(max-width:480px){
+.navbar .brand{font-size:13px}
+.navbar a{font-size:11px;padding:3px 6px}
+th,td{padding:4px 6px;font-size:11px}
+}
+</style></head><body>
+<nav class="navbar">
+  <span class="brand">🚛 车牌识别系统</span>
+  <a href="/">监控首页</a>
+  <a href="/video">视频预览</a>
+  <a href="/config" id="nav-config" style="display:none;">系统配置</a>
+  <a href="/locks" class="active">锁定管理</a>
+  <a href="/help" id="nav-help" style="display:none;">帮助</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','X-CSRF-Token':document.cookie.match(/csrf_token=([^;]+)/)?.[1]||''},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);
+}
+// 角色检查:根据权限显示导航链接
+fetch('/api/auth/status',{credentials:'same-origin'}).then(r=>r.json()).then(d=>{
+if(d.authenticated){
+if(d.role>=1){var c=document.getElementById('nav-config');if(c)c.style.display='';}
+if(d.role>=2){var h=document.getElementById('nav-help');if(h)h.style.display='';}
+}}).catch(()=>{});
+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;
+
+    // ✅ fix24-v21: capture_ts提升到函数作用域,供后续飞书消息使用抓拍时间
+    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;
+    }
+
+    // ✅ 修复:db_id <= 0时都尝试插入(包括db_id == -1的情况)
+    if (plate_info->db_id <= 0) {
+        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 ? "渣土车" : "";
+				// fix24-v21: 使用抓拍时间而非当前时间,确保飞书消息时间与水印/DB一致
+				std::string feishu_time_str;
+				if (capture_ts > 0) {
+					struct tm tm_buf;
+					struct tm* tm_ptr = localtime_r(&capture_ts, &tm_buf);
+					char buf[64];
+					strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_ptr);
+					feishu_time_str = buf;
+				} else {
+					feishu_time_str = get_format_time();
+				}
+				bool feishu_ok = send_feishu_msg(token,
+					project_name, plate, feishu_time_str,
+					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;
+
+    // ✅ fix24-v21: capture_ts提升到函数作用域,供后续飞书消息使用抓拍时间
+    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;
+    }
+
+    // ✅ 修复:db_id <= 0时都尝试插入(包括db_id == -1的情况)
+    if (plate_info->db_id <= 0) {
+        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 ? "渣土车" : "";
+				// fix24-v21: 使用抓拍时间而非当前时间,确保飞书消息时间与水印/DB一致
+				std::string feishu_time_str;
+				if (capture_ts > 0) {
+					struct tm tm_buf;
+					struct tm* tm_ptr = localtime_r(&capture_ts, &tm_buf);
+					char buf[64];
+					strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_ptr);
+					feishu_time_str = buf;
+				} else {
+					feishu_time_str = get_format_time();
+				}
+				bool feishu_ok = send_feishu_msg(token,
+					project_name, plate, feishu_time_str,
+					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;
+}

+ 20 - 0
src/web_server.h

@@ -0,0 +1,20 @@
+#ifndef WEB_SERVER_H
+#define WEB_SERVER_H
+#include "common.h"
+void start_web_server();
+void stop_web_server();
+std::string generate_html_page();
+std::string generate_video_page();  // ✅ fix18新增
+std::string generate_monitor_report();
+std::string generate_message_list_json(int page, const std::string& search_keyword);
+int manual_retry_record(int id);
+std::string escape_html(const std::string& str);
+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 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);
+#endif

+ 1732 - 0
src/web_server1.cpp

@@ -0,0 +1,1732 @@
+/**
+ * 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;
+}

+ 584 - 0
src/weight_scale.cpp

@@ -0,0 +1,584 @@
+/**
+ * weight_scale.cpp — 称重系统完整实现
+ * fix20: 从 plate_log2mqtt.cpp 完整迁移 F1-F12 功能
+ * 包括:耀华仪表TCP通信、稳定读数检测、称重数据上报平台
+ */
+#include "weight_scale.h"
+#include "mqtt_client.h"
+#include "network_client.h"
+#include "database.h"
+#include "utils.h"
+#include "md5ex1.h"
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <sys/stat.h>
+
+// ==================== 耀华仪表帧解析常量 ====================
+static const int FRAME_LENGTH = 12;   // 帧长度12字节
+static const int BUFFER_SIZE = 1024;  // TCP接收缓冲区
+static const int RECONNECT_DELAY_MS = 5000;
+static const int READ_INTERVAL_MS = 200;
+
+// ==================== TCP连接状态 ====================
+static std::vector<uint8_t> g_recv_buffer;
+static std::vector<double> g_stability_samples;
+static std::vector<double> g_candidate_weights;
+static std::mutex g_data_mutex;
+static std::atomic<bool> g_weight_running{false};
+static std::thread* g_tcp_thread = nullptr;
+
+// ==================== 称重+MQTT异步任务队列 ====================
+struct WeightMqttTask {
+    std::string plate;
+    int station_type;  // 1=进站, 2=出站
+    std::string tb_num;
+};
+static std::deque<WeightMqttTask> g_weight_mqtt_queue;
+static std::mutex g_weight_mqtt_mtx;
+static std::condition_variable g_weight_mqtt_cv;
+static std::atomic<bool> g_weight_mqtt_running{false};
+static std::unique_ptr<std::thread> g_weight_mqtt_thread;
+
+// ✅ fix24: MQTT发送去重 — 防止同一车牌短时间内重复发送MQTT消息
+// 无论首次上传还是重试队列,都在此处统一去重
+static std::unordered_map<std::string, time_t> g_mqtt_dedup_map;
+static std::mutex g_mqtt_dedup_mtx;
+static const int MQTT_DEDUP_INTERVAL_SEC = 30; // 30秒内同一车牌只发送一次MQTT
+
+// ==================== 耀华仪表帧解析 ====================
+static bool parse_frame(const uint8_t* frame, double& weight_kg, char& status) {
+    // 帧格式: 0x02 + sign(1) + digits(8) + status(1) + 0x03
+    if (frame[0] != 0x02 || frame[11] != 0x03) return false;
+    char sign = frame[1];
+    if (sign != '+' && sign != '-') return false;
+    std::string weight_str;
+    for (int i = 2; i <= 9; ++i) {
+        if (!isdigit(frame[i])) return false;
+        weight_str += frame[i];
+    }
+    status = frame[10];
+    try {
+        long long num = std::stoll(weight_str);
+        weight_kg = num / 100.0;  // 耀华仪表单位:分 → 转换为公斤
+        if (sign == '-') weight_kg = -weight_kg;
+    } catch (...) { return false; }
+    return true;
+}
+
+// ==================== 处理接收缓冲区 ====================
+static void process_buffer() {
+    while (g_recv_buffer.size() >= FRAME_LENGTH) {
+        auto it = std::find(g_recv_buffer.begin(), g_recv_buffer.end(), static_cast<uint8_t>(0x02));
+        if (it == g_recv_buffer.end()) { g_recv_buffer.clear(); return; }
+        size_t frame_start = std::distance(g_recv_buffer.begin(), it);
+        if (frame_start + FRAME_LENGTH > g_recv_buffer.size()) return;
+        if (g_recv_buffer[frame_start + 11] != static_cast<uint8_t>(0x03)) {
+            g_recv_buffer.erase(g_recv_buffer.begin(), g_recv_buffer.begin() + frame_start + 1);
+            continue;
+        }
+        uint8_t frame[FRAME_LENGTH];
+        std::copy(g_recv_buffer.begin() + frame_start,
+                  g_recv_buffer.begin() + frame_start + FRAME_LENGTH, frame);
+        double weight_kg;
+        char status;
+        if (parse_frame(frame, weight_kg, status)) {
+            // ✅ fix21b: 与plate_log2mqtt原始逻辑一致:排除status='G'/'H'的帧,接受其他状态
+            // 耀华仪表的'G'/'H'状态码在此仪表上不代表有效称重数据
+            if (status != 'G' && status != 'H' && weight_kg >= g_weight_min_kg && weight_kg <= g_weight_max_kg) {
+                std::lock_guard<std::mutex> lock(g_data_mutex);
+                g_stability_samples.push_back(weight_kg);
+                if ((int)g_stability_samples.size() > g_weight_stable_samples) {
+                    g_stability_samples.erase(g_stability_samples.begin());
+                }
+                if ((int)g_candidate_weights.size() < g_weight_candidate_count) {
+                    g_candidate_weights.push_back(weight_kg);
+                }
+            }
+        }
+        g_recv_buffer.erase(g_recv_buffer.begin(), g_recv_buffer.begin() + frame_start + FRAME_LENGTH);
+    }
+}
+
+// ==================== TCP客户端循环线程 ====================
+static void tcp_client_loop() {
+    std::cout << "[称重] TCP客户端线程启动,连接 " << g_weight_server_ip
+              << ":" << g_weight_server_port << std::endl;
+    while (g_weight_running.load()) {
+        int sock = socket(AF_INET, SOCK_STREAM, 0);
+        if (sock < 0) {
+            std::this_thread::sleep_for(std::chrono::milliseconds(RECONNECT_DELAY_MS));
+            continue;
+        }
+        struct timeval timeout;
+        timeout.tv_sec = g_weight_tcp_connect_timeout;
+        timeout.tv_usec = 0;
+        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+        setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+
+        sockaddr_in addr{};
+        addr.sin_family = AF_INET;
+        addr.sin_port = htons(g_weight_server_port);
+        inet_pton(AF_INET, g_weight_server_ip.c_str(), &addr.sin_addr);
+
+        if (connect(sock, (sockaddr*)&addr, sizeof(addr)) < 0) {
+            close(sock);
+            std::this_thread::sleep_for(std::chrono::milliseconds(RECONNECT_DELAY_MS));
+            continue;
+        }
+        std::cout << "[称重] TCP已连接 " << g_weight_server_ip << ":" << g_weight_server_port << std::endl;
+        g_recv_buffer.clear();
+        g_candidate_weights.clear();
+        g_stability_samples.clear();
+
+        while (g_weight_running.load()) {
+            uint8_t buf[BUFFER_SIZE];
+            int n = recv(sock, buf, sizeof(buf), 0);
+            if (n <= 0) break;
+            g_recv_buffer.insert(g_recv_buffer.end(), buf, buf + n);
+            process_buffer();
+            std::this_thread::sleep_for(std::chrono::milliseconds(READ_INTERVAL_MS));
+        }
+        close(sock);
+        std::cout << "[称重] TCP断开," << RECONNECT_DELAY_MS/1000 << "秒后重连..." << std::endl;
+        std::this_thread::sleep_for(std::chrono::milliseconds(RECONNECT_DELAY_MS));
+    }
+    std::cout << "[称重] TCP客户端线程退出" << std::endl;
+}
+
+// ==================== 稳定性检测 ====================
+static bool is_weight_stable() {
+    std::lock_guard<std::mutex> lock(g_data_mutex);
+    if ((int)g_stability_samples.size() < g_weight_stable_samples) return false;
+    double min_w = *std::min_element(g_stability_samples.begin(), g_stability_samples.end());
+    double max_w = *std::max_element(g_stability_samples.begin(), g_stability_samples.end());
+    return (max_w - min_w) <= g_weight_stable_threshold;
+}
+
+static double get_max_candidate_weight() {
+    std::lock_guard<std::mutex> lock(g_data_mutex);
+    if (g_candidate_weights.empty()) return 0.0;
+    return *std::max_element(g_candidate_weights.begin(), g_candidate_weights.end());
+}
+
+static void clear_candidate_data() {
+    std::lock_guard<std::mutex> lock(g_data_mutex);
+    g_candidate_weights.clear();
+    g_stability_samples.clear();
+}
+
+// ==================== 读取称重数据(阻塞等待稳定) ====================
+static bool read_weight_with_timeout(double threshold_kg, double& out_weight_ton) {
+    time_t start_time = time(NULL);
+    clear_candidate_data();
+    int wait_seconds = 0;
+    int last_candidate_count = 0;
+
+    while (difftime(time(NULL), start_time) < g_weight_detection_time && g_running) {
+        int cur_candidates = (int)g_candidate_weights.size();
+        int cur_samples = (int)g_stability_samples.size();
+
+        // fix21: 每5秒输出一次诊断信息
+        if (wait_seconds > 0 && wait_seconds % 5 == 0) {
+            double latest_w = 0;
+            {
+                std::lock_guard<std::mutex> lock(g_data_mutex);
+                if (!g_stability_samples.empty())
+                    latest_w = g_stability_samples.back();
+            }
+            std::cout << "[称重] 等待中... " << wait_seconds << "/" << g_weight_detection_time << "秒"
+                      << " 候选:" << cur_candidates << "/" << g_weight_candidate_count
+                      << " 样本:" << cur_samples << "/" << g_weight_stable_samples
+                      << " 最新:" << latest_w << "公斤" << std::endl;
+        }
+
+        if (cur_candidates >= g_weight_candidate_count && is_weight_stable()) {
+            double weight_kg = get_max_candidate_weight();
+            out_weight_ton = weight_kg / 1000.0;
+            if (weight_kg > threshold_kg && weight_kg >= g_weight_min_kg) {
+                std::cout << "[称重] 稳定读数: " << weight_kg << "公斤 (" << out_weight_ton << "吨)" << std::endl;
+                return true;
+            } else {
+                std::cout << "[称重] 重量无效不上传: " << weight_kg << "公斤"
+                          << " (阈值:" << threshold_kg << "公斤, 最小:" << g_weight_min_kg << "公斤)" << std::endl;
+                return false;
+            }
+        }
+        std::this_thread::sleep_for(std::chrono::seconds(1));
+        wait_seconds++;
+    }
+
+    // fix21: 超时时输出详细诊断信息
+    {
+        std::lock_guard<std::mutex> lock(g_data_mutex);
+        std::cerr << "[称重] 读取超时(" << g_weight_detection_time << "秒),未获取到有效重量"
+                  << " | 候选数:" << g_candidate_weights.size() << "/" << g_weight_candidate_count
+                  << " 稳定样本:" << g_stability_samples.size() << "/" << g_weight_stable_samples;
+        if (!g_stability_samples.empty()) {
+            double min_w = *std::min_element(g_stability_samples.begin(), g_stability_samples.end());
+            double max_w = *std::max_element(g_stability_samples.begin(), g_stability_samples.end());
+            std::cerr << " 范围:[" << min_w << "-" << max_w << "]公斤"
+                      << " 波动:" << (max_w - min_w) << "公斤(需<=" << g_weight_stable_threshold << ")";
+        } else {
+            std::cerr << " | TCP仪表无数据返回(检查仪表连接/帧格式)";
+        }
+        std::cerr << std::endl;
+    }
+    return false;
+}
+
+// ==================== 称重数据上传到平台 ====================
+static size_t weight_write_callback(void* contents, size_t size, size_t nmemb, std::string* str) {
+    str->append((char*)contents, size * nmemb);
+    return size * nmemb;
+}
+
+static int upload_weight_data(const std::string& tb_no, float weight_ton) {
+    if (app_api.empty()) return -1;
+    std::string timestamp = get_new_timestamp();
+    std::string sign = calculate_sign(app_key, app_secret, timestamp);
+    std::string response;
+    CURL* curl = curl_easy_init();
+    if (!curl) return -1;
+
+    std::string req_url;
+    if (TestFlag.load() == 1) {
+        req_url = app_api + "createWeighTest?app_key=" + app_key
+                + "&sign=" + sign + "&timestamp=" + timestamp + "&v=1";
+    } else {
+        if (g_wType == 1) {
+            req_url = app_api + "createWsiteVehicleWeigh?app_key=" + app_key
+                    + "&sign=" + sign + "&timestamp=" + timestamp + "&v=1";
+        } else if (g_wType == 3) {
+            req_url = app_api + "createDsiteVehicleWeigh?app_key=" + app_key
+                    + "&sign=" + sign + "&timestamp=" + timestamp + "&v=1";
+        } else {
+            req_url = app_api + "createWeighTest?app_key=" + app_key
+                    + "&sign=" + sign + "&timestamp=" + timestamp + "&v=1";
+        }
+    }
+
+    cJSON* json = cJSON_CreateObject();
+    cJSON_AddStringToObject(json, "tbNo", tb_no.c_str());
+    if (g_wType == 3) {
+        cJSON_AddStringToObject(json, "litter", "工程渣土");
+    }
+    cJSON_AddNumberToObject(json, "weight", weight_ton);
+    char* postdata = cJSON_PrintUnformatted(json);
+
+    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_POSTFIELDS, postdata);
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, weight_write_callback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
+    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
+    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
+
+    CURLcode res = curl_easy_perform(curl);
+    int ret = -1;
+    if (res == CURLE_OK && !response.empty()) {
+        cJSON* root = cJSON_Parse(response.c_str());
+        if (root) {
+            cJSON* status = cJSON_GetObjectItemCaseSensitive(root, "status");
+            if (status && cJSON_IsNumber(status) && status->valuedouble == 200) {
+                ret = 0;
+                std::cout << "[称重] 平台上传成功 联单:" << tb_no << " 重量:" << weight_ton << "吨" << std::endl;
+            } else {
+                cJSON* msg = cJSON_GetObjectItemCaseSensitive(root, "msg");
+                std::cerr << "[称重] 平台返回错误: " << (msg ? msg->valuestring : "未知") << std::endl;
+            }
+            cJSON_Delete(root);
+        }
+    } else {
+        std::cerr << "[称重] CURL请求失败: " << curl_easy_strerror(res) << std::endl;
+    }
+
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    cJSON_Delete(json);
+    free(postdata);
+    return ret;
+}
+
+static int upload_weight_with_retry(const std::string& tb_no, float weight_ton) {
+    for (int retry = 0; retry < g_weight_max_retries; ++retry) {
+        if (retry > 0) std::this_thread::sleep_for(std::chrono::milliseconds(g_weight_retry_delay_ms));
+        if (upload_weight_data(tb_no, weight_ton) == 0) return 0;
+    }
+    return -1;
+}
+
+// ==================== 公开API ====================
+
+bool weight_scale_init() {
+    if (!g_weight_enabled) {
+        std::cout << "[称重] 称重系统未启用(flagWeight=0)" << std::endl;
+        return false;
+    }
+    // 参数安全校验
+    if (g_weight_threshold_in < 100.0) {
+        std::cerr << "[称重] 进站阈值过低(" << g_weight_threshold_in << "公斤),修正为500" << std::endl;
+        g_weight_threshold_in = 500.0;
+    }
+    if (g_weight_threshold_out < 100.0) {
+        std::cerr << "[称重] 出站阈值过低(" << g_weight_threshold_out << "公斤),修正为500" << std::endl;
+        g_weight_threshold_out = 500.0;
+    }
+    if (g_weight_stable_threshold < 10.0) {
+        std::cerr << "[称重] 稳定阈值过低(" << g_weight_stable_threshold << "公斤),修正为50" << std::endl;
+        g_weight_stable_threshold = 50.0;
+    }
+
+    std::cout << "[称重] 初始化 - 仪表:" << g_weight_server_ip << ":" << g_weight_server_port
+              << " 进站阈值:" << g_weight_threshold_in << "公斤"
+              << " 出站阈值:" << g_weight_threshold_out << "公斤"
+              << " 检测时间:" << g_weight_detection_time << "秒"
+              << " 稳定样本:" << g_weight_stable_samples
+              << " 候选数量:" << g_weight_candidate_count << std::endl;
+
+    g_weight_running = true;
+    g_tcp_thread = new std::thread(tcp_client_loop);
+    std::cout << "[称重] TCP客户端线程已启动" << std::endl;
+    return true;
+}
+
+bool weight_scale_read(double& weight_kg, int timeout_sec) {
+    (void)timeout_sec;  // 使用g_weight_detection_time
+    double weight_ton = 0.0;
+    double threshold = 500.0;  // 默认阈值,由调用方决定
+    if (read_weight_with_timeout(threshold, weight_ton)) {
+        weight_kg = weight_ton * 1000.0;
+        return true;
+    }
+    return false;
+}
+
+void weight_scale_close() {
+    g_weight_running = false;
+    if (g_tcp_thread) {
+        if (g_tcp_thread->joinable()) g_tcp_thread->join();
+        delete g_tcp_thread;
+        g_tcp_thread = nullptr;
+    }
+    std::cout << "[称重] 已关闭" << std::endl;
+}
+
+// ==================== 称重+MQTT异步任务 ====================
+
+void enqueue_weight_mqtt_task(const std::string& plate, int station_type,
+                               const std::string& tb_num) {
+    if (!g_weight_enabled && !g_mqtt_enabled) {
+        std::cerr << "[称重+MQTT] 任务被丢弃(称重和MQTT均未启用): " << plate << std::endl;
+        return;
+    }
+
+    // ✅ fix24: MQTT发送去重 — 检查30秒内是否已为同一车牌发送过MQTT
+    {
+        std::lock_guard<std::mutex> lock(g_mqtt_dedup_mtx);
+        auto it = g_mqtt_dedup_map.find(plate);
+        if (it != g_mqtt_dedup_map.end()) {
+            time_t elapsed = time(NULL) - it->second;
+            if (elapsed < MQTT_DEDUP_INTERVAL_SEC) {
+                std::cout << "[MQTT去重] " << plate
+                          << " " << (station_type == 1 ? "进站" : "出站")
+                          << " " << elapsed << "秒内已发送过,跳过重复MQTT" << std::endl;
+                return;
+            }
+        }
+        g_mqtt_dedup_map[plate] = time(NULL);
+    }
+
+    std::cout << "[称重+MQTT] 提交任务: " << plate
+              << (station_type == 1 ? " 进站" : " 出站")
+              << " 联单:" << tb_num
+              << " [称重:" << (g_weight_enabled ? "启用" : "禁用")
+              << " MQTT:" << (g_mqtt_enabled ? "启用" : "禁用") << "]" << std::endl;
+    {
+        std::lock_guard<std::mutex> lock(g_weight_mqtt_mtx);
+        g_weight_mqtt_queue.push_back({plate, station_type, tb_num});
+    }
+    g_weight_mqtt_cv.notify_one();
+}
+
+static void weight_mqtt_worker_func() {
+    std::cout << "[称重+MQTT] worker线程启动" << std::endl;
+    while (g_weight_mqtt_running.load() && g_running) {
+        WeightMqttTask task;
+        {
+            std::unique_lock<std::mutex> lock(g_weight_mqtt_mtx);
+            g_weight_mqtt_cv.wait_for(lock, std::chrono::seconds(1),
+                [] { return !g_weight_mqtt_queue.empty() || !g_weight_mqtt_running.load(); });
+            if (!g_weight_mqtt_running.load() || !g_running) break;
+            if (g_weight_mqtt_queue.empty()) continue;
+            task = g_weight_mqtt_queue.front();
+            g_weight_mqtt_queue.pop_front();
+        }
+
+        std::string in_out_str = (task.station_type == 1) ? "进站" : "出站";
+        std::cout << "[称重+MQTT] 处理任务: " << task.plate << " " << in_out_str
+                  << " 联单:" << task.tb_num << std::endl;
+
+        // ===== 称重读取 + 上传 =====
+        double weight_ton = 0.0;
+        double weight_kg = 0.0;
+        bool weight_read_ok = false;
+        bool weight_upload_ok = false;
+
+        if (g_weight_enabled) {
+            double threshold_kg = (task.station_type == 1) ?
+                g_weight_threshold_in : g_weight_threshold_out;
+            std::cout << "[称重] 开始读取, 阈值:" << threshold_kg << "公斤" << std::endl;
+
+            clear_candidate_data();
+            weight_read_ok = read_weight_with_timeout(threshold_kg, weight_ton);
+            if (weight_read_ok) {
+                weight_kg = weight_ton * 1000.0;
+                std::cout << "[称重] 有效重量: " << weight_kg << "公斤 (" << weight_ton << "吨)" << std::endl;
+                int result = upload_weight_with_retry(task.tb_num, static_cast<float>(weight_ton));
+                if (result == 0) {
+                    weight_upload_ok = true;
+                    std::cout << "[称重] 上传成功 联单:" << task.tb_num << " 重量:" << weight_ton << "吨" << std::endl;
+                } else {
+                    std::cerr << "[称重] 上传失败(已重试" << g_weight_max_retries << "次) 联单:" << task.tb_num << std::endl;
+                }
+                // ✅ fix24-v9: 保存称重记录到数据库
+                int status = weight_upload_ok ? 1 : 2;
+                std::string resp = weight_upload_ok ? "上传成功" : "上传失败";
+                db_insert_weight_record(task.plate, task.tb_num, task.station_type, weight_kg, status, resp);
+            } else {
+                // 未读取到有效重量,也记录一条
+                db_insert_weight_record(task.plate, task.tb_num, task.station_type, 0.0, 2, "未获取有效重量");
+            }
+            clear_candidate_data();
+        }
+
+        // ===== MQTT发布 =====
+        if (g_mqtt_enabled) {
+            // 构建MQTT消息(与plate_log2mqtt.cpp格式完全一致)
+            time_t now_t = time(NULL);
+            struct tm* tm_info = localtime(&now_t);
+            char time_buf[64];
+            strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);
+
+            std::string msg =
+                "工程名:    " + project_name + "\n"
+                "车牌号:    " + task.plate + "\n"
+                "时间:       " + std::string(time_buf) + "\n"
+                "工地编号:  " + point_number + "\n"
+                "门号:        " + throughway + "\n"
+                "进出站:     " + in_out_str + "\n"
+                "车牌颜色:  " + g_plate_color + "\n"
+                "车型:         " + g_vehicle_type + "\n"
+                "三联单编号:" + task.tb_num + "\n"
+                "一组照片(1张低位照片,1张高位照片)上传成功";
+
+            if (g_weight_enabled) {
+                if (weight_read_ok) {
+                    msg += "\n称重数据: " + std::to_string(weight_ton) + "吨 ("
+                         + std::to_string(weight_kg) + "公斤) ["
+                         + (weight_upload_ok ? "上传成功" : "上传失败") + "]";
+                } else {
+                    msg += "\n称重数据: 未读取到有效重量";
+                }
+            }
+
+            // MQTT发布(带重试)
+            bool mqtt_ok = false;
+            for (int pub_att = 0; pub_att < 3 && !mqtt_ok; pub_att++) {
+                if (mqtt_client_publish("", msg)) {
+                    mqtt_ok = true;
+                    std::cout << "[MQTT] 发布成功: " << task.plate << " " << in_out_str << std::endl;
+                } else if (pub_att < 2) {
+                    std::cerr << "[MQTT] 发布失败,重试(" << (pub_att+1) << "/3)" << std::endl;
+                    std::this_thread::sleep_for(std::chrono::seconds(1));
+                }
+            }
+            if (!mqtt_ok) {
+                std::cerr << "[MQTT] 发布最终失败: " << task.plate << std::endl;
+            }
+        }
+
+        // fix21: 任务完成汇总日志
+        std::cout << "[称重+MQTT] 任务完成: " << task.plate << " " << in_out_str
+                  << " 称重:" << (g_weight_enabled ? (weight_read_ok ? (weight_upload_ok ? "✅上传成功" : "⚠️读取成功但上传失败") : "❌未获取有效重量") : "跳过")
+                  << " MQTT:" << (g_mqtt_enabled ? "已处理" : "跳过") << std::endl;
+    }
+    std::cout << "[称重+MQTT] worker线程退出" << std::endl;
+}
+
+void start_weight_mqtt_worker() {
+    if (!g_weight_enabled && !g_mqtt_enabled) return;
+    g_weight_mqtt_running = true;
+    g_weight_mqtt_thread = std::make_unique<std::thread>(weight_mqtt_worker_func);
+    std::cout << "[称重+MQTT] 异步worker已启动" << std::endl;
+}
+
+void stop_weight_mqtt_worker() {
+    g_weight_mqtt_running = false;
+    g_weight_mqtt_cv.notify_all();
+    if (g_weight_mqtt_thread && g_weight_mqtt_thread->joinable()) {
+        g_weight_mqtt_thread->join();
+    }
+    g_weight_mqtt_thread.reset();
+}
+
+// ==================== fix24-v9: 手动重试称重上传 ====================
+bool weight_manual_retry(int record_id) {
+    if (!g_weight_enabled) {
+        std::cerr << "[称重] 称重系统未启用,无法重试" << std::endl;
+        return false;
+    }
+    if (record_id <= 0) return false;
+
+    // 从数据库获取记录
+    std::string tb_num;
+    double weight_kg = 0.0;
+    int retry_count = 0;
+    {
+        // 直接查询数据库获取记录详情
+        const char* sql = "SELECT tb_num, weight_kg, retry_count FROM weight_records WHERE id = ?;";
+        // 需要访问 g_db,通过 database.h 的接口
+        // 简化处理:通过 db_get_weight_records 获取
+        auto records = db_get_weight_records(1, 1000, "");
+        bool found = false;
+        for (const auto& rec : records) {
+            if (rec.id == record_id) {
+                tb_num = rec.tb_num;
+                weight_kg = std::stod(rec.hi_photo_path); // weight stored here
+                retry_count = rec.hi_upload_status; // retry_count stored here
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            std::cerr << "[称重] 记录ID " << record_id << " 不存在" << std::endl;
+            return false;
+        }
+    }
+
+    if (weight_kg <= 0) {
+        std::cerr << "[称重] 记录ID " << record_id << " 重量无效(" << weight_kg << "kg)" << std::endl;
+        db_update_weight_upload_status(record_id, 2, "重量数据无效");
+        return false;
+    }
+
+    double weight_ton = weight_kg / 1000.0;
+    std::cout << "[称重] 手动重试 记录ID:" << record_id << " 联单:" << tb_num
+              << " 重量:" << weight_ton << "吨" << std::endl;
+
+    db_increment_weight_retry_count(record_id);
+
+    int result = upload_weight_data(tb_num, static_cast<float>(weight_ton));
+    if (result == 0) {
+        db_update_weight_upload_status(record_id, 1, "手动重试成功");
+        std::cout << "[称重] 手动重试成功 记录ID:" << record_id << std::endl;
+        return true;
+    } else {
+        db_update_weight_upload_status(record_id, 2, "手动重试失败");
+        std::cerr << "[称重] 手动重试失败 记录ID:" << record_id << std::endl;
+        return false;
+    }
+}

+ 56 - 0
src/weight_scale.h

@@ -0,0 +1,56 @@
+/**
+ * weight_scale.h — 称重系统模块(fix20完整实现)
+ * 从 plate_log2mqtt.cpp 完整迁移 F1-F12 功能
+ * 包括:耀华仪表TCP通信、稳定读数检测、称重数据上报平台
+ */
+#ifndef WEIGHT_SCALE_H
+#define WEIGHT_SCALE_H
+#include "common.h"
+#include <string>
+
+/**
+ * 初始化称重系统(启动TCP客户端线程连接称重仪表)
+ * @return true=初始化成功, false=未启用或失败
+ */
+bool weight_scale_init();
+
+/**
+ * 读取当前称重数据(阻塞等待稳定读数)
+ * @param weight_kg 输出:重量(kg)
+ * @param timeout_sec 超时秒数(实际使用g_weight_detection_time)
+ * @return true=读取成功, false=超时或失败
+ */
+bool weight_scale_read(double& weight_kg, int timeout_sec);
+
+/**
+ * 关闭称重系统(停止TCP线程)
+ */
+void weight_scale_close();
+
+/**
+ * 提交称重+MQTT任务到异步队列(不阻塞主循环)
+ * @param plate 车牌号
+ * @param station_type 1=进站, 2=出站
+ * @param tb_num 联单编号
+ */
+void enqueue_weight_mqtt_task(const std::string& plate, int station_type,
+                               const std::string& tb_num);
+
+/**
+ * 启动称重+MQTT异步worker线程
+ */
+void start_weight_mqtt_worker();
+
+/**
+ * 停止称重+MQTT异步worker线程
+ */
+void stop_weight_mqtt_worker();
+
+/**
+ * 手动重试称重上传(fix24-v9)
+ * @param record_id 称重记录ID
+ * @return true=重试成功, false=重试失败
+ */
+bool weight_manual_retry(int record_id);
+
+#endif

+ 3191 - 0
third_party/cJSON.c

@@ -0,0 +1,3191 @@
+/*
+  Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+/* cJSON */
+/* JSON parser in C. */
+
+/* disable warnings about old C89 functions in MSVC */
+#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER)
+#define _CRT_SECURE_NO_DEPRECATE
+#endif
+
+#ifdef __GNUC__
+#pragma GCC visibility push(default)
+#endif
+#if defined(_MSC_VER)
+#pragma warning (push)
+/* disable warning about single line comments in system headers */
+#pragma warning (disable : 4001)
+#endif
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <ctype.h>
+#include <float.h>
+
+#ifdef ENABLE_LOCALES
+#include <locale.h>
+#endif
+
+#if defined(_MSC_VER)
+#pragma warning (pop)
+#endif
+#ifdef __GNUC__
+#pragma GCC visibility pop
+#endif
+
+#include "cJSON.h"
+
+/* define our own boolean type */
+#ifdef true
+#undef true
+#endif
+#define true ((cJSON_bool)1)
+
+#ifdef false
+#undef false
+#endif
+#define false ((cJSON_bool)0)
+
+/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */
+#ifndef isinf
+#define isinf(d) (isnan((d - d)) && !isnan(d))
+#endif
+#ifndef isnan
+#define isnan(d) (d != d)
+#endif
+
+#ifndef NAN
+#ifdef _WIN32
+#define NAN sqrt(-1.0)
+#else
+#define NAN 0.0/0.0
+#endif
+#endif
+
+typedef struct {
+    const unsigned char *json;
+    size_t position;
+} error;
+static error global_error = { NULL, 0 };
+
+CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void)
+{
+    return (const char*) (global_error.json + global_error.position);
+}
+
+CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item)
+{
+    if (!cJSON_IsString(item))
+    {
+        return NULL;
+    }
+
+    return item->valuestring;
+}
+
+CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item)
+{
+    if (!cJSON_IsNumber(item))
+    {
+        return (double) NAN;
+    }
+
+    return item->valuedouble;
+}
+
+/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */
+#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19)
+    #error cJSON.h and cJSON.c have different versions. Make sure that both have the same.
+#endif
+
+CJSON_PUBLIC(const char*) cJSON_Version(void)
+{
+    static char version[15];
+    sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH);
+
+    return version;
+}
+
+/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */
+static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2)
+{
+    if ((string1 == NULL) || (string2 == NULL))
+    {
+        return 1;
+    }
+
+    if (string1 == string2)
+    {
+        return 0;
+    }
+
+    for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++)
+    {
+        if (*string1 == '\0')
+        {
+            return 0;
+        }
+    }
+
+    return tolower(*string1) - tolower(*string2);
+}
+
+typedef struct internal_hooks
+{
+    void *(CJSON_CDECL *allocate)(size_t size);
+    void (CJSON_CDECL *deallocate)(void *pointer);
+    void *(CJSON_CDECL *reallocate)(void *pointer, size_t size);
+} internal_hooks;
+
+#if defined(_MSC_VER)
+/* work around MSVC error C2322: '...' address of dllimport '...' is not static */
+static void * CJSON_CDECL internal_malloc(size_t size)
+{
+    return malloc(size);
+}
+static void CJSON_CDECL internal_free(void *pointer)
+{
+    free(pointer);
+}
+static void * CJSON_CDECL internal_realloc(void *pointer, size_t size)
+{
+    return realloc(pointer, size);
+}
+#else
+#define internal_malloc malloc
+#define internal_free free
+#define internal_realloc realloc
+#endif
+
+/* strlen of character literals resolved at compile time */
+#define static_strlen(string_literal) (sizeof(string_literal) - sizeof(""))
+
+static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc };
+
+static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks)
+{
+    size_t length = 0;
+    unsigned char *copy = NULL;
+
+    if (string == NULL)
+    {
+        return NULL;
+    }
+
+    length = strlen((const char*)string) + sizeof("");
+    copy = (unsigned char*)hooks->allocate(length);
+    if (copy == NULL)
+    {
+        return NULL;
+    }
+    memcpy(copy, string, length);
+
+    return copy;
+}
+
+CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks)
+{
+    if (hooks == NULL)
+    {
+        /* Reset hooks */
+        global_hooks.allocate = malloc;
+        global_hooks.deallocate = free;
+        global_hooks.reallocate = realloc;
+        return;
+    }
+
+    global_hooks.allocate = malloc;
+    if (hooks->malloc_fn != NULL)
+    {
+        global_hooks.allocate = hooks->malloc_fn;
+    }
+
+    global_hooks.deallocate = free;
+    if (hooks->free_fn != NULL)
+    {
+        global_hooks.deallocate = hooks->free_fn;
+    }
+
+    /* use realloc only if both free and malloc are used */
+    global_hooks.reallocate = NULL;
+    if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free))
+    {
+        global_hooks.reallocate = realloc;
+    }
+}
+
+/* Internal constructor. */
+static cJSON *cJSON_New_Item(const internal_hooks * const hooks)
+{
+    cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON));
+    if (node)
+    {
+        memset(node, '\0', sizeof(cJSON));
+    }
+
+    return node;
+}
+
+/* Delete a cJSON structure. */
+CJSON_PUBLIC(void) cJSON_Delete(cJSON *item)
+{
+    cJSON *next = NULL;
+    while (item != NULL)
+    {
+        next = item->next;
+        if (!(item->type & cJSON_IsReference) && (item->child != NULL))
+        {
+            cJSON_Delete(item->child);
+        }
+        if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL))
+        {
+            global_hooks.deallocate(item->valuestring);
+            item->valuestring = NULL;
+        }
+        if (!(item->type & cJSON_StringIsConst) && (item->string != NULL))
+        {
+            global_hooks.deallocate(item->string);
+            item->string = NULL;
+        }
+        global_hooks.deallocate(item);
+        item = next;
+    }
+}
+
+/* get the decimal point character of the current locale */
+static unsigned char get_decimal_point(void)
+{
+#ifdef ENABLE_LOCALES
+    struct lconv *lconv = localeconv();
+    return (unsigned char) lconv->decimal_point[0];
+#else
+    return '.';
+#endif
+}
+
+typedef struct
+{
+    const unsigned char *content;
+    size_t length;
+    size_t offset;
+    size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */
+    internal_hooks hooks;
+} parse_buffer;
+
+/* check if the given size is left to read in a given parse buffer (starting with 1) */
+#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length))
+/* check if the buffer can be accessed at the given index (starting with 0) */
+#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length))
+#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index))
+/* get a pointer to the buffer at the position */
+#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset)
+
+/* Parse the input text to generate a number, and populate the result into item. */
+static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer)
+{
+    double number = 0;
+    unsigned char *after_end = NULL;
+    unsigned char *number_c_string;
+    unsigned char decimal_point = get_decimal_point();
+    size_t i = 0;
+    size_t number_string_length = 0;
+    cJSON_bool has_decimal_point = false;
+
+    if ((input_buffer == NULL) || (input_buffer->content == NULL))
+    {
+        return false;
+    }
+
+    /* copy the number into a temporary buffer and replace '.' with the decimal point
+     * of the current locale (for strtod)
+     * This also takes care of '\0' not necessarily being available for marking the end of the input */
+    for (i = 0; can_access_at_index(input_buffer, i); i++)
+    {
+        switch (buffer_at_offset(input_buffer)[i])
+        {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            case '+':
+            case '-':
+            case 'e':
+            case 'E':
+                number_string_length++;
+                break;
+
+            case '.':
+                number_string_length++;
+                has_decimal_point = true;
+                break;
+
+            default:
+                goto loop_end;
+        }
+    }
+loop_end:
+    /* malloc for temporary buffer, add 1 for '\0' */
+    number_c_string = (unsigned char *) input_buffer->hooks.allocate(number_string_length + 1);
+    if (number_c_string == NULL)
+    {
+        return false; /* allocation failure */
+    }
+
+    memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length);
+    number_c_string[number_string_length] = '\0';
+
+    if (has_decimal_point)
+    {
+        for (i = 0; i < number_string_length; i++)
+        {
+            if (number_c_string[i] == '.')
+            {
+                /* replace '.' with the decimal point of the current locale (for strtod) */
+                number_c_string[i] = decimal_point;
+            }
+        }
+    }
+
+    number = strtod((const char*)number_c_string, (char**)&after_end);
+    if (number_c_string == after_end)
+    {
+        /* free the temporary buffer */
+        input_buffer->hooks.deallocate(number_c_string);
+        return false; /* parse_error */
+    }
+
+    item->valuedouble = number;
+
+    /* use saturation in case of overflow */
+    if (number >= INT_MAX)
+    {
+        item->valueint = INT_MAX;
+    }
+    else if (number <= (double)INT_MIN)
+    {
+        item->valueint = INT_MIN;
+    }
+    else
+    {
+        item->valueint = (int)number;
+    }
+
+    item->type = cJSON_Number;
+
+    input_buffer->offset += (size_t)(after_end - number_c_string);
+    /* free the temporary buffer */
+    input_buffer->hooks.deallocate(number_c_string);
+    return true;
+}
+
+/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */
+CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number)
+{
+    if (number >= INT_MAX)
+    {
+        object->valueint = INT_MAX;
+    }
+    else if (number <= (double)INT_MIN)
+    {
+        object->valueint = INT_MIN;
+    }
+    else
+    {
+        object->valueint = (int)number;
+    }
+
+    return object->valuedouble = number;
+}
+
+/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */
+CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring)
+{
+    char *copy = NULL;
+    size_t v1_len;
+    size_t v2_len;
+    /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */
+    if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference))
+    {
+        return NULL;
+    }
+    /* return NULL if the object is corrupted or valuestring is NULL */
+    if (object->valuestring == NULL || valuestring == NULL)
+    {
+        return NULL;
+    }
+
+    v1_len = strlen(valuestring);
+    v2_len = strlen(object->valuestring);
+
+    if (v1_len <= v2_len)
+    {
+        /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */
+        if (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring ))
+        {
+            return NULL;
+        }
+        strcpy(object->valuestring, valuestring);
+        return object->valuestring;
+    }
+    copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks);
+    if (copy == NULL)
+    {
+        return NULL;
+    }
+    if (object->valuestring != NULL)
+    {
+        cJSON_free(object->valuestring);
+    }
+    object->valuestring = copy;
+
+    return copy;
+}
+
+typedef struct
+{
+    unsigned char *buffer;
+    size_t length;
+    size_t offset;
+    size_t depth; /* current nesting depth (for formatted printing) */
+    cJSON_bool noalloc;
+    cJSON_bool format; /* is this print a formatted print */
+    internal_hooks hooks;
+} printbuffer;
+
+/* realloc printbuffer if necessary to have at least "needed" bytes more */
+static unsigned char* ensure(printbuffer * const p, size_t needed)
+{
+    unsigned char *newbuffer = NULL;
+    size_t newsize = 0;
+
+    if ((p == NULL) || (p->buffer == NULL))
+    {
+        return NULL;
+    }
+
+    if ((p->length > 0) && (p->offset >= p->length))
+    {
+        /* make sure that offset is valid */
+        return NULL;
+    }
+
+    if (needed > INT_MAX)
+    {
+        /* sizes bigger than INT_MAX are currently not supported */
+        return NULL;
+    }
+
+    needed += p->offset + 1;
+    if (needed <= p->length)
+    {
+        return p->buffer + p->offset;
+    }
+
+    if (p->noalloc) {
+        return NULL;
+    }
+
+    /* calculate new buffer size */
+    if (needed > (INT_MAX / 2))
+    {
+        /* overflow of int, use INT_MAX if possible */
+        if (needed <= INT_MAX)
+        {
+            newsize = INT_MAX;
+        }
+        else
+        {
+            return NULL;
+        }
+    }
+    else
+    {
+        newsize = needed * 2;
+    }
+
+    if (p->hooks.reallocate != NULL)
+    {
+        /* reallocate with realloc if available */
+        newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize);
+        if (newbuffer == NULL)
+        {
+            p->hooks.deallocate(p->buffer);
+            p->length = 0;
+            p->buffer = NULL;
+
+            return NULL;
+        }
+    }
+    else
+    {
+        /* otherwise reallocate manually */
+        newbuffer = (unsigned char*)p->hooks.allocate(newsize);
+        if (!newbuffer)
+        {
+            p->hooks.deallocate(p->buffer);
+            p->length = 0;
+            p->buffer = NULL;
+
+            return NULL;
+        }
+
+        memcpy(newbuffer, p->buffer, p->offset + 1);
+        p->hooks.deallocate(p->buffer);
+    }
+    p->length = newsize;
+    p->buffer = newbuffer;
+
+    return newbuffer + p->offset;
+}
+
+/* calculate the new length of the string in a printbuffer and update the offset */
+static void update_offset(printbuffer * const buffer)
+{
+    const unsigned char *buffer_pointer = NULL;
+    if ((buffer == NULL) || (buffer->buffer == NULL))
+    {
+        return;
+    }
+    buffer_pointer = buffer->buffer + buffer->offset;
+
+    buffer->offset += strlen((const char*)buffer_pointer);
+}
+
+/* securely comparison of floating-point variables */
+static cJSON_bool compare_double(double a, double b)
+{
+    double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b);
+    return (fabs(a - b) <= maxVal * DBL_EPSILON);
+}
+
+/* Render the number nicely from the given item into a string. */
+static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer)
+{
+    unsigned char *output_pointer = NULL;
+    double d = item->valuedouble;
+    int length = 0;
+    size_t i = 0;
+    unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */
+    unsigned char decimal_point = get_decimal_point();
+    double test = 0.0;
+
+    if (output_buffer == NULL)
+    {
+        return false;
+    }
+
+    /* This checks for NaN and Infinity */
+    if (isnan(d) || isinf(d))
+    {
+        length = sprintf((char*)number_buffer, "null");
+    }
+    else if(d == (double)item->valueint)
+    {
+        length = sprintf((char*)number_buffer, "%d", item->valueint);
+    }
+    else
+    {
+        /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */
+        length = sprintf((char*)number_buffer, "%1.15g", d);
+
+        /* Check whether the original double can be recovered */
+        if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d))
+        {
+            /* If not, print with 17 decimal places of precision */
+            length = sprintf((char*)number_buffer, "%1.17g", d);
+        }
+    }
+
+    /* sprintf failed or buffer overrun occurred */
+    if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1)))
+    {
+        return false;
+    }
+
+    /* reserve appropriate space in the output */
+    output_pointer = ensure(output_buffer, (size_t)length + sizeof(""));
+    if (output_pointer == NULL)
+    {
+        return false;
+    }
+
+    /* copy the printed number to the output and replace locale
+     * dependent decimal point with '.' */
+    for (i = 0; i < ((size_t)length); i++)
+    {
+        if (number_buffer[i] == decimal_point)
+        {
+            output_pointer[i] = '.';
+            continue;
+        }
+
+        output_pointer[i] = number_buffer[i];
+    }
+    output_pointer[i] = '\0';
+
+    output_buffer->offset += (size_t)length;
+
+    return true;
+}
+
+/* parse 4 digit hexadecimal number */
+static unsigned parse_hex4(const unsigned char * const input)
+{
+    unsigned int h = 0;
+    size_t i = 0;
+
+    for (i = 0; i < 4; i++)
+    {
+        /* parse digit */
+        if ((input[i] >= '0') && (input[i] <= '9'))
+        {
+            h += (unsigned int) input[i] - '0';
+        }
+        else if ((input[i] >= 'A') && (input[i] <= 'F'))
+        {
+            h += (unsigned int) 10 + input[i] - 'A';
+        }
+        else if ((input[i] >= 'a') && (input[i] <= 'f'))
+        {
+            h += (unsigned int) 10 + input[i] - 'a';
+        }
+        else /* invalid */
+        {
+            return 0;
+        }
+
+        if (i < 3)
+        {
+            /* shift left to make place for the next nibble */
+            h = h << 4;
+        }
+    }
+
+    return h;
+}
+
+/* converts a UTF-16 literal to UTF-8
+ * A literal can be one or two sequences of the form \uXXXX */
+static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer)
+{
+    long unsigned int codepoint = 0;
+    unsigned int first_code = 0;
+    const unsigned char *first_sequence = input_pointer;
+    unsigned char utf8_length = 0;
+    unsigned char utf8_position = 0;
+    unsigned char sequence_length = 0;
+    unsigned char first_byte_mark = 0;
+
+    if ((input_end - first_sequence) < 6)
+    {
+        /* input ends unexpectedly */
+        goto fail;
+    }
+
+    /* get the first utf16 sequence */
+    first_code = parse_hex4(first_sequence + 2);
+
+    /* check that the code is valid */
+    if (((first_code >= 0xDC00) && (first_code <= 0xDFFF)))
+    {
+        goto fail;
+    }
+
+    /* UTF16 surrogate pair */
+    if ((first_code >= 0xD800) && (first_code <= 0xDBFF))
+    {
+        const unsigned char *second_sequence = first_sequence + 6;
+        unsigned int second_code = 0;
+        sequence_length = 12; /* \uXXXX\uXXXX */
+
+        if ((input_end - second_sequence) < 6)
+        {
+            /* input ends unexpectedly */
+            goto fail;
+        }
+
+        if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u'))
+        {
+            /* missing second half of the surrogate pair */
+            goto fail;
+        }
+
+        /* get the second utf16 sequence */
+        second_code = parse_hex4(second_sequence + 2);
+        /* check that the code is valid */
+        if ((second_code < 0xDC00) || (second_code > 0xDFFF))
+        {
+            /* invalid second half of the surrogate pair */
+            goto fail;
+        }
+
+
+        /* calculate the unicode codepoint from the surrogate pair */
+        codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF));
+    }
+    else
+    {
+        sequence_length = 6; /* \uXXXX */
+        codepoint = first_code;
+    }
+
+    /* encode as UTF-8
+     * takes at maximum 4 bytes to encode:
+     * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */
+    if (codepoint < 0x80)
+    {
+        /* normal ascii, encoding 0xxxxxxx */
+        utf8_length = 1;
+    }
+    else if (codepoint < 0x800)
+    {
+        /* two bytes, encoding 110xxxxx 10xxxxxx */
+        utf8_length = 2;
+        first_byte_mark = 0xC0; /* 11000000 */
+    }
+    else if (codepoint < 0x10000)
+    {
+        /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */
+        utf8_length = 3;
+        first_byte_mark = 0xE0; /* 11100000 */
+    }
+    else if (codepoint <= 0x10FFFF)
+    {
+        /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */
+        utf8_length = 4;
+        first_byte_mark = 0xF0; /* 11110000 */
+    }
+    else
+    {
+        /* invalid unicode codepoint */
+        goto fail;
+    }
+
+    /* encode as utf8 */
+    for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--)
+    {
+        /* 10xxxxxx */
+        (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF);
+        codepoint >>= 6;
+    }
+    /* encode first byte */
+    if (utf8_length > 1)
+    {
+        (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF);
+    }
+    else
+    {
+        (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F);
+    }
+
+    *output_pointer += utf8_length;
+
+    return sequence_length;
+
+fail:
+    return 0;
+}
+
+/* Parse the input text into an unescaped cinput, and populate item. */
+static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer)
+{
+    const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1;
+    const unsigned char *input_end = buffer_at_offset(input_buffer) + 1;
+    unsigned char *output_pointer = NULL;
+    unsigned char *output = NULL;
+
+    /* not a string */
+    if (buffer_at_offset(input_buffer)[0] != '\"')
+    {
+        goto fail;
+    }
+
+    {
+        /* calculate approximate size of the output (overestimate) */
+        size_t allocation_length = 0;
+        size_t skipped_bytes = 0;
+        while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"'))
+        {
+            /* is escape sequence */
+            if (input_end[0] == '\\')
+            {
+                if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length)
+                {
+                    /* prevent buffer overflow when last input character is a backslash */
+                    goto fail;
+                }
+                skipped_bytes++;
+                input_end++;
+            }
+            input_end++;
+        }
+        if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"'))
+        {
+            goto fail; /* string ended unexpectedly */
+        }
+
+        /* This is at most how much we need for the output */
+        allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes;
+        output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof(""));
+        if (output == NULL)
+        {
+            goto fail; /* allocation failure */
+        }
+    }
+
+    output_pointer = output;
+    /* loop through the string literal */
+    while (input_pointer < input_end)
+    {
+        if (*input_pointer != '\\')
+        {
+            *output_pointer++ = *input_pointer++;
+        }
+        /* escape sequence */
+        else
+        {
+            unsigned char sequence_length = 2;
+            if ((input_end - input_pointer) < 1)
+            {
+                goto fail;
+            }
+
+            switch (input_pointer[1])
+            {
+                case 'b':
+                    *output_pointer++ = '\b';
+                    break;
+                case 'f':
+                    *output_pointer++ = '\f';
+                    break;
+                case 'n':
+                    *output_pointer++ = '\n';
+                    break;
+                case 'r':
+                    *output_pointer++ = '\r';
+                    break;
+                case 't':
+                    *output_pointer++ = '\t';
+                    break;
+                case '\"':
+                case '\\':
+                case '/':
+                    *output_pointer++ = input_pointer[1];
+                    break;
+
+                /* UTF-16 literal */
+                case 'u':
+                    sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer);
+                    if (sequence_length == 0)
+                    {
+                        /* failed to convert UTF16-literal to UTF-8 */
+                        goto fail;
+                    }
+                    break;
+
+                default:
+                    goto fail;
+            }
+            input_pointer += sequence_length;
+        }
+    }
+
+    /* zero terminate the output */
+    *output_pointer = '\0';
+
+    item->type = cJSON_String;
+    item->valuestring = (char*)output;
+
+    input_buffer->offset = (size_t) (input_end - input_buffer->content);
+    input_buffer->offset++;
+
+    return true;
+
+fail:
+    if (output != NULL)
+    {
+        input_buffer->hooks.deallocate(output);
+        output = NULL;
+    }
+
+    if (input_pointer != NULL)
+    {
+        input_buffer->offset = (size_t)(input_pointer - input_buffer->content);
+    }
+
+    return false;
+}
+
+/* Render the cstring provided to an escaped version that can be printed. */
+static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer)
+{
+    const unsigned char *input_pointer = NULL;
+    unsigned char *output = NULL;
+    unsigned char *output_pointer = NULL;
+    size_t output_length = 0;
+    /* numbers of additional characters needed for escaping */
+    size_t escape_characters = 0;
+
+    if (output_buffer == NULL)
+    {
+        return false;
+    }
+
+    /* empty string */
+    if (input == NULL)
+    {
+        output = ensure(output_buffer, sizeof("\"\""));
+        if (output == NULL)
+        {
+            return false;
+        }
+        strcpy((char*)output, "\"\"");
+
+        return true;
+    }
+
+    /* set "flag" to 1 if something needs to be escaped */
+    for (input_pointer = input; *input_pointer; input_pointer++)
+    {
+        switch (*input_pointer)
+        {
+            case '\"':
+            case '\\':
+            case '\b':
+            case '\f':
+            case '\n':
+            case '\r':
+            case '\t':
+                /* one character escape sequence */
+                escape_characters++;
+                break;
+            default:
+                if (*input_pointer < 32)
+                {
+                    /* UTF-16 escape sequence uXXXX */
+                    escape_characters += 5;
+                }
+                break;
+        }
+    }
+    output_length = (size_t)(input_pointer - input) + escape_characters;
+
+    output = ensure(output_buffer, output_length + sizeof("\"\""));
+    if (output == NULL)
+    {
+        return false;
+    }
+
+    /* no characters have to be escaped */
+    if (escape_characters == 0)
+    {
+        output[0] = '\"';
+        memcpy(output + 1, input, output_length);
+        output[output_length + 1] = '\"';
+        output[output_length + 2] = '\0';
+
+        return true;
+    }
+
+    output[0] = '\"';
+    output_pointer = output + 1;
+    /* copy the string */
+    for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++)
+    {
+        if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\'))
+        {
+            /* normal character, copy */
+            *output_pointer = *input_pointer;
+        }
+        else
+        {
+            /* character needs to be escaped */
+            *output_pointer++ = '\\';
+            switch (*input_pointer)
+            {
+                case '\\':
+                    *output_pointer = '\\';
+                    break;
+                case '\"':
+                    *output_pointer = '\"';
+                    break;
+                case '\b':
+                    *output_pointer = 'b';
+                    break;
+                case '\f':
+                    *output_pointer = 'f';
+                    break;
+                case '\n':
+                    *output_pointer = 'n';
+                    break;
+                case '\r':
+                    *output_pointer = 'r';
+                    break;
+                case '\t':
+                    *output_pointer = 't';
+                    break;
+                default:
+                    /* escape and print as unicode codepoint */
+                    sprintf((char*)output_pointer, "u%04x", *input_pointer);
+                    output_pointer += 4;
+                    break;
+            }
+        }
+    }
+    output[output_length + 1] = '\"';
+    output[output_length + 2] = '\0';
+
+    return true;
+}
+
+/* Invoke print_string_ptr (which is useful) on an item. */
+static cJSON_bool print_string(const cJSON * const item, printbuffer * const p)
+{
+    return print_string_ptr((unsigned char*)item->valuestring, p);
+}
+
+/* Predeclare these prototypes. */
+static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer);
+static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer);
+static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer);
+static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer);
+static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer);
+static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer);
+
+/* Utility to jump whitespace and cr/lf */
+static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer)
+{
+    if ((buffer == NULL) || (buffer->content == NULL))
+    {
+        return NULL;
+    }
+
+    if (cannot_access_at_index(buffer, 0))
+    {
+        return buffer;
+    }
+
+    while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32))
+    {
+       buffer->offset++;
+    }
+
+    if (buffer->offset == buffer->length)
+    {
+        buffer->offset--;
+    }
+
+    return buffer;
+}
+
+/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */
+static parse_buffer *skip_utf8_bom(parse_buffer * const buffer)
+{
+    if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0))
+    {
+        return NULL;
+    }
+
+    if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0))
+    {
+        buffer->offset += 3;
+    }
+
+    return buffer;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated)
+{
+    size_t buffer_length;
+
+    if (NULL == value)
+    {
+        return NULL;
+    }
+
+    /* Adding null character size due to require_null_terminated. */
+    buffer_length = strlen(value) + sizeof("");
+
+    return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated);
+}
+
+/* Parse an object - create a new root, and populate. */
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated)
+{
+    parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } };
+    cJSON *item = NULL;
+
+    /* reset error position */
+    global_error.json = NULL;
+    global_error.position = 0;
+
+    if (value == NULL || 0 == buffer_length)
+    {
+        goto fail;
+    }
+
+    buffer.content = (const unsigned char*)value;
+    buffer.length = buffer_length;
+    buffer.offset = 0;
+    buffer.hooks = global_hooks;
+
+    item = cJSON_New_Item(&global_hooks);
+    if (item == NULL) /* memory fail */
+    {
+        goto fail;
+    }
+
+    if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer))))
+    {
+        /* parse failure. ep is set. */
+        goto fail;
+    }
+
+    /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */
+    if (require_null_terminated)
+    {
+        buffer_skip_whitespace(&buffer);
+        if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0')
+        {
+            goto fail;
+        }
+    }
+    if (return_parse_end)
+    {
+        *return_parse_end = (const char*)buffer_at_offset(&buffer);
+    }
+
+    return item;
+
+fail:
+    if (item != NULL)
+    {
+        cJSON_Delete(item);
+    }
+
+    if (value != NULL)
+    {
+        error local_error;
+        local_error.json = (const unsigned char*)value;
+        local_error.position = 0;
+
+        if (buffer.offset < buffer.length)
+        {
+            local_error.position = buffer.offset;
+        }
+        else if (buffer.length > 0)
+        {
+            local_error.position = buffer.length - 1;
+        }
+
+        if (return_parse_end != NULL)
+        {
+            *return_parse_end = (const char*)local_error.json + local_error.position;
+        }
+
+        global_error = local_error;
+    }
+
+    return NULL;
+}
+
+/* Default options for cJSON_Parse */
+CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value)
+{
+    return cJSON_ParseWithOpts(value, 0, 0);
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length)
+{
+    return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0);
+}
+
+#define cjson_min(a, b) (((a) < (b)) ? (a) : (b))
+
+static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks)
+{
+    static const size_t default_buffer_size = 256;
+    printbuffer buffer[1];
+    unsigned char *printed = NULL;
+
+    memset(buffer, 0, sizeof(buffer));
+
+    /* create buffer */
+    buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size);
+    buffer->length = default_buffer_size;
+    buffer->format = format;
+    buffer->hooks = *hooks;
+    if (buffer->buffer == NULL)
+    {
+        goto fail;
+    }
+
+    /* print the value */
+    if (!print_value(item, buffer))
+    {
+        goto fail;
+    }
+    update_offset(buffer);
+
+    /* check if reallocate is available */
+    if (hooks->reallocate != NULL)
+    {
+        printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1);
+        if (printed == NULL) {
+            goto fail;
+        }
+        buffer->buffer = NULL;
+    }
+    else /* otherwise copy the JSON over to a new buffer */
+    {
+        printed = (unsigned char*) hooks->allocate(buffer->offset + 1);
+        if (printed == NULL)
+        {
+            goto fail;
+        }
+        memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1));
+        printed[buffer->offset] = '\0'; /* just to be sure */
+
+        /* free the buffer */
+        hooks->deallocate(buffer->buffer);
+        buffer->buffer = NULL;
+    }
+
+    return printed;
+
+fail:
+    if (buffer->buffer != NULL)
+    {
+        hooks->deallocate(buffer->buffer);
+        buffer->buffer = NULL;
+    }
+
+    if (printed != NULL)
+    {
+        hooks->deallocate(printed);
+        printed = NULL;
+    }
+
+    return NULL;
+}
+
+/* Render a cJSON item/entity/structure to text. */
+CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)
+{
+    return (char*)print(item, true, &global_hooks);
+}
+
+CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item)
+{
+    return (char*)print(item, false, &global_hooks);
+}
+
+CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt)
+{
+    printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } };
+
+    if (prebuffer < 0)
+    {
+        return NULL;
+    }
+
+    p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer);
+    if (!p.buffer)
+    {
+        return NULL;
+    }
+
+    p.length = (size_t)prebuffer;
+    p.offset = 0;
+    p.noalloc = false;
+    p.format = fmt;
+    p.hooks = global_hooks;
+
+    if (!print_value(item, &p))
+    {
+        global_hooks.deallocate(p.buffer);
+        p.buffer = NULL;
+        return NULL;
+    }
+
+    return (char*)p.buffer;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format)
+{
+    printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } };
+
+    if ((length < 0) || (buffer == NULL))
+    {
+        return false;
+    }
+
+    p.buffer = (unsigned char*)buffer;
+    p.length = (size_t)length;
+    p.offset = 0;
+    p.noalloc = true;
+    p.format = format;
+    p.hooks = global_hooks;
+
+    return print_value(item, &p);
+}
+
+/* Parser core - when encountering text, process appropriately. */
+static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer)
+{
+    if ((input_buffer == NULL) || (input_buffer->content == NULL))
+    {
+        return false; /* no input */
+    }
+
+    /* parse the different types of values */
+    /* null */
+    if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0))
+    {
+        item->type = cJSON_NULL;
+        input_buffer->offset += 4;
+        return true;
+    }
+    /* false */
+    if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0))
+    {
+        item->type = cJSON_False;
+        input_buffer->offset += 5;
+        return true;
+    }
+    /* true */
+    if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0))
+    {
+        item->type = cJSON_True;
+        item->valueint = 1;
+        input_buffer->offset += 4;
+        return true;
+    }
+    /* string */
+    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"'))
+    {
+        return parse_string(item, input_buffer);
+    }
+    /* number */
+    if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9'))))
+    {
+        return parse_number(item, input_buffer);
+    }
+    /* array */
+    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '['))
+    {
+        return parse_array(item, input_buffer);
+    }
+    /* object */
+    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{'))
+    {
+        return parse_object(item, input_buffer);
+    }
+
+    return false;
+}
+
+/* Render a value to text. */
+static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer)
+{
+    unsigned char *output = NULL;
+
+    if ((item == NULL) || (output_buffer == NULL))
+    {
+        return false;
+    }
+
+    switch ((item->type) & 0xFF)
+    {
+        case cJSON_NULL:
+            output = ensure(output_buffer, 5);
+            if (output == NULL)
+            {
+                return false;
+            }
+            strcpy((char*)output, "null");
+            return true;
+
+        case cJSON_False:
+            output = ensure(output_buffer, 6);
+            if (output == NULL)
+            {
+                return false;
+            }
+            strcpy((char*)output, "false");
+            return true;
+
+        case cJSON_True:
+            output = ensure(output_buffer, 5);
+            if (output == NULL)
+            {
+                return false;
+            }
+            strcpy((char*)output, "true");
+            return true;
+
+        case cJSON_Number:
+            return print_number(item, output_buffer);
+
+        case cJSON_Raw:
+        {
+            size_t raw_length = 0;
+            if (item->valuestring == NULL)
+            {
+                return false;
+            }
+
+            raw_length = strlen(item->valuestring) + sizeof("");
+            output = ensure(output_buffer, raw_length);
+            if (output == NULL)
+            {
+                return false;
+            }
+            memcpy(output, item->valuestring, raw_length);
+            return true;
+        }
+
+        case cJSON_String:
+            return print_string(item, output_buffer);
+
+        case cJSON_Array:
+            return print_array(item, output_buffer);
+
+        case cJSON_Object:
+            return print_object(item, output_buffer);
+
+        default:
+            return false;
+    }
+}
+
+/* Build an array from input text. */
+static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer)
+{
+    cJSON *head = NULL; /* head of the linked list */
+    cJSON *current_item = NULL;
+
+    if (input_buffer->depth >= CJSON_NESTING_LIMIT)
+    {
+        return false; /* to deeply nested */
+    }
+    input_buffer->depth++;
+
+    if (buffer_at_offset(input_buffer)[0] != '[')
+    {
+        /* not an array */
+        goto fail;
+    }
+
+    input_buffer->offset++;
+    buffer_skip_whitespace(input_buffer);
+    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']'))
+    {
+        /* empty array */
+        goto success;
+    }
+
+    /* check if we skipped to the end of the buffer */
+    if (cannot_access_at_index(input_buffer, 0))
+    {
+        input_buffer->offset--;
+        goto fail;
+    }
+
+    /* step back to character in front of the first element */
+    input_buffer->offset--;
+    /* loop through the comma separated array elements */
+    do
+    {
+        /* allocate next item */
+        cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks));
+        if (new_item == NULL)
+        {
+            goto fail; /* allocation failure */
+        }
+
+        /* attach next item to list */
+        if (head == NULL)
+        {
+            /* start the linked list */
+            current_item = head = new_item;
+        }
+        else
+        {
+            /* add to the end and advance */
+            current_item->next = new_item;
+            new_item->prev = current_item;
+            current_item = new_item;
+        }
+
+        /* parse next value */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if (!parse_value(current_item, input_buffer))
+        {
+            goto fail; /* failed to parse value */
+        }
+        buffer_skip_whitespace(input_buffer);
+    }
+    while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
+
+    if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']')
+    {
+        goto fail; /* expected end of array */
+    }
+
+success:
+    input_buffer->depth--;
+
+    if (head != NULL) {
+        head->prev = current_item;
+    }
+
+    item->type = cJSON_Array;
+    item->child = head;
+
+    input_buffer->offset++;
+
+    return true;
+
+fail:
+    if (head != NULL)
+    {
+        cJSON_Delete(head);
+    }
+
+    return false;
+}
+
+/* Render an array to text */
+static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer)
+{
+    unsigned char *output_pointer = NULL;
+    size_t length = 0;
+    cJSON *current_element = item->child;
+
+    if (output_buffer == NULL)
+    {
+        return false;
+    }
+
+    /* Compose the output array. */
+    /* opening square bracket */
+    output_pointer = ensure(output_buffer, 1);
+    if (output_pointer == NULL)
+    {
+        return false;
+    }
+
+    *output_pointer = '[';
+    output_buffer->offset++;
+    output_buffer->depth++;
+
+    while (current_element != NULL)
+    {
+        if (!print_value(current_element, output_buffer))
+        {
+            return false;
+        }
+        update_offset(output_buffer);
+        if (current_element->next)
+        {
+            length = (size_t) (output_buffer->format ? 2 : 1);
+            output_pointer = ensure(output_buffer, length + 1);
+            if (output_pointer == NULL)
+            {
+                return false;
+            }
+            *output_pointer++ = ',';
+            if(output_buffer->format)
+            {
+                *output_pointer++ = ' ';
+            }
+            *output_pointer = '\0';
+            output_buffer->offset += length;
+        }
+        current_element = current_element->next;
+    }
+
+    output_pointer = ensure(output_buffer, 2);
+    if (output_pointer == NULL)
+    {
+        return false;
+    }
+    *output_pointer++ = ']';
+    *output_pointer = '\0';
+    output_buffer->depth--;
+
+    return true;
+}
+
+/* Build an object from the text. */
+static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer)
+{
+    cJSON *head = NULL; /* linked list head */
+    cJSON *current_item = NULL;
+
+    if (input_buffer->depth >= CJSON_NESTING_LIMIT)
+    {
+        return false; /* to deeply nested */
+    }
+    input_buffer->depth++;
+
+    if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{'))
+    {
+        goto fail; /* not an object */
+    }
+
+    input_buffer->offset++;
+    buffer_skip_whitespace(input_buffer);
+    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}'))
+    {
+        goto success; /* empty object */
+    }
+
+    /* check if we skipped to the end of the buffer */
+    if (cannot_access_at_index(input_buffer, 0))
+    {
+        input_buffer->offset--;
+        goto fail;
+    }
+
+    /* step back to character in front of the first element */
+    input_buffer->offset--;
+    /* loop through the comma separated array elements */
+    do
+    {
+        /* allocate next item */
+        cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks));
+        if (new_item == NULL)
+        {
+            goto fail; /* allocation failure */
+        }
+
+        /* attach next item to list */
+        if (head == NULL)
+        {
+            /* start the linked list */
+            current_item = head = new_item;
+        }
+        else
+        {
+            /* add to the end and advance */
+            current_item->next = new_item;
+            new_item->prev = current_item;
+            current_item = new_item;
+        }
+
+        if (cannot_access_at_index(input_buffer, 1))
+        {
+            goto fail; /* nothing comes after the comma */
+        }
+
+        /* parse the name of the child */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if (!parse_string(current_item, input_buffer))
+        {
+            goto fail; /* failed to parse name */
+        }
+        buffer_skip_whitespace(input_buffer);
+
+        /* swap valuestring and string, because we parsed the name */
+        current_item->string = current_item->valuestring;
+        current_item->valuestring = NULL;
+
+        if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':'))
+        {
+            goto fail; /* invalid object */
+        }
+
+        /* parse the value */
+        input_buffer->offset++;
+        buffer_skip_whitespace(input_buffer);
+        if (!parse_value(current_item, input_buffer))
+        {
+            goto fail; /* failed to parse value */
+        }
+        buffer_skip_whitespace(input_buffer);
+    }
+    while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
+
+    if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}'))
+    {
+        goto fail; /* expected end of object */
+    }
+
+success:
+    input_buffer->depth--;
+
+    if (head != NULL) {
+        head->prev = current_item;
+    }
+
+    item->type = cJSON_Object;
+    item->child = head;
+
+    input_buffer->offset++;
+    return true;
+
+fail:
+    if (head != NULL)
+    {
+        cJSON_Delete(head);
+    }
+
+    return false;
+}
+
+/* Render an object to text. */
+static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer)
+{
+    unsigned char *output_pointer = NULL;
+    size_t length = 0;
+    cJSON *current_item = item->child;
+
+    if (output_buffer == NULL)
+    {
+        return false;
+    }
+
+    /* Compose the output: */
+    length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */
+    output_pointer = ensure(output_buffer, length + 1);
+    if (output_pointer == NULL)
+    {
+        return false;
+    }
+
+    *output_pointer++ = '{';
+    output_buffer->depth++;
+    if (output_buffer->format)
+    {
+        *output_pointer++ = '\n';
+    }
+    output_buffer->offset += length;
+
+    while (current_item)
+    {
+        if (output_buffer->format)
+        {
+            size_t i;
+            output_pointer = ensure(output_buffer, output_buffer->depth);
+            if (output_pointer == NULL)
+            {
+                return false;
+            }
+            for (i = 0; i < output_buffer->depth; i++)
+            {
+                *output_pointer++ = '\t';
+            }
+            output_buffer->offset += output_buffer->depth;
+        }
+
+        /* print key */
+        if (!print_string_ptr((unsigned char*)current_item->string, output_buffer))
+        {
+            return false;
+        }
+        update_offset(output_buffer);
+
+        length = (size_t) (output_buffer->format ? 2 : 1);
+        output_pointer = ensure(output_buffer, length);
+        if (output_pointer == NULL)
+        {
+            return false;
+        }
+        *output_pointer++ = ':';
+        if (output_buffer->format)
+        {
+            *output_pointer++ = '\t';
+        }
+        output_buffer->offset += length;
+
+        /* print value */
+        if (!print_value(current_item, output_buffer))
+        {
+            return false;
+        }
+        update_offset(output_buffer);
+
+        /* print comma if not last */
+        length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0));
+        output_pointer = ensure(output_buffer, length + 1);
+        if (output_pointer == NULL)
+        {
+            return false;
+        }
+        if (current_item->next)
+        {
+            *output_pointer++ = ',';
+        }
+
+        if (output_buffer->format)
+        {
+            *output_pointer++ = '\n';
+        }
+        *output_pointer = '\0';
+        output_buffer->offset += length;
+
+        current_item = current_item->next;
+    }
+
+    output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2);
+    if (output_pointer == NULL)
+    {
+        return false;
+    }
+    if (output_buffer->format)
+    {
+        size_t i;
+        for (i = 0; i < (output_buffer->depth - 1); i++)
+        {
+            *output_pointer++ = '\t';
+        }
+    }
+    *output_pointer++ = '}';
+    *output_pointer = '\0';
+    output_buffer->depth--;
+
+    return true;
+}
+
+/* Get Array size/item / object item. */
+CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array)
+{
+    cJSON *child = NULL;
+    size_t size = 0;
+
+    if (array == NULL)
+    {
+        return 0;
+    }
+
+    child = array->child;
+
+    while(child != NULL)
+    {
+        size++;
+        child = child->next;
+    }
+
+    /* FIXME: Can overflow here. Cannot be fixed without breaking the API */
+
+    return (int)size;
+}
+
+static cJSON* get_array_item(const cJSON *array, size_t index)
+{
+    cJSON *current_child = NULL;
+
+    if (array == NULL)
+    {
+        return NULL;
+    }
+
+    current_child = array->child;
+    while ((current_child != NULL) && (index > 0))
+    {
+        index--;
+        current_child = current_child->next;
+    }
+
+    return current_child;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index)
+{
+    if (index < 0)
+    {
+        return NULL;
+    }
+
+    return get_array_item(array, (size_t)index);
+}
+
+static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive)
+{
+    cJSON *current_element = NULL;
+
+    if ((object == NULL) || (name == NULL))
+    {
+        return NULL;
+    }
+
+    current_element = object->child;
+    if (case_sensitive)
+    {
+        while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0))
+        {
+            current_element = current_element->next;
+        }
+    }
+    else
+    {
+        while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0))
+        {
+            current_element = current_element->next;
+        }
+    }
+
+    if ((current_element == NULL) || (current_element->string == NULL)) {
+        return NULL;
+    }
+
+    return current_element;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string)
+{
+    return get_object_item(object, string, false);
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string)
+{
+    return get_object_item(object, string, true);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string)
+{
+    return cJSON_GetObjectItem(object, string) ? 1 : 0;
+}
+
+/* Utility for array list handling. */
+static void suffix_object(cJSON *prev, cJSON *item)
+{
+    prev->next = item;
+    item->prev = prev;
+}
+
+/* Utility for handling references. */
+static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks)
+{
+    cJSON *reference = NULL;
+    if (item == NULL)
+    {
+        return NULL;
+    }
+
+    reference = cJSON_New_Item(hooks);
+    if (reference == NULL)
+    {
+        return NULL;
+    }
+
+    memcpy(reference, item, sizeof(cJSON));
+    reference->string = NULL;
+    reference->type |= cJSON_IsReference;
+    reference->next = reference->prev = NULL;
+    return reference;
+}
+
+static cJSON_bool add_item_to_array(cJSON *array, cJSON *item)
+{
+    cJSON *child = NULL;
+
+    if ((item == NULL) || (array == NULL) || (array == item))
+    {
+        return false;
+    }
+
+    child = array->child;
+    /*
+     * To find the last item in array quickly, we use prev in array
+     */
+    if (child == NULL)
+    {
+        /* list is empty, start new one */
+        array->child = item;
+        item->prev = item;
+        item->next = NULL;
+    }
+    else
+    {
+        /* append to the end */
+        if (child->prev)
+        {
+            suffix_object(child->prev, item);
+            array->child->prev = item;
+        }
+    }
+
+    return true;
+}
+
+/* Add item to array/object. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item)
+{
+    return add_item_to_array(array, item);
+}
+
+#if defined(__clang__) || (defined(__GNUC__)  && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5))))
+    #pragma GCC diagnostic push
+#endif
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wcast-qual"
+#endif
+/* helper function to cast away const */
+static void* cast_away_const(const void* string)
+{
+    return (void*)string;
+}
+#if defined(__clang__) || (defined(__GNUC__)  && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5))))
+    #pragma GCC diagnostic pop
+#endif
+
+
+static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key)
+{
+    char *new_key = NULL;
+    int new_type = cJSON_Invalid;
+
+    if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item))
+    {
+        return false;
+    }
+
+    if (constant_key)
+    {
+        new_key = (char*)cast_away_const(string);
+        new_type = item->type | cJSON_StringIsConst;
+    }
+    else
+    {
+        new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks);
+        if (new_key == NULL)
+        {
+            return false;
+        }
+
+        new_type = item->type & ~cJSON_StringIsConst;
+    }
+
+    if (!(item->type & cJSON_StringIsConst) && (item->string != NULL))
+    {
+        hooks->deallocate(item->string);
+    }
+
+    item->string = new_key;
+    item->type = new_type;
+
+    return add_item_to_array(object, item);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item)
+{
+    return add_item_to_object(object, string, item, &global_hooks, false);
+}
+
+/* Add an item to an object with constant string as key */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item)
+{
+    return add_item_to_object(object, string, item, &global_hooks, true);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item)
+{
+    if (array == NULL)
+    {
+        return false;
+    }
+
+    return add_item_to_array(array, create_reference(item, &global_hooks));
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item)
+{
+    if ((object == NULL) || (string == NULL))
+    {
+        return false;
+    }
+
+    return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false);
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name)
+{
+    cJSON *null = cJSON_CreateNull();
+    if (add_item_to_object(object, name, null, &global_hooks, false))
+    {
+        return null;
+    }
+
+    cJSON_Delete(null);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name)
+{
+    cJSON *true_item = cJSON_CreateTrue();
+    if (add_item_to_object(object, name, true_item, &global_hooks, false))
+    {
+        return true_item;
+    }
+
+    cJSON_Delete(true_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name)
+{
+    cJSON *false_item = cJSON_CreateFalse();
+    if (add_item_to_object(object, name, false_item, &global_hooks, false))
+    {
+        return false_item;
+    }
+
+    cJSON_Delete(false_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean)
+{
+    cJSON *bool_item = cJSON_CreateBool(boolean);
+    if (add_item_to_object(object, name, bool_item, &global_hooks, false))
+    {
+        return bool_item;
+    }
+
+    cJSON_Delete(bool_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number)
+{
+    cJSON *number_item = cJSON_CreateNumber(number);
+    if (add_item_to_object(object, name, number_item, &global_hooks, false))
+    {
+        return number_item;
+    }
+
+    cJSON_Delete(number_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string)
+{
+    cJSON *string_item = cJSON_CreateString(string);
+    if (add_item_to_object(object, name, string_item, &global_hooks, false))
+    {
+        return string_item;
+    }
+
+    cJSON_Delete(string_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw)
+{
+    cJSON *raw_item = cJSON_CreateRaw(raw);
+    if (add_item_to_object(object, name, raw_item, &global_hooks, false))
+    {
+        return raw_item;
+    }
+
+    cJSON_Delete(raw_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name)
+{
+    cJSON *object_item = cJSON_CreateObject();
+    if (add_item_to_object(object, name, object_item, &global_hooks, false))
+    {
+        return object_item;
+    }
+
+    cJSON_Delete(object_item);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name)
+{
+    cJSON *array = cJSON_CreateArray();
+    if (add_item_to_object(object, name, array, &global_hooks, false))
+    {
+        return array;
+    }
+
+    cJSON_Delete(array);
+    return NULL;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item)
+{
+    if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL))
+    {
+        return NULL;
+    }
+
+    if (item != parent->child)
+    {
+        /* not the first element */
+        item->prev->next = item->next;
+    }
+    if (item->next != NULL)
+    {
+        /* not the last element */
+        item->next->prev = item->prev;
+    }
+
+    if (item == parent->child)
+    {
+        /* first element */
+        parent->child = item->next;
+    }
+    else if (item->next == NULL)
+    {
+        /* last element */
+        parent->child->prev = item->prev;
+    }
+
+    /* make sure the detached item doesn't point anywhere anymore */
+    item->prev = NULL;
+    item->next = NULL;
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which)
+{
+    if (which < 0)
+    {
+        return NULL;
+    }
+
+    return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which));
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which)
+{
+    cJSON_Delete(cJSON_DetachItemFromArray(array, which));
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string)
+{
+    cJSON *to_detach = cJSON_GetObjectItem(object, string);
+
+    return cJSON_DetachItemViaPointer(object, to_detach);
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string)
+{
+    cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string);
+
+    return cJSON_DetachItemViaPointer(object, to_detach);
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string)
+{
+    cJSON_Delete(cJSON_DetachItemFromObject(object, string));
+}
+
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string)
+{
+    cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string));
+}
+
+/* Replace array/object items with new ones. */
+CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem)
+{
+    cJSON *after_inserted = NULL;
+
+    if (which < 0 || newitem == NULL)
+    {
+        return false;
+    }
+
+    after_inserted = get_array_item(array, (size_t)which);
+    if (after_inserted == NULL)
+    {
+        return add_item_to_array(array, newitem);
+    }
+
+    if (after_inserted != array->child && after_inserted->prev == NULL) {
+        /* return false if after_inserted is a corrupted array item */
+        return false;
+    }
+
+    newitem->next = after_inserted;
+    newitem->prev = after_inserted->prev;
+    after_inserted->prev = newitem;
+    if (after_inserted == array->child)
+    {
+        array->child = newitem;
+    }
+    else
+    {
+        newitem->prev->next = newitem;
+    }
+    return true;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement)
+{
+    if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL))
+    {
+        return false;
+    }
+
+    if (replacement == item)
+    {
+        return true;
+    }
+
+    replacement->next = item->next;
+    replacement->prev = item->prev;
+
+    if (replacement->next != NULL)
+    {
+        replacement->next->prev = replacement;
+    }
+    if (parent->child == item)
+    {
+        if (parent->child->prev == parent->child)
+        {
+            replacement->prev = replacement;
+        }
+        parent->child = replacement;
+    }
+    else
+    {   /*
+         * To find the last item in array quickly, we use prev in array.
+         * We can't modify the last item's next pointer where this item was the parent's child
+         */
+        if (replacement->prev != NULL)
+        {
+            replacement->prev->next = replacement;
+        }
+        if (replacement->next == NULL)
+        {
+            parent->child->prev = replacement;
+        }
+    }
+
+    item->next = NULL;
+    item->prev = NULL;
+    cJSON_Delete(item);
+
+    return true;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem)
+{
+    if (which < 0)
+    {
+        return false;
+    }
+
+    return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem);
+}
+
+static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive)
+{
+    if ((replacement == NULL) || (string == NULL))
+    {
+        return false;
+    }
+
+    /* replace the name in the replacement */
+    if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL))
+    {
+        cJSON_free(replacement->string);
+    }
+    replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
+    if (replacement->string == NULL)
+    {
+        return false;
+    }
+
+    replacement->type &= ~cJSON_StringIsConst;
+
+    return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem)
+{
+    return replace_item_in_object(object, string, newitem, false);
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem)
+{
+    return replace_item_in_object(object, string, newitem, true);
+}
+
+/* Create basic types: */
+CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_NULL;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_True;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_False;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = boolean ? cJSON_True : cJSON_False;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_Number;
+        item->valuedouble = num;
+
+        /* use saturation in case of overflow */
+        if (num >= INT_MAX)
+        {
+            item->valueint = INT_MAX;
+        }
+        else if (num <= (double)INT_MIN)
+        {
+            item->valueint = INT_MIN;
+        }
+        else
+        {
+            item->valueint = (int)num;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_String;
+        item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
+        if(!item->valuestring)
+        {
+            cJSON_Delete(item);
+            return NULL;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if (item != NULL)
+    {
+        item->type = cJSON_String | cJSON_IsReference;
+        item->valuestring = (char*)cast_away_const(string);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if (item != NULL) {
+        item->type = cJSON_Object | cJSON_IsReference;
+        item->child = (cJSON*)cast_away_const(child);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) {
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if (item != NULL) {
+        item->type = cJSON_Array | cJSON_IsReference;
+        item->child = (cJSON*)cast_away_const(child);
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type = cJSON_Raw;
+        item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks);
+        if(!item->valuestring)
+        {
+            cJSON_Delete(item);
+            return NULL;
+        }
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if(item)
+    {
+        item->type=cJSON_Array;
+    }
+
+    return item;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void)
+{
+    cJSON *item = cJSON_New_Item(&global_hooks);
+    if (item)
+    {
+        item->type = cJSON_Object;
+    }
+
+    return item;
+}
+
+/* Create Arrays: */
+CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count)
+{
+    size_t i = 0;
+    cJSON *n = NULL;
+    cJSON *p = NULL;
+    cJSON *a = NULL;
+
+    if ((count < 0) || (numbers == NULL))
+    {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++)
+    {
+        n = cJSON_CreateNumber(numbers[i]);
+        if (!n)
+        {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i)
+        {
+            a->child = n;
+        }
+        else
+        {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if (a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count)
+{
+    size_t i = 0;
+    cJSON *n = NULL;
+    cJSON *p = NULL;
+    cJSON *a = NULL;
+
+    if ((count < 0) || (numbers == NULL))
+    {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++)
+    {
+        n = cJSON_CreateNumber((double)numbers[i]);
+        if(!n)
+        {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i)
+        {
+            a->child = n;
+        }
+        else
+        {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if (a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count)
+{
+    size_t i = 0;
+    cJSON *n = NULL;
+    cJSON *p = NULL;
+    cJSON *a = NULL;
+
+    if ((count < 0) || (numbers == NULL))
+    {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for(i = 0; a && (i < (size_t)count); i++)
+    {
+        n = cJSON_CreateNumber(numbers[i]);
+        if(!n)
+        {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i)
+        {
+            a->child = n;
+        }
+        else
+        {
+            suffix_object(p, n);
+        }
+        p = n;
+    }
+
+    if (a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count)
+{
+    size_t i = 0;
+    cJSON *n = NULL;
+    cJSON *p = NULL;
+    cJSON *a = NULL;
+
+    if ((count < 0) || (strings == NULL))
+    {
+        return NULL;
+    }
+
+    a = cJSON_CreateArray();
+
+    for (i = 0; a && (i < (size_t)count); i++)
+    {
+        n = cJSON_CreateString(strings[i]);
+        if(!n)
+        {
+            cJSON_Delete(a);
+            return NULL;
+        }
+        if(!i)
+        {
+            a->child = n;
+        }
+        else
+        {
+            suffix_object(p,n);
+        }
+        p = n;
+    }
+
+    if (a && a->child) {
+        a->child->prev = n;
+    }
+
+    return a;
+}
+
+/* Duplication */
+cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse);
+
+CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse)
+{
+    return cJSON_Duplicate_rec(item, 0, recurse );
+}
+
+cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse)
+{
+    cJSON *newitem = NULL;
+    cJSON *child = NULL;
+    cJSON *next = NULL;
+    cJSON *newchild = NULL;
+
+    /* Bail on bad ptr */
+    if (!item)
+    {
+        goto fail;
+    }
+    /* Create new item */
+    newitem = cJSON_New_Item(&global_hooks);
+    if (!newitem)
+    {
+        goto fail;
+    }
+    /* Copy over all vars */
+    newitem->type = item->type & (~cJSON_IsReference);
+    newitem->valueint = item->valueint;
+    newitem->valuedouble = item->valuedouble;
+    if (item->valuestring)
+    {
+        newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks);
+        if (!newitem->valuestring)
+        {
+            goto fail;
+        }
+    }
+    if (item->string)
+    {
+        newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks);
+        if (!newitem->string)
+        {
+            goto fail;
+        }
+    }
+    /* If non-recursive, then we're done! */
+    if (!recurse)
+    {
+        return newitem;
+    }
+    /* Walk the ->next chain for the child. */
+    child = item->child;
+    while (child != NULL)
+    {
+        if(depth >= CJSON_CIRCULAR_LIMIT) {
+            goto fail;
+        }
+        newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */
+        if (!newchild)
+        {
+            goto fail;
+        }
+        if (next != NULL)
+        {
+            /* If newitem->child already set, then crosswire ->prev and ->next and move on */
+            next->next = newchild;
+            newchild->prev = next;
+            next = newchild;
+        }
+        else
+        {
+            /* Set newitem->child and move to it */
+            newitem->child = newchild;
+            next = newchild;
+        }
+        child = child->next;
+    }
+    if (newitem && newitem->child)
+    {
+        newitem->child->prev = newchild;
+    }
+
+    return newitem;
+
+fail:
+    if (newitem != NULL)
+    {
+        cJSON_Delete(newitem);
+    }
+
+    return NULL;
+}
+
+static void skip_oneline_comment(char **input)
+{
+    *input += static_strlen("//");
+
+    for (; (*input)[0] != '\0'; ++(*input))
+    {
+        if ((*input)[0] == '\n') {
+            *input += static_strlen("\n");
+            return;
+        }
+    }
+}
+
+static void skip_multiline_comment(char **input)
+{
+    *input += static_strlen("/*");
+
+    for (; (*input)[0] != '\0'; ++(*input))
+    {
+        if (((*input)[0] == '*') && ((*input)[1] == '/'))
+        {
+            *input += static_strlen("*/");
+            return;
+        }
+    }
+}
+
+static void minify_string(char **input, char **output) {
+    (*output)[0] = (*input)[0];
+    *input += static_strlen("\"");
+    *output += static_strlen("\"");
+
+
+    for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) {
+        (*output)[0] = (*input)[0];
+
+        if ((*input)[0] == '\"') {
+            (*output)[0] = '\"';
+            *input += static_strlen("\"");
+            *output += static_strlen("\"");
+            return;
+        } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) {
+            (*output)[1] = (*input)[1];
+            *input += static_strlen("\"");
+            *output += static_strlen("\"");
+        }
+    }
+}
+
+CJSON_PUBLIC(void) cJSON_Minify(char *json)
+{
+    char *into = json;
+
+    if (json == NULL)
+    {
+        return;
+    }
+
+    while (json[0] != '\0')
+    {
+        switch (json[0])
+        {
+            case ' ':
+            case '\t':
+            case '\r':
+            case '\n':
+                json++;
+                break;
+
+            case '/':
+                if (json[1] == '/')
+                {
+                    skip_oneline_comment(&json);
+                }
+                else if (json[1] == '*')
+                {
+                    skip_multiline_comment(&json);
+                } else {
+                    json++;
+                }
+                break;
+
+            case '\"':
+                minify_string(&json, (char**)&into);
+                break;
+
+            default:
+                into[0] = json[0];
+                json++;
+                into++;
+        }
+    }
+
+    /* and null-terminate. */
+    *into = '\0';
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Invalid;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_False;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xff) == cJSON_True;
+}
+
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & (cJSON_True | cJSON_False)) != 0;
+}
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_NULL;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Number;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_String;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Array;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Object;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item)
+{
+    if (item == NULL)
+    {
+        return false;
+    }
+
+    return (item->type & 0xFF) == cJSON_Raw;
+}
+
+CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive)
+{
+    if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF)))
+    {
+        return false;
+    }
+
+    /* check if type is valid */
+    switch (a->type & 0xFF)
+    {
+        case cJSON_False:
+        case cJSON_True:
+        case cJSON_NULL:
+        case cJSON_Number:
+        case cJSON_String:
+        case cJSON_Raw:
+        case cJSON_Array:
+        case cJSON_Object:
+            break;
+
+        default:
+            return false;
+    }
+
+    /* identical objects are equal */
+    if (a == b)
+    {
+        return true;
+    }
+
+    switch (a->type & 0xFF)
+    {
+        /* in these cases and equal type is enough */
+        case cJSON_False:
+        case cJSON_True:
+        case cJSON_NULL:
+            return true;
+
+        case cJSON_Number:
+            if (compare_double(a->valuedouble, b->valuedouble))
+            {
+                return true;
+            }
+            return false;
+
+        case cJSON_String:
+        case cJSON_Raw:
+            if ((a->valuestring == NULL) || (b->valuestring == NULL))
+            {
+                return false;
+            }
+            if (strcmp(a->valuestring, b->valuestring) == 0)
+            {
+                return true;
+            }
+
+            return false;
+
+        case cJSON_Array:
+        {
+            cJSON *a_element = a->child;
+            cJSON *b_element = b->child;
+
+            for (; (a_element != NULL) && (b_element != NULL);)
+            {
+                if (!cJSON_Compare(a_element, b_element, case_sensitive))
+                {
+                    return false;
+                }
+
+                a_element = a_element->next;
+                b_element = b_element->next;
+            }
+
+            /* one of the arrays is longer than the other */
+            if (a_element != b_element) {
+                return false;
+            }
+
+            return true;
+        }
+
+        case cJSON_Object:
+        {
+            cJSON *a_element = NULL;
+            cJSON *b_element = NULL;
+            cJSON_ArrayForEach(a_element, a)
+            {
+                /* TODO This has O(n^2) runtime, which is horrible! */
+                b_element = get_object_item(b, a_element->string, case_sensitive);
+                if (b_element == NULL)
+                {
+                    return false;
+                }
+
+                if (!cJSON_Compare(a_element, b_element, case_sensitive))
+                {
+                    return false;
+                }
+            }
+
+            /* doing this twice, once on a and b to prevent true comparison if a subset of b
+             * TODO: Do this the proper way, this is just a fix for now */
+            cJSON_ArrayForEach(b_element, b)
+            {
+                a_element = get_object_item(a, b_element->string, case_sensitive);
+                if (a_element == NULL)
+                {
+                    return false;
+                }
+
+                if (!cJSON_Compare(b_element, a_element, case_sensitive))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        default:
+            return false;
+    }
+}
+
+CJSON_PUBLIC(void *) cJSON_malloc(size_t size)
+{
+    return global_hooks.allocate(size);
+}
+
+CJSON_PUBLIC(void) cJSON_free(void *object)
+{
+    global_hooks.deallocate(object);
+    object = NULL;
+}

+ 306 - 0
third_party/cJSON.h

@@ -0,0 +1,306 @@
+/*
+  Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+#ifndef cJSON__h
+#define cJSON__h
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
+#define __WINDOWS__
+#endif
+
+#ifdef __WINDOWS__
+
+/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention.  For windows you have 3 define options:
+
+CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
+CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
+CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
+
+For *nix builds that support visibility attribute, you can define similar behavior by
+
+setting default visibility to hidden by adding
+-fvisibility=hidden (for gcc)
+or
+-xldscope=hidden (for sun cc)
+to CFLAGS
+
+then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
+
+*/
+
+#define CJSON_CDECL __cdecl
+#define CJSON_STDCALL __stdcall
+
+/* export symbols by default, this is necessary for copy pasting the C and header file */
+#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
+#define CJSON_EXPORT_SYMBOLS
+#endif
+
+#if defined(CJSON_HIDE_SYMBOLS)
+#define CJSON_PUBLIC(type)   type CJSON_STDCALL
+#elif defined(CJSON_EXPORT_SYMBOLS)
+#define CJSON_PUBLIC(type)   __declspec(dllexport) type CJSON_STDCALL
+#elif defined(CJSON_IMPORT_SYMBOLS)
+#define CJSON_PUBLIC(type)   __declspec(dllimport) type CJSON_STDCALL
+#endif
+#else /* !__WINDOWS__ */
+#define CJSON_CDECL
+#define CJSON_STDCALL
+
+#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
+#define CJSON_PUBLIC(type)   __attribute__((visibility("default"))) type
+#else
+#define CJSON_PUBLIC(type) type
+#endif
+#endif
+
+/* project version */
+#define CJSON_VERSION_MAJOR 1
+#define CJSON_VERSION_MINOR 7
+#define CJSON_VERSION_PATCH 19
+
+#include <stddef.h>
+
+/* cJSON Types: */
+#define cJSON_Invalid (0)
+#define cJSON_False  (1 << 0)
+#define cJSON_True   (1 << 1)
+#define cJSON_NULL   (1 << 2)
+#define cJSON_Number (1 << 3)
+#define cJSON_String (1 << 4)
+#define cJSON_Array  (1 << 5)
+#define cJSON_Object (1 << 6)
+#define cJSON_Raw    (1 << 7) /* raw json */
+
+#define cJSON_IsReference 256
+#define cJSON_StringIsConst 512
+
+/* The cJSON structure: */
+typedef struct cJSON
+{
+    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
+    struct cJSON *next;
+    struct cJSON *prev;
+    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
+    struct cJSON *child;
+
+    /* The type of the item, as above. */
+    int type;
+
+    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
+    char *valuestring;
+    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
+    int valueint;
+    /* The item's number, if type==cJSON_Number */
+    double valuedouble;
+
+    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
+    char *string;
+} cJSON;
+
+typedef struct cJSON_Hooks
+{
+      /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
+      void *(CJSON_CDECL *malloc_fn)(size_t sz);
+      void (CJSON_CDECL *free_fn)(void *ptr);
+} cJSON_Hooks;
+
+typedef int cJSON_bool;
+
+/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
+ * This is to prevent stack overflows. */
+#ifndef CJSON_NESTING_LIMIT
+#define CJSON_NESTING_LIMIT 1000
+#endif
+
+/* Limits the length of circular references can be before cJSON rejects to parse them.
+ * This is to prevent stack overflows. */
+#ifndef CJSON_CIRCULAR_LIMIT
+#define CJSON_CIRCULAR_LIMIT 10000
+#endif
+
+/* returns the version of cJSON as a string */
+CJSON_PUBLIC(const char*) cJSON_Version(void);
+
+/* Supply malloc, realloc and free functions to cJSON */
+CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
+
+/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
+/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
+CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
+/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
+/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
+CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
+
+/* Render a cJSON entity to text for transfer/storage. */
+CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
+/* Render a cJSON entity to text for transfer/storage without any formatting. */
+CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
+/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
+CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
+/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
+/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
+CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
+/* Delete a cJSON entity and all subentities. */
+CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
+
+/* Returns the number of items in an array (or object). */
+CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
+/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
+CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
+/* Get item "string" from object. Case insensitive. */
+CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
+CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
+CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
+/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
+CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
+
+/* Check item type and return its value */
+CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
+CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
+
+/* These functions check the type of an item */
+CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
+CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
+
+/* These calls create a cJSON item of the appropriate type. */
+CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
+CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
+CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
+CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
+CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
+CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
+/* raw json */
+CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
+CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
+CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
+
+/* Create a string where valuestring references a string so
+ * it will not be freed by cJSON_Delete */
+CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
+/* Create an object/array that only references it's elements so
+ * they will not be freed by cJSON_Delete */
+CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
+CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
+
+/* These utilities create an Array of count items.
+ * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
+CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
+CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
+CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
+CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
+
+/* Append item to the specified array/object. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
+/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
+ * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
+ * writing to `item->string` */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
+/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
+CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
+
+/* Remove/Detach items from Arrays/Objects. */
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
+CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
+CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
+
+/* Update array items. */
+CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
+CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
+
+/* Duplicate a cJSON item */
+CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
+/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
+ * need to be released. With recurse!=0, it will duplicate any children connected to the item.
+ * The item->next and ->prev pointers are always zero on return from Duplicate. */
+/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
+ * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
+CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
+
+/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
+ * The input pointer json cannot point to a read-only address area, such as a string constant, 
+ * but should point to a readable and writable address area. */
+CJSON_PUBLIC(void) cJSON_Minify(char *json);
+
+/* Helper functions for creating and adding items to an object at the same time.
+ * They return the added item or NULL on failure. */
+CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
+CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
+CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
+CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
+CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
+CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
+
+/* When assigning an integer value, it needs to be propagated to valuedouble too. */
+#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
+/* helper for the cJSON_SetNumberValue macro */
+CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
+#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
+/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
+CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
+
+/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
+#define cJSON_SetBoolValue(object, boolValue) ( \
+    (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
+    (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
+    cJSON_Invalid\
+)
+
+/* Macro for iterating over an array or object */
+#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
+
+/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
+CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
+CJSON_PUBLIC(void) cJSON_free(void *object);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

+ 180 - 0
third_party/cvxFont.cpp

@@ -0,0 +1,180 @@
+#include "cvxFont.h"
+#include <cassert>
+#include <clocale>
+#include <utility>
+#include <sstream>
+#include <cstdlib>
+
+cvx::CvxFont::CvxFont(const cv::String& fontType)
+{
+    assert(!fontType.empty());
+    m_error = FT_Init_FreeType(&m_library);
+    if (m_error){
+        std::cerr << "library initial error!" << std::endl;
+        return;
+    }
+    m_error = FT_New_Face(m_library, fontType.c_str(), 0, &m_face);
+    if (m_error == FT_Err_Unknown_File_Format){
+        std::cerr << "unsupported font format!" << std::endl;
+        return;
+    }
+    else if (m_error){
+        std::cerr << " can not open font files" << std::endl;
+        return;
+    }
+    // use default parameters
+    m_font = new FontProperty;
+    initFont();
+    setlocale(LC_ALL, "");
+}
+
+// release freetype resource
+cvx::CvxFont::~CvxFont()
+{
+    delete m_font;
+    FT_Done_Face(m_face);
+    FT_Done_FreeType(m_library);
+}
+
+void cvx::CvxFont::setFontSize(const int fontSize)
+{
+    m_font->fontSize = fontSize;
+    FT_Set_Pixel_Sizes(m_face, fontSize, 0);
+}
+
+// initial font
+void cvx::CvxFont::initFont()
+{
+    setFontSize(16);
+    setSpaceRatio(0.5);
+    setFontRatio(0);
+    setRotateAngle(0);
+    setDiaphaneity(1);
+    setUnderline(false);
+    setVertical(false);
+    // set font
+    FT_Set_Pixel_Sizes(m_face, getFontSize(), 0);
+}
+
+void cvx::CvxFont::rotateFont(double angle) {
+    angle = (angle / 360) * 3.14159 * 2;
+    /* set up matrix */
+    m_matrix.xx = static_cast<FT_Fixed>(cos(angle) * 0x10000L);
+    m_matrix.xy = static_cast<FT_Fixed>(-sin(angle) * 0x10000L);
+    m_matrix.yx = static_cast<FT_Fixed>(sin(angle) * 0x10000L);
+    m_matrix.yy = static_cast<FT_Fixed>(cos(angle) * 0x10000L);
+
+    FT_Set_Transform(m_face, &m_matrix, nullptr);
+}
+
+void cvx::CvxFont::putTextStr(cv::Mat& img, const cv::String& text, cv::Point pos, const cv::Scalar& color)
+{
+    CV_Assert(!img.empty());
+    CV_Assert(!text.empty());
+
+    int xStart = pos.x;
+    int yStart = pos.y;
+    m_maxDiffHeight = 0;
+
+    const char* ptr = text.c_str();
+    std::mbtowc(nullptr, nullptr, 0); // reset the conversion state
+    const char* end = ptr + std::strlen(ptr);
+    int ret;
+    for (wchar_t wc; (ret = std::mbtowc(&wc, ptr, end - ptr)) > 0; ptr += ret) {
+        putWChar(img, (wc & 0xffffffff), pos, color);
+    }
+
+    int xEnd = pos.x;
+    int yEnd = pos.y;
+    if (getUnderline()) {
+        if (getVertical()) {
+            cv::line(img, cv::Point(xStart + m_maxDiffHeight, yStart), cv::Point(xStart + m_maxDiffHeight, yEnd), color, 2);
+        }
+        else {
+            cv::line(img, cv::Point(xStart, yStart + m_maxDiffHeight), cv::Point(xEnd, yStart + m_maxDiffHeight), color, 2);
+        }
+    }
+
+}
+
+void cvx::CvxFont::putWChar(cv::Mat& img, uint32_t wc, cv::Point& pos, const cv::Scalar& color)
+{
+    rotateFont(getAngle());
+    const auto vertical = getVertical();
+    const auto size = getFontSize();
+
+    // Converting a Character Code Into a Glyph Index
+    FT_UInt glyph_index = FT_Get_Char_Index(m_face, wc);
+    FT_Load_Glyph(m_face, glyph_index, FT_LOAD_DEFAULT);
+    FT_Render_Glyph(m_face->glyph, FT_RENDER_MODE_MONO);
+
+    FT_GlyphSlot slot = m_face->glyph;
+    FT_Bitmap bitmap = slot->bitmap;
+    bool isSpace = wc == ' ';
+
+    // get rows and cols of current wide char
+    auto rows = bitmap.rows;
+    auto cols = bitmap.width;
+
+    cv::Point gPos = pos;
+    //gPos.y += m_font->fontSize;
+    if (vertical)
+    {
+        gPos.x += (slot->metrics.vertBearingX >> 6);
+        gPos.y += (slot->metrics.vertBearingY >> 6);
+        m_maxDiffHeight = std::max(m_maxDiffHeight, rows - (slot->metrics.vertBearingY >> 6));
+    }
+    else
+    {
+        gPos.x += (slot->metrics.horiBearingX >> 6);
+        gPos.y -= (slot->metrics.horiBearingY >> 6);
+        m_maxDiffHeight = std::max(m_maxDiffHeight, rows - (slot->metrics.horiBearingY >> 6));
+    }
+
+    // https://stackoverflow.com/questions/52254639/how-to-access-pixels-state-in-monochrome-bitmap-using-freetype2
+    for (auto i = 0; i < rows; ++i)
+    {
+        for (auto j = 0; j < cols; ++j)
+        {
+            int off = i * slot->bitmap.pitch + j / 8;
+
+            if (slot->bitmap.buffer[off] & (0x80 >> (j % 8)))
+            {
+                const auto r = gPos.y + i; //vertical ? pos.y + i : pos.y + i + (size - rows); // to make align to bottom
+                const auto c = gPos.x + j;
+
+                if (r >= 0 && r < img.rows && c >= 0 && c < img.cols)
+                {
+                    cv::Vec3b scalar = img.at<cv::Vec3b>(cv::Point(c, r));
+
+                    // merge set color with origin color
+                    double p = getDiaphaneity();
+                    for (int k = 0; k < 3; ++k)
+                    {
+                        scalar.val[k] = static_cast<uchar>(scalar.val[k] * (1 - p) + color.val[k] * p);
+                    }
+
+                    img.at<cv::Vec3b>(cv::Point(c, r)) = cv::Vec3b(scalar[0], scalar[1], scalar[2]);
+                }
+            }
+        }
+    }
+    // modify position to next character
+    const auto space = static_cast<int>(size * getSpaceRatio());
+    const auto sep = static_cast<int>(size * getFontRatio());
+    // vertical string or not, default not vertical
+    if (vertical){
+        const auto moveX = (static_cast<int>(getAngle()) == 0) ?  (slot->metrics.vertAdvance >> 6) : rows + 1;
+        pos.y += isSpace ? space : moveX + sep;
+    }else{
+        const auto moveY = (static_cast<int>(getAngle()) == 0) ? (slot->metrics.horiAdvance >> 6) : cols + 1;
+        pos.x += isSpace ? space : moveY + sep;
+    }
+}
+
+void cvx::putText(cv::Mat& img, const std::string& text, cv::Point pos, cvx::CvxFont& fontFace, int fontSize, const cv::Scalar& color) {
+    fontFace.setFontSize(fontSize);
+    fontFace.putTextStr(img, text, std::move(pos), color);
+    fontFace.initFont();
+}
+

+ 71 - 0
third_party/cvxFont.h

@@ -0,0 +1,71 @@
+#ifndef OPENCVUNICODE_CVXFONT_H
+#define OPENCVUNICODE_CVXFONT_H
+
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include <opencv2/opencv.hpp>
+
+#include <typeinfo>
+#include <codecvt>
+#include <string>
+#include <locale>
+
+namespace cvx {
+    struct FontProperty {
+        int fontSize;           // font size (pixel)
+        double spaceRatio;       // ratio of distance when meet a space, base on font size
+        double fontRatio;        // ratio of distance between each character, base on font size
+        double fontRotateAngle;  // rotate angle
+        double fontDiaphaneity;  // merge ratio
+        bool fontIsUnderline;   // underline
+        bool fontIsVertical;    // put text in vertical
+    };
+
+    class CvxFont
+    {
+    public:
+
+        explicit CvxFont(const cv::String& fontType);
+        virtual ~CvxFont();
+
+        void setFontSize(int fontSize);
+        void setSpaceRatio(const double spaceRatio) { m_font->spaceRatio = spaceRatio; }
+        void setFontRatio(const double fontRatio) { m_font->fontRatio = fontRatio; }
+        void setRotateAngle(const double angle) { m_font->fontRotateAngle = angle; }
+        void setUnderline(const bool isUnderline) { m_font->fontIsUnderline = isUnderline; }
+        void setDiaphaneity(const double diaphaneity) { m_font->fontDiaphaneity = diaphaneity; }
+        void setVertical(const bool vertical) { m_font->fontIsVertical = vertical; }
+
+        [[nodiscard]] int getFontSize() const { return m_font->fontSize; }
+        [[nodiscard]] double getSpaceRatio() const { return m_font->spaceRatio; }
+        [[nodiscard]] double getFontRatio() const { return m_font->fontRatio; }
+        [[nodiscard]] double getAngle() const { return m_font->fontRotateAngle; }
+        [[nodiscard]] bool getUnderline() const { return m_font->fontIsUnderline; }
+        [[nodiscard]] double getDiaphaneity() const { return m_font->fontDiaphaneity; }
+        [[nodiscard]] bool getVertical() const { return m_font->fontIsVertical; }
+
+    private:
+        void initFont();
+        void rotateFont(double angle);
+        void putTextStr(cv::Mat& img, const cv::String& text, cv::Point pos, const cv::Scalar& color);
+        void putWChar(cv::Mat& img, uint32_t wc, cv::Point& pos, const cv::Scalar& color);
+        friend void putText(cv::Mat&, const std::string&, cv::Point, cvx::CvxFont&, int, const cv::Scalar&);
+        FT_Library   m_library{};   // font library
+        FT_Face      m_face{};      // font type
+        FT_Matrix    m_matrix{};
+        FT_Vector    m_pen{};
+        FT_Error     m_error;
+
+        FontProperty* m_font;
+        long m_maxDiffHeight{ 0 };
+
+    };
+
+    void putText(cv::Mat& img, const std::string& text, cv::Point pos, cvx::CvxFont& fontFace, int fontSize, const cv::Scalar& color);
+    void putSymbols(cv::Mat& img, std::vector<uint32_t>& symbols, cv::Point pos, cvx::CvxFont& fontFace, int fontSize, const cv::Scalar& color);
+    void putOneSymbol(cv::Mat& img, uint32_t symbol, cv::Point pos, cvx::CvxFont& fontFace, int fontSize, const cv::Scalar& color);
+}
+
+#endif //OPENCVUNICODE_CVXFONT_H
+
+

+ 20091 - 0
third_party/httplib.h

@@ -0,0 +1,20091 @@
+//
+//  httplib.h
+//
+//  Copyright (c) 2026 Yuji Hirose. All rights reserved.
+//  MIT License
+//
+
+#ifndef CPPHTTPLIB_HTTPLIB_H
+#define CPPHTTPLIB_HTTPLIB_H
+
+#define CPPHTTPLIB_VERSION "0.42.0"
+#define CPPHTTPLIB_VERSION_NUM "0x002a00"
+
+#ifdef _WIN32
+#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
+#error                                                                         \
+    "cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later."
+#endif
+#endif
+
+/*
+ * Configuration
+ */
+
+#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND
+#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND
+#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND 10000
+#endif
+
+#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT
+#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 100
+#endif
+
+#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND
+#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300
+#endif
+
+#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND
+#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND
+#define CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND
+#define CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND
+#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND
+#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND
+#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND 300
+#endif
+
+#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND
+#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND
+#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND
+#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND
+#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD
+#define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024
+#endif
+
+#ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND
+#define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000
+#endif
+
+#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD
+#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024)
+#endif
+
+#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND
+#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50
+#endif
+
+#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND
+#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0
+#endif
+
+#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND
+#ifdef _WIN32
+#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000
+#else
+#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0
+#endif
+#endif
+
+#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH
+#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192
+#endif
+
+#ifndef CPPHTTPLIB_HEADER_MAX_LENGTH
+#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192
+#endif
+
+#ifndef CPPHTTPLIB_HEADER_MAX_COUNT
+#define CPPHTTPLIB_HEADER_MAX_COUNT 100
+#endif
+
+#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT
+#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20
+#endif
+
+#ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT
+#define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024
+#endif
+
+#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH
+#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB
+#endif
+
+#ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH
+#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192
+#endif
+
+#ifndef CPPHTTPLIB_RANGE_MAX_COUNT
+#define CPPHTTPLIB_RANGE_MAX_COUNT 1024
+#endif
+
+#ifndef CPPHTTPLIB_TCP_NODELAY
+#define CPPHTTPLIB_TCP_NODELAY false
+#endif
+
+#ifndef CPPHTTPLIB_IPV6_V6ONLY
+#define CPPHTTPLIB_IPV6_V6ONLY false
+#endif
+
+#ifndef CPPHTTPLIB_RECV_BUFSIZ
+#define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u)
+#endif
+
+#ifndef CPPHTTPLIB_SEND_BUFSIZ
+#define CPPHTTPLIB_SEND_BUFSIZ size_t(16384u)
+#endif
+
+#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ
+#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u)
+#endif
+
+#ifndef CPPHTTPLIB_THREAD_POOL_COUNT
+#define CPPHTTPLIB_THREAD_POOL_COUNT                                           \
+  ((std::max)(8u, std::thread::hardware_concurrency() > 0                      \
+                      ? std::thread::hardware_concurrency() - 1                \
+                      : 0))
+#endif
+
+#ifndef CPPHTTPLIB_THREAD_POOL_MAX_COUNT
+#define CPPHTTPLIB_THREAD_POOL_MAX_COUNT (CPPHTTPLIB_THREAD_POOL_COUNT * 4)
+#endif
+
+#ifndef CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT
+#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 3 // seconds
+#endif
+
+#ifndef CPPHTTPLIB_RECV_FLAGS
+#define CPPHTTPLIB_RECV_FLAGS 0
+#endif
+
+#ifndef CPPHTTPLIB_SEND_FLAGS
+#define CPPHTTPLIB_SEND_FLAGS 0
+#endif
+
+#ifndef CPPHTTPLIB_LISTEN_BACKLOG
+#define CPPHTTPLIB_LISTEN_BACKLOG 5
+#endif
+
+#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
+#define CPPHTTPLIB_MAX_LINE_LENGTH 32768
+#endif
+
+#ifndef CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH
+#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216
+#endif
+
+#ifndef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
+#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 300
+#endif
+
+#ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND
+#define CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND 5
+#endif
+
+#ifndef CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND
+#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 30
+#endif
+
+#ifndef CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS
+#define CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS 0
+#endif
+
+/*
+ * Headers
+ */
+
+#ifdef _WIN32
+#ifndef _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_WARNINGS
+#endif //_CRT_SECURE_NO_WARNINGS
+
+#ifndef _CRT_NONSTDC_NO_DEPRECATE
+#define _CRT_NONSTDC_NO_DEPRECATE
+#endif //_CRT_NONSTDC_NO_DEPRECATE
+
+#if defined(_MSC_VER)
+#if _MSC_VER < 1900
+#error Sorry, Visual Studio versions prior to 2015 are not supported
+#endif
+
+#pragma comment(lib, "ws2_32.lib")
+
+#ifndef _SSIZE_T_DEFINED
+using ssize_t = __int64;
+#define _SSIZE_T_DEFINED
+#endif
+#endif // _MSC_VER
+
+#ifndef S_ISREG
+#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG)
+#endif // S_ISREG
+
+#ifndef S_ISDIR
+#define S_ISDIR(m) (((m) & S_IFDIR) == S_IFDIR)
+#endif // S_ISDIR
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif // NOMINMAX
+
+#include <io.h>
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+#if defined(__has_include)
+#if __has_include(<afunix.h>)
+// afunix.h uses types declared in winsock2.h, so has to be included after it.
+#include <afunix.h>
+#define CPPHTTPLIB_HAVE_AFUNIX_H 1
+#endif
+#endif
+
+#ifndef WSA_FLAG_NO_HANDLE_INHERIT
+#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
+#endif
+
+using nfds_t = unsigned long;
+using socket_t = SOCKET;
+using socklen_t = int;
+
+#else // not _WIN32
+
+#include <arpa/inet.h>
+#if !defined(_AIX) && !defined(__MVS__)
+#include <ifaddrs.h>
+#endif
+#ifdef __MVS__
+#include <strings.h>
+#ifndef NI_MAXHOST
+#define NI_MAXHOST 1025
+#endif
+#endif
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#ifdef __linux__
+#include <resolv.h>
+#undef _res // Undefine _res macro to avoid conflicts with user code (#2278)
+#endif
+#include <csignal>
+#include <netinet/tcp.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+using socket_t = int;
+#ifndef INVALID_SOCKET
+#define INVALID_SOCKET (-1)
+#endif
+#endif //_WIN32
+
+#if defined(__APPLE__)
+#include <TargetConditionals.h>
+#endif
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <cctype>
+#include <chrono>
+#include <climits>
+#include <condition_variable>
+#include <cstdlib>
+#include <cstring>
+#include <errno.h>
+#include <exception>
+#include <fcntl.h>
+#include <fstream>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <list>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <random>
+#include <regex>
+#include <set>
+#include <sstream>
+#include <string>
+#include <sys/stat.h>
+#include <system_error>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+
+// On macOS with a TLS backend, enable Keychain root certificates by default
+// unless the user explicitly opts out.
+#if defined(__APPLE__) && defined(__clang__) &&                                \
+    !defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) &&         \
+    (defined(CPPHTTPLIB_OPENSSL_SUPPORT) ||                                    \
+     defined(CPPHTTPLIB_MBEDTLS_SUPPORT) ||                                    \
+     defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
+#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+#endif
+#endif
+
+// On Windows, enable Schannel certificate verification by default
+// unless the user explicitly opts out.
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+#define CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+#endif
+
+#if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) ||                        \
+    defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
+#if TARGET_OS_MAC && defined(__clang__)
+#include <CFNetwork/CFHost.h>
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+#endif
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+#ifdef _WIN32
+#include <wincrypt.h>
+
+// these are defined in wincrypt.h and it breaks compilation if BoringSSL is
+// used
+#undef X509_NAME
+#undef X509_CERT_PAIR
+#undef X509_EXTENSIONS
+#undef PKCS7_SIGNER_INFO
+
+#ifdef _MSC_VER
+#pragma comment(lib, "crypt32.lib")
+#endif
+#endif // _WIN32
+
+#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+#if TARGET_OS_MAC
+#include <Security/Security.h>
+#endif
+#endif
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/ssl.h>
+#include <openssl/x509v3.h>
+
+#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK)
+#include <openssl/applink.c>
+#endif
+
+#include <iostream>
+#include <sstream>
+
+#if defined(OPENSSL_IS_BORINGSSL) || defined(LIBRESSL_VERSION_NUMBER)
+#if OPENSSL_VERSION_NUMBER < 0x1010107f
+#error Please use OpenSSL or a current version of BoringSSL
+#endif
+#define SSL_get1_peer_certificate SSL_get_peer_certificate
+#elif OPENSSL_VERSION_NUMBER < 0x30000000L
+#error Sorry, OpenSSL versions prior to 3.0.0 are not supported
+#endif
+
+#endif // CPPHTTPLIB_OPENSSL_SUPPORT
+
+#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
+#include <mbedtls/ctr_drbg.h>
+#include <mbedtls/entropy.h>
+#include <mbedtls/error.h>
+#include <mbedtls/md5.h>
+#include <mbedtls/net_sockets.h>
+#include <mbedtls/oid.h>
+#include <mbedtls/pk.h>
+#include <mbedtls/sha1.h>
+#include <mbedtls/sha256.h>
+#include <mbedtls/sha512.h>
+#include <mbedtls/ssl.h>
+#include <mbedtls/x509_crt.h>
+#ifdef _WIN32
+#include <wincrypt.h>
+#ifdef _MSC_VER
+#pragma comment(lib, "crypt32.lib")
+#endif
+#endif // _WIN32
+#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+#if TARGET_OS_MAC
+#include <Security/Security.h>
+#endif
+#endif
+
+// Mbed TLS 3.x API compatibility
+#if MBEDTLS_VERSION_MAJOR >= 3
+#define CPPHTTPLIB_MBEDTLS_V3
+#endif
+
+#endif // CPPHTTPLIB_MBEDTLS_SUPPORT
+
+#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT
+#include <wolfssl/options.h>
+
+#include <wolfssl/openssl/x509v3.h>
+
+// Fallback definitions for older wolfSSL versions (e.g., 5.6.6)
+#ifndef WOLFSSL_GEN_EMAIL
+#define WOLFSSL_GEN_EMAIL 1
+#endif
+#ifndef WOLFSSL_GEN_DNS
+#define WOLFSSL_GEN_DNS 2
+#endif
+#ifndef WOLFSSL_GEN_URI
+#define WOLFSSL_GEN_URI 6
+#endif
+#ifndef WOLFSSL_GEN_IPADD
+#define WOLFSSL_GEN_IPADD 7
+#endif
+
+#include <wolfssl/ssl.h>
+#include <wolfssl/wolfcrypt/hash.h>
+#include <wolfssl/wolfcrypt/md5.h>
+#include <wolfssl/wolfcrypt/sha256.h>
+#include <wolfssl/wolfcrypt/sha512.h>
+#ifdef _WIN32
+#include <wincrypt.h>
+#ifdef _MSC_VER
+#pragma comment(lib, "crypt32.lib")
+#endif
+#endif // _WIN32
+#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+#if TARGET_OS_MAC
+#include <Security/Security.h>
+#endif
+#endif
+#endif // CPPHTTPLIB_WOLFSSL_SUPPORT
+
+// Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available
+#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) ||                                     \
+    defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
+#define CPPHTTPLIB_SSL_ENABLED
+#endif
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+#include <zlib.h>
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+#include <brotli/decode.h>
+#include <brotli/encode.h>
+#endif
+
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+#include <zstd.h>
+#endif
+
+/*
+ * Declaration
+ */
+namespace httplib {
+
+namespace ws {
+class WebSocket;
+} // namespace ws
+
+namespace detail {
+
+/*
+ * Backport std::make_unique from C++14.
+ *
+ * NOTE: This code came up with the following stackoverflow post:
+ * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique
+ *
+ */
+
+template <class T, class... Args>
+typename std::enable_if<!std::is_array<T>::value, std::unique_ptr<T>>::type
+make_unique(Args &&...args) {
+  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
+
+template <class T>
+typename std::enable_if<std::is_array<T>::value, std::unique_ptr<T>>::type
+make_unique(std::size_t n) {
+  typedef typename std::remove_extent<T>::type RT;
+  return std::unique_ptr<T>(new RT[n]);
+}
+
+namespace case_ignore {
+
+inline unsigned char to_lower(int c) {
+  const static unsigned char table[256] = {
+      0,   1,   2,   3,   4,   5,   6,   7,   8,   9,   10,  11,  12,  13,  14,
+      15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,
+      30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,  42,  43,  44,
+      45,  46,  47,  48,  49,  50,  51,  52,  53,  54,  55,  56,  57,  58,  59,
+      60,  61,  62,  63,  64,  97,  98,  99,  100, 101, 102, 103, 104, 105, 106,
+      107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
+      122, 91,  92,  93,  94,  95,  96,  97,  98,  99,  100, 101, 102, 103, 104,
+      105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
+      120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+      135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
+      150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
+      165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
+      180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226,
+      227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241,
+      242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224,
+      225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+      240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
+      255,
+  };
+  return table[(unsigned char)(char)c];
+}
+
+inline std::string to_lower(const std::string &s) {
+  std::string result = s;
+  std::transform(
+      result.begin(), result.end(), result.begin(),
+      [](unsigned char c) { return static_cast<char>(to_lower(c)); });
+  return result;
+}
+
+inline bool equal(const std::string &a, const std::string &b) {
+  return a.size() == b.size() &&
+         std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) {
+           return to_lower(ca) == to_lower(cb);
+         });
+}
+
+struct equal_to {
+  bool operator()(const std::string &a, const std::string &b) const {
+    return equal(a, b);
+  }
+};
+
+struct hash {
+  size_t operator()(const std::string &key) const {
+    return hash_core(key.data(), key.size(), 0);
+  }
+
+  size_t hash_core(const char *s, size_t l, size_t h) const {
+    return (l == 0) ? h
+                    : hash_core(s + 1, l - 1,
+                                // Unsets the 6 high bits of h, therefore no
+                                // overflow happens
+                                (((std::numeric_limits<size_t>::max)() >> 6) &
+                                 h * 33) ^
+                                    static_cast<unsigned char>(to_lower(*s)));
+  }
+};
+
+template <typename T>
+using unordered_set = std::unordered_set<T, detail::case_ignore::hash,
+                                         detail::case_ignore::equal_to>;
+
+} // namespace case_ignore
+
+// This is based on
+// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189".
+
+struct scope_exit {
+  explicit scope_exit(std::function<void(void)> &&f)
+      : exit_function(std::move(f)), execute_on_destruction{true} {}
+
+  scope_exit(scope_exit &&rhs) noexcept
+      : exit_function(std::move(rhs.exit_function)),
+        execute_on_destruction{rhs.execute_on_destruction} {
+    rhs.release();
+  }
+
+  ~scope_exit() {
+    if (execute_on_destruction) { this->exit_function(); }
+  }
+
+  void release() { this->execute_on_destruction = false; }
+
+private:
+  scope_exit(const scope_exit &) = delete;
+  void operator=(const scope_exit &) = delete;
+  scope_exit &operator=(scope_exit &&) = delete;
+
+  std::function<void(void)> exit_function;
+  bool execute_on_destruction;
+};
+
+// Simple from_chars implementation for integer and double types (C++17
+// substitute)
+template <typename T> struct from_chars_result {
+  const char *ptr;
+  std::errc ec;
+};
+
+template <typename T>
+inline from_chars_result<T> from_chars(const char *first, const char *last,
+                                       T &value, int base = 10) {
+  value = 0;
+  const char *p = first;
+  bool negative = false;
+
+  if (p != last && *p == '-') {
+    negative = true;
+    ++p;
+  }
+  if (p == last) { return {first, std::errc::invalid_argument}; }
+
+  T result = 0;
+  for (; p != last; ++p) {
+    char c = *p;
+    int digit = -1;
+    if ('0' <= c && c <= '9') {
+      digit = c - '0';
+    } else if ('a' <= c && c <= 'z') {
+      digit = c - 'a' + 10;
+    } else if ('A' <= c && c <= 'Z') {
+      digit = c - 'A' + 10;
+    } else {
+      break;
+    }
+
+    if (digit < 0 || digit >= base) { break; }
+    if (result > ((std::numeric_limits<T>::max)() - digit) / base) {
+      return {p, std::errc::result_out_of_range};
+    }
+    result = result * base + digit;
+  }
+
+  if (p == first || (negative && p == first + 1)) {
+    return {first, std::errc::invalid_argument};
+  }
+
+  value = negative ? -result : result;
+  return {p, std::errc{}};
+}
+
+// from_chars for double (simple wrapper for strtod)
+inline from_chars_result<double> from_chars(const char *first, const char *last,
+                                            double &value) {
+  std::string s(first, last);
+  char *endptr = nullptr;
+  errno = 0;
+  value = std::strtod(s.c_str(), &endptr);
+  if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; }
+  if (errno == ERANGE) {
+    return {first + (endptr - s.c_str()), std::errc::result_out_of_range};
+  }
+  return {first + (endptr - s.c_str()), std::errc{}};
+}
+
+inline bool parse_port(const char *s, size_t len, int &port) {
+  int val = 0;
+  auto r = from_chars(s, s + len, val);
+  if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
+  port = val;
+  return true;
+}
+
+inline bool parse_port(const std::string &s, int &port) {
+  return parse_port(s.data(), s.size(), port);
+}
+
+struct UrlComponents {
+  std::string scheme;
+  std::string host;
+  std::string port;
+  std::string path;
+  std::string query;
+};
+
+inline bool parse_url(const std::string &url, UrlComponents &uc) {
+  uc = {};
+  size_t pos = 0;
+
+  auto sep = url.find("://");
+  if (sep != std::string::npos) {
+    uc.scheme = url.substr(0, sep);
+
+    // Scheme must be [a-z]+ only
+    if (uc.scheme.empty()) { return false; }
+    for (auto c : uc.scheme) {
+      if (c < 'a' || c > 'z') { return false; }
+    }
+
+    pos = sep + 3;
+  } else if (url.compare(0, 2, "//") == 0) {
+    pos = 2;
+  }
+
+  auto has_authority_prefix = pos > 0;
+  auto has_authority = has_authority_prefix || (!url.empty() && url[0] != '/' &&
+                                                url[0] != '?' && url[0] != '#');
+  if (has_authority) {
+    if (pos < url.size() && url[pos] == '[') {
+      auto close = url.find(']', pos);
+      if (close == std::string::npos) { return false; }
+      uc.host = url.substr(pos + 1, close - pos - 1);
+
+      // IPv6 host must be [a-fA-F0-9:]+ only
+      if (uc.host.empty()) { return false; }
+      for (auto c : uc.host) {
+        if (!((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') ||
+              (c >= '0' && c <= '9') || c == ':')) {
+          return false;
+        }
+      }
+
+      pos = close + 1;
+    } else {
+      auto end = url.find_first_of(":/?#", pos);
+      if (end == std::string::npos) { end = url.size(); }
+      uc.host = url.substr(pos, end - pos);
+      pos = end;
+    }
+
+    if (pos < url.size() && url[pos] == ':') {
+      ++pos;
+      auto end = url.find_first_of("/?#", pos);
+      if (end == std::string::npos) { end = url.size(); }
+      uc.port = url.substr(pos, end - pos);
+      pos = end;
+    }
+
+    // Without :// or //, the entire input must be consumed as host[:port].
+    // If there is leftover (path, query, etc.), this is not a valid
+    // host[:port] string — clear and reparse as a plain path.
+    if (!has_authority_prefix && pos < url.size()) {
+      uc.host.clear();
+      uc.port.clear();
+      pos = 0;
+    }
+  }
+
+  if (pos < url.size() && url[pos] != '?' && url[pos] != '#') {
+    auto end = url.find_first_of("?#", pos);
+    if (end == std::string::npos) { end = url.size(); }
+    uc.path = url.substr(pos, end - pos);
+    pos = end;
+  }
+
+  if (pos < url.size() && url[pos] == '?') {
+    auto end = url.find('#', pos);
+    if (end == std::string::npos) { end = url.size(); }
+    uc.query = url.substr(pos, end - pos);
+  }
+
+  return true;
+}
+
+} // namespace detail
+
+enum class SSLVerifierResponse {
+  // no decision has been made, use the built-in certificate verifier
+  NoDecisionMade,
+  // connection certificate is verified and accepted
+  CertificateAccepted,
+  // connection certificate was processed but is rejected
+  CertificateRejected
+};
+
+enum StatusCode {
+  // Information responses
+  Continue_100 = 100,
+  SwitchingProtocol_101 = 101,
+  Processing_102 = 102,
+  EarlyHints_103 = 103,
+
+  // Successful responses
+  OK_200 = 200,
+  Created_201 = 201,
+  Accepted_202 = 202,
+  NonAuthoritativeInformation_203 = 203,
+  NoContent_204 = 204,
+  ResetContent_205 = 205,
+  PartialContent_206 = 206,
+  MultiStatus_207 = 207,
+  AlreadyReported_208 = 208,
+  IMUsed_226 = 226,
+
+  // Redirection messages
+  MultipleChoices_300 = 300,
+  MovedPermanently_301 = 301,
+  Found_302 = 302,
+  SeeOther_303 = 303,
+  NotModified_304 = 304,
+  UseProxy_305 = 305,
+  unused_306 = 306,
+  TemporaryRedirect_307 = 307,
+  PermanentRedirect_308 = 308,
+
+  // Client error responses
+  BadRequest_400 = 400,
+  Unauthorized_401 = 401,
+  PaymentRequired_402 = 402,
+  Forbidden_403 = 403,
+  NotFound_404 = 404,
+  MethodNotAllowed_405 = 405,
+  NotAcceptable_406 = 406,
+  ProxyAuthenticationRequired_407 = 407,
+  RequestTimeout_408 = 408,
+  Conflict_409 = 409,
+  Gone_410 = 410,
+  LengthRequired_411 = 411,
+  PreconditionFailed_412 = 412,
+  PayloadTooLarge_413 = 413,
+  UriTooLong_414 = 414,
+  UnsupportedMediaType_415 = 415,
+  RangeNotSatisfiable_416 = 416,
+  ExpectationFailed_417 = 417,
+  ImATeapot_418 = 418,
+  MisdirectedRequest_421 = 421,
+  UnprocessableContent_422 = 422,
+  Locked_423 = 423,
+  FailedDependency_424 = 424,
+  TooEarly_425 = 425,
+  UpgradeRequired_426 = 426,
+  PreconditionRequired_428 = 428,
+  TooManyRequests_429 = 429,
+  RequestHeaderFieldsTooLarge_431 = 431,
+  UnavailableForLegalReasons_451 = 451,
+
+  // Server error responses
+  InternalServerError_500 = 500,
+  NotImplemented_501 = 501,
+  BadGateway_502 = 502,
+  ServiceUnavailable_503 = 503,
+  GatewayTimeout_504 = 504,
+  HttpVersionNotSupported_505 = 505,
+  VariantAlsoNegotiates_506 = 506,
+  InsufficientStorage_507 = 507,
+  LoopDetected_508 = 508,
+  NotExtended_510 = 510,
+  NetworkAuthenticationRequired_511 = 511,
+};
+
+using Headers =
+    std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
+                            detail::case_ignore::equal_to>;
+
+using Params = std::multimap<std::string, std::string>;
+using Match = std::smatch;
+
+using DownloadProgress = std::function<bool(size_t current, size_t total)>;
+using UploadProgress = std::function<bool(size_t current, size_t total)>;
+
+/*
+ * detail: type-erased storage used by UserData.
+ * ABI-stable regardless of C++ standard — always uses this custom
+ * implementation instead of std::any.
+ */
+namespace detail {
+
+using any_type_id = const void *;
+
+template <typename T> any_type_id any_typeid() noexcept {
+  static const char id = 0;
+  return &id;
+}
+
+struct any_storage {
+  virtual ~any_storage() = default;
+  virtual std::unique_ptr<any_storage> clone() const = 0;
+  virtual any_type_id type_id() const noexcept = 0;
+};
+
+template <typename T> struct any_value final : any_storage {
+  T value;
+  template <typename U> explicit any_value(U &&v) : value(std::forward<U>(v)) {}
+  std::unique_ptr<any_storage> clone() const override {
+    return std::unique_ptr<any_storage>(new any_value<T>(value));
+  }
+  any_type_id type_id() const noexcept override { return any_typeid<T>(); }
+};
+
+} // namespace detail
+
+class UserData {
+public:
+  UserData() = default;
+  UserData(UserData &&) noexcept = default;
+  UserData &operator=(UserData &&) noexcept = default;
+
+  UserData(const UserData &o) {
+    for (const auto &e : o.entries_) {
+      if (e.second) { entries_[e.first] = e.second->clone(); }
+    }
+  }
+
+  UserData &operator=(const UserData &o) {
+    if (this != &o) {
+      entries_.clear();
+      for (const auto &e : o.entries_) {
+        if (e.second) { entries_[e.first] = e.second->clone(); }
+      }
+    }
+    return *this;
+  }
+
+  template <typename T> void set(const std::string &key, T &&value) {
+    using D = typename std::decay<T>::type;
+    entries_[key].reset(new detail::any_value<D>(std::forward<T>(value)));
+  }
+
+  template <typename T> T *get(const std::string &key) noexcept {
+    auto it = entries_.find(key);
+    if (it == entries_.end() || !it->second) { return nullptr; }
+    if (it->second->type_id() != detail::any_typeid<T>()) { return nullptr; }
+    return &static_cast<detail::any_value<T> *>(it->second.get())->value;
+  }
+
+  template <typename T> const T *get(const std::string &key) const noexcept {
+    auto it = entries_.find(key);
+    if (it == entries_.end() || !it->second) { return nullptr; }
+    if (it->second->type_id() != detail::any_typeid<T>()) { return nullptr; }
+    return &static_cast<const detail::any_value<T> *>(it->second.get())->value;
+  }
+
+  bool has(const std::string &key) const noexcept {
+    return entries_.find(key) != entries_.end();
+  }
+
+  void erase(const std::string &key) { entries_.erase(key); }
+
+  void clear() noexcept { entries_.clear(); }
+
+private:
+  std::unordered_map<std::string, std::unique_ptr<detail::any_storage>>
+      entries_;
+};
+
+struct Response;
+using ResponseHandler = std::function<bool(const Response &response)>;
+
+struct FormData {
+  std::string name;
+  std::string content;
+  std::string filename;
+  std::string content_type;
+  Headers headers;
+};
+
+struct FormField {
+  std::string name;
+  std::string content;
+  Headers headers;
+};
+using FormFields = std::multimap<std::string, FormField>;
+
+using FormFiles = std::multimap<std::string, FormData>;
+
+struct MultipartFormData {
+  FormFields fields; // Text fields from multipart
+  FormFiles files;   // Files from multipart
+
+  // Text field access
+  std::string get_field(const std::string &key, size_t id = 0) const;
+  std::vector<std::string> get_fields(const std::string &key) const;
+  bool has_field(const std::string &key) const;
+  size_t get_field_count(const std::string &key) const;
+
+  // File access
+  FormData get_file(const std::string &key, size_t id = 0) const;
+  std::vector<FormData> get_files(const std::string &key) const;
+  bool has_file(const std::string &key) const;
+  size_t get_file_count(const std::string &key) const;
+};
+
+struct UploadFormData {
+  std::string name;
+  std::string content;
+  std::string filename;
+  std::string content_type;
+};
+using UploadFormDataItems = std::vector<UploadFormData>;
+
+class DataSink {
+public:
+  DataSink() : os(&sb_), sb_(*this) {}
+
+  DataSink(const DataSink &) = delete;
+  DataSink &operator=(const DataSink &) = delete;
+  DataSink(DataSink &&) = delete;
+  DataSink &operator=(DataSink &&) = delete;
+
+  std::function<bool(const char *data, size_t data_len)> write;
+  std::function<bool()> is_writable;
+  std::function<void()> done;
+  std::function<void(const Headers &trailer)> done_with_trailer;
+  std::ostream os;
+
+private:
+  class data_sink_streambuf final : public std::streambuf {
+  public:
+    explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {}
+
+  protected:
+    std::streamsize xsputn(const char *s, std::streamsize n) override {
+      if (sink_.write(s, static_cast<size_t>(n))) { return n; }
+      return 0;
+    }
+
+  private:
+    DataSink &sink_;
+  };
+
+  data_sink_streambuf sb_;
+};
+
+using ContentProvider =
+    std::function<bool(size_t offset, size_t length, DataSink &sink)>;
+
+using ContentProviderWithoutLength =
+    std::function<bool(size_t offset, DataSink &sink)>;
+
+using ContentProviderResourceReleaser = std::function<void(bool success)>;
+
+struct FormDataProvider {
+  std::string name;
+  ContentProviderWithoutLength provider;
+  std::string filename;
+  std::string content_type;
+};
+using FormDataProviderItems = std::vector<FormDataProvider>;
+
+inline FormDataProvider
+make_file_provider(const std::string &name, const std::string &filepath,
+                   const std::string &filename = std::string(),
+                   const std::string &content_type = std::string()) {
+  FormDataProvider fdp;
+  fdp.name = name;
+  fdp.filename = filename.empty() ? filepath : filename;
+  fdp.content_type = content_type;
+  fdp.provider = [filepath](size_t offset, DataSink &sink) -> bool {
+    std::ifstream f(filepath, std::ios::binary);
+    if (!f) { return false; }
+    if (offset > 0) {
+      f.seekg(static_cast<std::streamoff>(offset));
+      if (!f.good()) {
+        sink.done();
+        return true;
+      }
+    }
+    char buf[8192];
+    f.read(buf, sizeof(buf));
+    auto n = static_cast<size_t>(f.gcount());
+    if (n > 0) { return sink.write(buf, n); }
+    sink.done(); // EOF
+    return true;
+  };
+  return fdp;
+}
+
+inline std::pair<size_t, ContentProvider>
+make_file_body(const std::string &filepath) {
+  size_t size = 0;
+  {
+    std::ifstream f(filepath, std::ios::binary | std::ios::ate);
+    if (!f) { return {0, ContentProvider{}}; }
+    size = static_cast<size_t>(f.tellg());
+  }
+
+  ContentProvider provider = [filepath](size_t offset, size_t length,
+                                        DataSink &sink) -> bool {
+    std::ifstream f(filepath, std::ios::binary);
+    if (!f) { return false; }
+    f.seekg(static_cast<std::streamoff>(offset));
+    if (!f.good()) { return false; }
+    char buf[8192];
+    while (length > 0) {
+      auto to_read = (std::min)(sizeof(buf), length);
+      f.read(buf, static_cast<std::streamsize>(to_read));
+      auto n = static_cast<size_t>(f.gcount());
+      if (n == 0) { break; }
+      if (!sink.write(buf, n)) { return false; }
+      length -= n;
+    }
+    return true;
+  };
+  return {size, std::move(provider)};
+}
+
+using ContentReceiverWithProgress = std::function<bool(
+    const char *data, size_t data_length, size_t offset, size_t total_length)>;
+
+using ContentReceiver =
+    std::function<bool(const char *data, size_t data_length)>;
+
+using FormDataHeader = std::function<bool(const FormData &file)>;
+
+class ContentReader {
+public:
+  using Reader = std::function<bool(ContentReceiver receiver)>;
+  using FormDataReader =
+      std::function<bool(FormDataHeader header, ContentReceiver receiver)>;
+
+  ContentReader(Reader reader, FormDataReader multipart_reader)
+      : reader_(std::move(reader)),
+        formdata_reader_(std::move(multipart_reader)) {}
+
+  bool operator()(FormDataHeader header, ContentReceiver receiver) const {
+    return formdata_reader_(std::move(header), std::move(receiver));
+  }
+
+  bool operator()(ContentReceiver receiver) const {
+    return reader_(std::move(receiver));
+  }
+
+  Reader reader_;
+  FormDataReader formdata_reader_;
+};
+
+using Range = std::pair<ssize_t, ssize_t>;
+using Ranges = std::vector<Range>;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+// TLS abstraction layer - public type definitions and API
+namespace tls {
+
+// Opaque handles (defined as void* for abstraction)
+using ctx_t = void *;
+using session_t = void *;
+using const_session_t = const void *; // For read-only session access
+using cert_t = void *;
+using ca_store_t = void *;
+
+// TLS versions
+enum class Version {
+  TLS1_2 = 0x0303,
+  TLS1_3 = 0x0304,
+};
+
+// Subject Alternative Names (SAN) entry types
+enum class SanType { DNS, IP, EMAIL, URI, OTHER };
+
+// SAN entry structure
+struct SanEntry {
+  SanType type;
+  std::string value;
+};
+
+// Verification context for certificate verification callback
+struct VerifyContext {
+  session_t session;        // TLS session handle
+  cert_t cert;              // Current certificate being verified
+  int depth;                // Certificate chain depth (0 = leaf)
+  bool preverify_ok;        // OpenSSL/Mbed TLS pre-verification result
+  long error_code;          // Backend-specific error code (0 = no error)
+  const char *error_string; // Human-readable error description
+
+  // Certificate introspection methods
+  std::string subject_cn() const;
+  std::string issuer_name() const;
+  bool check_hostname(const char *hostname) const;
+  std::vector<SanEntry> sans() const;
+  bool validity(time_t &not_before, time_t &not_after) const;
+  std::string serial() const;
+};
+
+using VerifyCallback = std::function<bool(const VerifyContext &ctx)>;
+
+// TlsError codes for TLS operations (backend-independent)
+enum class ErrorCode : int {
+  Success = 0,
+  WantRead,         // Non-blocking: need to wait for read
+  WantWrite,        // Non-blocking: need to wait for write
+  PeerClosed,       // Peer closed the connection
+  Fatal,            // Unrecoverable error
+  SyscallError,     // System call error (check sys_errno)
+  CertVerifyFailed, // Certificate verification failed
+  HostnameMismatch, // Hostname verification failed
+};
+
+// TLS error information
+struct TlsError {
+  ErrorCode code = ErrorCode::Fatal;
+  uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value
+  int sys_errno = 0;         // errno when SyscallError
+
+  // Convert verification error code to human-readable string
+  static std::string verify_error_to_string(long error_code);
+};
+
+// RAII wrapper for peer certificate
+class PeerCert {
+public:
+  PeerCert();
+  PeerCert(PeerCert &&other) noexcept;
+  PeerCert &operator=(PeerCert &&other) noexcept;
+  ~PeerCert();
+
+  PeerCert(const PeerCert &) = delete;
+  PeerCert &operator=(const PeerCert &) = delete;
+
+  explicit operator bool() const;
+  std::string subject_cn() const;
+  std::string issuer_name() const;
+  bool check_hostname(const char *hostname) const;
+  std::vector<SanEntry> sans() const;
+  bool validity(time_t &not_before, time_t &not_after) const;
+  std::string serial() const;
+
+private:
+  explicit PeerCert(cert_t cert);
+  cert_t cert_ = nullptr;
+  friend PeerCert get_peer_cert_from_session(const_session_t session);
+};
+
+// Callback for TLS context setup (used by SSLServer constructor)
+using ContextSetupCallback = std::function<bool(ctx_t ctx)>;
+
+} // namespace tls
+#endif
+
+struct Request {
+  std::string method;
+  std::string path;
+  std::string matched_route;
+  Params params;
+  Headers headers;
+  Headers trailers;
+  std::string body;
+
+  std::string remote_addr;
+  int remote_port = -1;
+  std::string local_addr;
+  int local_port = -1;
+
+  // for server
+  std::string version;
+  std::string target;
+  MultipartFormData form;
+  Ranges ranges;
+  Match matches;
+  std::unordered_map<std::string, std::string> path_params;
+  std::function<bool()> is_connection_closed = []() { return true; };
+
+  // for client
+  std::vector<std::string> accept_content_types;
+  ResponseHandler response_handler;
+  ContentReceiverWithProgress content_receiver;
+  DownloadProgress download_progress;
+  UploadProgress upload_progress;
+
+  bool has_header(const std::string &key) const;
+  std::string get_header_value(const std::string &key, const char *def = "",
+                               size_t id = 0) const;
+  size_t get_header_value_u64(const std::string &key, size_t def = 0,
+                              size_t id = 0) const;
+  size_t get_header_value_count(const std::string &key) const;
+  void set_header(const std::string &key, const std::string &val);
+
+  bool has_trailer(const std::string &key) const;
+  std::string get_trailer_value(const std::string &key, size_t id = 0) const;
+  size_t get_trailer_value_count(const std::string &key) const;
+
+  bool has_param(const std::string &key) const;
+  std::string get_param_value(const std::string &key, size_t id = 0) const;
+  std::vector<std::string> get_param_values(const std::string &key) const;
+  size_t get_param_value_count(const std::string &key) const;
+
+  bool is_multipart_form_data() const;
+
+  // private members...
+  bool body_consumed_ = false;
+  size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
+  size_t content_length_ = 0;
+  ContentProvider content_provider_;
+  bool is_chunked_content_provider_ = false;
+  size_t authorization_count_ = 0;
+  std::chrono::time_point<std::chrono::steady_clock> start_time_ =
+      (std::chrono::steady_clock::time_point::min)();
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  tls::const_session_t ssl = nullptr;
+  tls::PeerCert peer_cert() const;
+  std::string sni() const;
+#endif
+};
+
+struct Response {
+  std::string version;
+  int status = -1;
+  std::string reason;
+  Headers headers;
+  Headers trailers;
+  std::string body;
+  std::string location; // Redirect location
+
+  // User-defined context — set by pre-routing/pre-request handlers and read
+  // by route handlers to pass arbitrary data (e.g. decoded auth tokens).
+  UserData user_data;
+
+  bool has_header(const std::string &key) const;
+  std::string get_header_value(const std::string &key, const char *def = "",
+                               size_t id = 0) const;
+  size_t get_header_value_u64(const std::string &key, size_t def = 0,
+                              size_t id = 0) const;
+  size_t get_header_value_count(const std::string &key) const;
+  void set_header(const std::string &key, const std::string &val);
+
+  bool has_trailer(const std::string &key) const;
+  std::string get_trailer_value(const std::string &key, size_t id = 0) const;
+  size_t get_trailer_value_count(const std::string &key) const;
+
+  void set_redirect(const std::string &url, int status = StatusCode::Found_302);
+  void set_content(const char *s, size_t n, const std::string &content_type);
+  void set_content(const std::string &s, const std::string &content_type);
+  void set_content(std::string &&s, const std::string &content_type);
+
+  void set_content_provider(
+      size_t length, const std::string &content_type, ContentProvider provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  void set_content_provider(
+      const std::string &content_type, ContentProviderWithoutLength provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  void set_chunked_content_provider(
+      const std::string &content_type, ContentProviderWithoutLength provider,
+      ContentProviderResourceReleaser resource_releaser = nullptr);
+
+  void set_file_content(const std::string &path,
+                        const std::string &content_type);
+  void set_file_content(const std::string &path);
+
+  Response() = default;
+  Response(const Response &) = default;
+  Response &operator=(const Response &) = default;
+  Response(Response &&) = default;
+  Response &operator=(Response &&) = default;
+  ~Response() {
+    if (content_provider_resource_releaser_) {
+      content_provider_resource_releaser_(content_provider_success_);
+    }
+  }
+
+  // private members...
+  size_t content_length_ = 0;
+  ContentProvider content_provider_;
+  ContentProviderResourceReleaser content_provider_resource_releaser_;
+  bool is_chunked_content_provider_ = false;
+  bool content_provider_success_ = false;
+  std::string file_content_path_;
+  std::string file_content_content_type_;
+};
+
+enum class Error {
+  Success = 0,
+  Unknown,
+  Connection,
+  BindIPAddress,
+  Read,
+  Write,
+  ExceedRedirectCount,
+  Canceled,
+  SSLConnection,
+  SSLLoadingCerts,
+  SSLServerVerification,
+  SSLServerHostnameVerification,
+  UnsupportedMultipartBoundaryChars,
+  Compression,
+  ConnectionTimeout,
+  ProxyConnection,
+  ConnectionClosed,
+  Timeout,
+  ResourceExhaustion,
+  TooManyFormDataFiles,
+  ExceedMaxPayloadSize,
+  ExceedUriMaxLength,
+  ExceedMaxSocketDescriptorCount,
+  InvalidRequestLine,
+  InvalidHTTPMethod,
+  InvalidHTTPVersion,
+  InvalidHeaders,
+  MultipartParsing,
+  OpenFile,
+  Listen,
+  GetSockName,
+  UnsupportedAddressFamily,
+  HTTPParsing,
+  InvalidRangeHeader,
+
+  // For internal use only
+  SSLPeerCouldBeClosed_,
+};
+
+std::string to_string(Error error);
+
+std::ostream &operator<<(std::ostream &os, const Error &obj);
+
+class Stream {
+public:
+  virtual ~Stream() = default;
+
+  virtual bool is_readable() const = 0;
+  virtual bool wait_readable() const = 0;
+  virtual bool wait_writable() const = 0;
+  virtual bool is_peer_alive() const { return wait_writable(); }
+
+  virtual ssize_t read(char *ptr, size_t size) = 0;
+  virtual ssize_t write(const char *ptr, size_t size) = 0;
+  virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0;
+  virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0;
+  virtual socket_t socket() const = 0;
+
+  virtual time_t duration() const = 0;
+
+  virtual void set_read_timeout(time_t sec, time_t usec = 0) {
+    (void)sec;
+    (void)usec;
+  }
+
+  ssize_t write(const char *ptr);
+  ssize_t write(const std::string &s);
+
+  Error get_error() const { return error_; }
+
+protected:
+  Error error_ = Error::Success;
+};
+
+class TaskQueue {
+public:
+  TaskQueue() = default;
+  virtual ~TaskQueue() = default;
+
+  virtual bool enqueue(std::function<void()> fn) = 0;
+  virtual void shutdown() = 0;
+
+  virtual void on_idle() {}
+};
+
+class ThreadPool final : public TaskQueue {
+public:
+  explicit ThreadPool(size_t n, size_t max_n = 0, size_t mqr = 0);
+  ThreadPool(const ThreadPool &) = delete;
+  ~ThreadPool() override = default;
+
+  bool enqueue(std::function<void()> fn) override;
+  void shutdown() override;
+
+private:
+  void worker(bool is_dynamic);
+  void move_to_finished(std::thread::id id);
+  void cleanup_finished_threads();
+
+  size_t base_thread_count_;
+  size_t max_thread_count_;
+  size_t max_queued_requests_;
+  size_t idle_thread_count_;
+
+  bool shutdown_;
+
+  std::list<std::function<void()>> jobs_;
+  std::vector<std::thread> threads_;       // base threads
+  std::list<std::thread> dynamic_threads_; // dynamic threads
+  std::vector<std::thread>
+      finished_threads_; // exited dynamic threads awaiting join
+
+  std::condition_variable cond_;
+  std::mutex mutex_;
+};
+
+using Logger = std::function<void(const Request &, const Response &)>;
+
+// Forward declaration for Error type
+enum class Error;
+using ErrorLogger = std::function<void(const Error &, const Request *)>;
+
+using SocketOptions = std::function<void(socket_t sock)>;
+
+void default_socket_options(socket_t sock);
+
+bool set_socket_opt(socket_t sock, int level, int optname, int optval);
+
+const char *status_message(int status);
+
+std::string to_string(Error error);
+
+std::ostream &operator<<(std::ostream &os, const Error &obj);
+
+std::string get_bearer_token_auth(const Request &req);
+
+namespace detail {
+
+class MatcherBase {
+public:
+  MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {}
+  virtual ~MatcherBase() = default;
+
+  const std::string &pattern() const { return pattern_; }
+
+  // Match request path and populate its matches and
+  virtual bool match(Request &request) const = 0;
+
+private:
+  std::string pattern_;
+};
+
+/**
+ * Captures parameters in request path and stores them in Request::path_params
+ *
+ * Capture name is a substring of a pattern from : to /.
+ * The rest of the pattern is matched against the request path directly
+ * Parameters are captured starting from the next character after
+ * the end of the last matched static pattern fragment until the next /.
+ *
+ * Example pattern:
+ * "/path/fragments/:capture/more/fragments/:second_capture"
+ * Static fragments:
+ * "/path/fragments/", "more/fragments/"
+ *
+ * Given the following request path:
+ * "/path/fragments/:1/more/fragments/:2"
+ * the resulting capture will be
+ * {{"capture", "1"}, {"second_capture", "2"}}
+ */
+class PathParamsMatcher final : public MatcherBase {
+public:
+  PathParamsMatcher(const std::string &pattern);
+
+  bool match(Request &request) const override;
+
+private:
+  // Treat segment separators as the end of path parameter capture
+  // Does not need to handle query parameters as they are parsed before path
+  // matching
+  static constexpr char separator = '/';
+
+  // Contains static path fragments to match against, excluding the '/' after
+  // path params
+  // Fragments are separated by path params
+  std::vector<std::string> static_fragments_;
+  // Stores the names of the path parameters to be used as keys in the
+  // Request::path_params map
+  std::vector<std::string> param_names_;
+};
+
+/**
+ * Performs std::regex_match on request path
+ * and stores the result in Request::matches
+ *
+ * Note that regex match is performed directly on the whole request.
+ * This means that wildcard patterns may match multiple path segments with /:
+ * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end".
+ */
+class RegexMatcher final : public MatcherBase {
+public:
+  RegexMatcher(const std::string &pattern)
+      : MatcherBase(pattern), regex_(pattern) {}
+
+  bool match(Request &request) const override;
+
+private:
+  std::regex regex_;
+};
+
+int close_socket(socket_t sock);
+
+ssize_t write_headers(Stream &strm, const Headers &headers);
+
+bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec,
+                         time_t usec);
+
+size_t get_multipart_content_length(const UploadFormDataItems &items,
+                                    const std::string &boundary);
+
+ContentProvider
+make_multipart_content_provider(const UploadFormDataItems &items,
+                                const std::string &boundary);
+
+} // namespace detail
+
+class Server {
+public:
+  using Handler = std::function<void(const Request &, Response &)>;
+
+  using ExceptionHandler =
+      std::function<void(const Request &, Response &, std::exception_ptr ep)>;
+
+  enum class HandlerResponse {
+    Handled,
+    Unhandled,
+  };
+  using HandlerWithResponse =
+      std::function<HandlerResponse(const Request &, Response &)>;
+
+  using HandlerWithContentReader = std::function<void(
+      const Request &, Response &, const ContentReader &content_reader)>;
+
+  using Expect100ContinueHandler =
+      std::function<int(const Request &, Response &)>;
+
+  using WebSocketHandler =
+      std::function<void(const Request &, ws::WebSocket &)>;
+  using SubProtocolSelector =
+      std::function<std::string(const std::vector<std::string> &protocols)>;
+
+  Server();
+
+  virtual ~Server();
+
+  virtual bool is_valid() const;
+
+  Server &Get(const std::string &pattern, Handler handler);
+  Server &Post(const std::string &pattern, Handler handler);
+  Server &Post(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Put(const std::string &pattern, Handler handler);
+  Server &Put(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Patch(const std::string &pattern, Handler handler);
+  Server &Patch(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Delete(const std::string &pattern, Handler handler);
+  Server &Delete(const std::string &pattern, HandlerWithContentReader handler);
+  Server &Options(const std::string &pattern, Handler handler);
+
+  Server &WebSocket(const std::string &pattern, WebSocketHandler handler);
+  Server &WebSocket(const std::string &pattern, WebSocketHandler handler,
+                    SubProtocolSelector sub_protocol_selector);
+
+  bool set_base_dir(const std::string &dir,
+                    const std::string &mount_point = std::string());
+  bool set_mount_point(const std::string &mount_point, const std::string &dir,
+                       Headers headers = Headers());
+  bool remove_mount_point(const std::string &mount_point);
+  Server &set_file_extension_and_mimetype_mapping(const std::string &ext,
+                                                  const std::string &mime);
+  Server &set_default_file_mimetype(const std::string &mime);
+  Server &set_file_request_handler(Handler handler);
+
+  template <class ErrorHandlerFunc>
+  Server &set_error_handler(ErrorHandlerFunc &&handler) {
+    return set_error_handler_core(
+        std::forward<ErrorHandlerFunc>(handler),
+        std::is_convertible<ErrorHandlerFunc, HandlerWithResponse>{});
+  }
+
+  Server &set_exception_handler(ExceptionHandler handler);
+
+  Server &set_pre_routing_handler(HandlerWithResponse handler);
+  Server &set_post_routing_handler(Handler handler);
+
+  Server &set_pre_request_handler(HandlerWithResponse handler);
+
+  Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
+  Server &set_logger(Logger logger);
+  Server &set_pre_compression_logger(Logger logger);
+  Server &set_error_logger(ErrorLogger error_logger);
+
+  Server &set_address_family(int family);
+  Server &set_tcp_nodelay(bool on);
+  Server &set_ipv6_v6only(bool on);
+  Server &set_socket_options(SocketOptions socket_options);
+
+  Server &set_default_headers(Headers headers);
+  Server &
+  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);
+
+  Server &set_trusted_proxies(const std::vector<std::string> &proxies);
+
+  Server &set_keep_alive_max_count(size_t count);
+  Server &set_keep_alive_timeout(time_t sec);
+  template <class Rep, class Period>
+  Server &
+  set_keep_alive_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_idle_interval(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  Server &set_idle_interval(const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_payload_max_length(size_t length);
+
+  Server &set_websocket_ping_interval(time_t sec);
+  template <class Rep, class Period>
+  Server &set_websocket_ping_interval(
+      const std::chrono::duration<Rep, Period> &duration);
+
+  Server &set_websocket_max_missed_pongs(int count);
+
+  bool bind_to_port(const std::string &host, int port, int socket_flags = 0);
+  int bind_to_any_port(const std::string &host, int socket_flags = 0);
+  bool listen_after_bind();
+
+  bool listen(const std::string &host, int port, int socket_flags = 0);
+
+  bool is_running() const;
+  void wait_until_ready() const;
+  void stop();
+  void decommission();
+
+  std::function<TaskQueue *(void)> new_task_queue;
+
+protected:
+  bool process_request(Stream &strm, const std::string &remote_addr,
+                       int remote_port, const std::string &local_addr,
+                       int local_port, bool close_connection,
+                       bool &connection_closed,
+                       const std::function<void(Request &)> &setup_request,
+                       bool *websocket_upgraded = nullptr);
+
+  std::atomic<socket_t> svr_sock_{INVALID_SOCKET};
+
+  std::vector<std::string> trusted_proxies_;
+
+  size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT;
+  time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND;
+  time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND;
+  time_t read_timeout_usec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND;
+  time_t write_timeout_sec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND;
+  time_t write_timeout_usec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND;
+  time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
+  time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
+  size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  time_t websocket_ping_interval_sec_ =
+      CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
+  int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS;
+
+private:
+  using Handlers =
+      std::vector<std::pair<std::unique_ptr<detail::MatcherBase>, Handler>>;
+  using HandlersForContentReader =
+      std::vector<std::pair<std::unique_ptr<detail::MatcherBase>,
+                            HandlerWithContentReader>>;
+
+  static std::unique_ptr<detail::MatcherBase>
+  make_matcher(const std::string &pattern);
+
+  template <typename H>
+  Server &add_handler(
+      std::vector<std::pair<std::unique_ptr<detail::MatcherBase>, H>> &handlers,
+      const std::string &pattern, H handler) {
+    handlers.emplace_back(make_matcher(pattern), std::move(handler));
+    return *this;
+  }
+
+  Server &set_error_handler_core(HandlerWithResponse handler, std::true_type);
+  Server &set_error_handler_core(Handler handler, std::false_type);
+
+  socket_t create_server_socket(const std::string &host, int port,
+                                int socket_flags,
+                                SocketOptions socket_options) const;
+  int bind_internal(const std::string &host, int port, int socket_flags);
+  bool listen_internal();
+
+  bool routing(Request &req, Response &res, Stream &strm);
+  bool handle_file_request(Request &req, Response &res);
+  bool check_if_not_modified(const Request &req, Response &res,
+                             const std::string &etag, time_t mtime) const;
+  bool check_if_range(Request &req, const std::string &etag,
+                      time_t mtime) const;
+  bool dispatch_request(Request &req, Response &res,
+                        const Handlers &handlers) const;
+  bool dispatch_request_for_content_reader(
+      Request &req, Response &res, ContentReader content_reader,
+      const HandlersForContentReader &handlers) const;
+
+  bool parse_request_line(const char *s, Request &req) const;
+  void apply_ranges(const Request &req, Response &res,
+                    std::string &content_type, std::string &boundary) const;
+  bool write_response(Stream &strm, bool close_connection, Request &req,
+                      Response &res);
+  bool write_response_with_content(Stream &strm, bool close_connection,
+                                   const Request &req, Response &res);
+  bool write_response_core(Stream &strm, bool close_connection,
+                           const Request &req, Response &res,
+                           bool need_apply_ranges);
+  bool write_content_with_provider(Stream &strm, const Request &req,
+                                   Response &res, const std::string &boundary,
+                                   const std::string &content_type);
+  bool read_content(Stream &strm, Request &req, Response &res);
+  bool read_content_with_content_receiver(Stream &strm, Request &req,
+                                          Response &res,
+                                          ContentReceiver receiver,
+                                          FormDataHeader multipart_header,
+                                          ContentReceiver multipart_receiver);
+  bool read_content_core(Stream &strm, Request &req, Response &res,
+                         ContentReceiver receiver,
+                         FormDataHeader multipart_header,
+                         ContentReceiver multipart_receiver) const;
+
+  virtual bool process_and_close_socket(socket_t sock);
+
+  void output_log(const Request &req, const Response &res) const;
+  void output_pre_compression_log(const Request &req,
+                                  const Response &res) const;
+  void output_error_log(const Error &err, const Request *req) const;
+
+  std::atomic<bool> is_running_{false};
+  std::atomic<bool> is_decommissioned{false};
+
+  struct MountPointEntry {
+    std::string mount_point;
+    std::string base_dir;
+    std::string resolved_base_dir;
+    Headers headers;
+  };
+  std::vector<MountPointEntry> base_dirs_;
+  std::map<std::string, std::string> file_extension_and_mimetype_map_;
+  std::string default_file_mimetype_ = "application/octet-stream";
+  Handler file_request_handler_;
+
+  Handlers get_handlers_;
+  Handlers post_handlers_;
+  HandlersForContentReader post_handlers_for_content_reader_;
+  Handlers put_handlers_;
+  HandlersForContentReader put_handlers_for_content_reader_;
+  Handlers patch_handlers_;
+  HandlersForContentReader patch_handlers_for_content_reader_;
+  Handlers delete_handlers_;
+  HandlersForContentReader delete_handlers_for_content_reader_;
+  Handlers options_handlers_;
+
+  struct WebSocketHandlerEntry {
+    std::unique_ptr<detail::MatcherBase> matcher;
+    WebSocketHandler handler;
+    SubProtocolSelector sub_protocol_selector;
+  };
+  using WebSocketHandlers = std::vector<WebSocketHandlerEntry>;
+  WebSocketHandlers websocket_handlers_;
+
+  HandlerWithResponse error_handler_;
+  ExceptionHandler exception_handler_;
+  HandlerWithResponse pre_routing_handler_;
+  Handler post_routing_handler_;
+  HandlerWithResponse pre_request_handler_;
+  Expect100ContinueHandler expect_100_continue_handler_;
+
+  mutable std::mutex logger_mutex_;
+  Logger logger_;
+  Logger pre_compression_logger_;
+  ErrorLogger error_logger_;
+
+  int address_family_ = AF_UNSPEC;
+  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
+  bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;
+  SocketOptions socket_options_ = default_socket_options;
+
+  Headers default_headers_;
+  std::function<ssize_t(Stream &, Headers &)> header_writer_ =
+      detail::write_headers;
+};
+
+class Result {
+public:
+  Result() = default;
+  Result(std::unique_ptr<Response> &&res, Error err,
+         Headers &&request_headers = Headers{})
+      : res_(std::move(res)), err_(err),
+        request_headers_(std::move(request_headers)) {}
+  // Response
+  operator bool() const { return res_ != nullptr; }
+  bool operator==(std::nullptr_t) const { return res_ == nullptr; }
+  bool operator!=(std::nullptr_t) const { return res_ != nullptr; }
+  const Response &value() const { return *res_; }
+  Response &value() { return *res_; }
+  const Response &operator*() const { return *res_; }
+  Response &operator*() { return *res_; }
+  const Response *operator->() const { return res_.get(); }
+  Response *operator->() { return res_.get(); }
+
+  // Error
+  Error error() const { return err_; }
+
+  // Request Headers
+  bool has_request_header(const std::string &key) const;
+  std::string get_request_header_value(const std::string &key,
+                                       const char *def = "",
+                                       size_t id = 0) const;
+  size_t get_request_header_value_u64(const std::string &key, size_t def = 0,
+                                      size_t id = 0) const;
+  size_t get_request_header_value_count(const std::string &key) const;
+
+private:
+  std::unique_ptr<Response> res_;
+  Error err_ = Error::Unknown;
+  Headers request_headers_;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+public:
+  Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
+         int ssl_error)
+      : res_(std::move(res)), err_(err),
+        request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {}
+  Result(std::unique_ptr<Response> &&res, Error err, Headers &&request_headers,
+         int ssl_error, uint64_t ssl_backend_error)
+      : res_(std::move(res)), err_(err),
+        request_headers_(std::move(request_headers)), ssl_error_(ssl_error),
+        ssl_backend_error_(ssl_backend_error) {}
+
+  int ssl_error() const { return ssl_error_; }
+  uint64_t ssl_backend_error() const { return ssl_backend_error_; }
+
+private:
+  int ssl_error_ = 0;
+  uint64_t ssl_backend_error_ = 0;
+#endif
+};
+
+struct ClientConnection {
+  socket_t sock = INVALID_SOCKET;
+
+  bool is_open() const { return sock != INVALID_SOCKET; }
+
+  ClientConnection() = default;
+
+  ~ClientConnection();
+
+  ClientConnection(const ClientConnection &) = delete;
+  ClientConnection &operator=(const ClientConnection &) = delete;
+
+  ClientConnection(ClientConnection &&other) noexcept
+      : sock(other.sock)
+#ifdef CPPHTTPLIB_SSL_ENABLED
+        ,
+        session(other.session)
+#endif
+  {
+    other.sock = INVALID_SOCKET;
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    other.session = nullptr;
+#endif
+  }
+
+  ClientConnection &operator=(ClientConnection &&other) noexcept {
+    if (this != &other) {
+      sock = other.sock;
+      other.sock = INVALID_SOCKET;
+#ifdef CPPHTTPLIB_SSL_ENABLED
+      session = other.session;
+      other.session = nullptr;
+#endif
+    }
+    return *this;
+  }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  tls::session_t session = nullptr;
+#endif
+};
+
+namespace detail {
+
+struct ChunkedDecoder;
+
+struct BodyReader {
+  Stream *stream = nullptr;
+  bool has_content_length = false;
+  size_t content_length = 0;
+  size_t payload_max_length = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  size_t bytes_read = 0;
+  bool chunked = false;
+  bool eof = false;
+  std::unique_ptr<ChunkedDecoder> chunked_decoder;
+  Error last_error = Error::Success;
+
+  ssize_t read(char *buf, size_t len);
+  bool has_error() const { return last_error != Error::Success; }
+};
+
+inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf,
+                                 size_t len) {
+  (void)stream;
+  return br.read(buf, len);
+}
+
+class decompressor;
+
+} // namespace detail
+
+class ClientImpl {
+public:
+  explicit ClientImpl(const std::string &host);
+
+  explicit ClientImpl(const std::string &host, int port);
+
+  explicit ClientImpl(const std::string &host, int port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path);
+
+  virtual ~ClientImpl();
+
+  virtual bool is_valid() const;
+
+  struct StreamHandle {
+    std::unique_ptr<Response> response;
+    Error error = Error::Success;
+
+    StreamHandle() = default;
+    StreamHandle(const StreamHandle &) = delete;
+    StreamHandle &operator=(const StreamHandle &) = delete;
+    StreamHandle(StreamHandle &&) = default;
+    StreamHandle &operator=(StreamHandle &&) = default;
+    ~StreamHandle() = default;
+
+    bool is_valid() const {
+      return response != nullptr && error == Error::Success;
+    }
+
+    ssize_t read(char *buf, size_t len);
+    void parse_trailers_if_needed();
+    Error get_read_error() const { return body_reader_.last_error; }
+    bool has_read_error() const { return body_reader_.has_error(); }
+
+    bool trailers_parsed_ = false;
+
+  private:
+    friend class ClientImpl;
+
+    ssize_t read_with_decompression(char *buf, size_t len);
+
+    std::unique_ptr<ClientConnection> connection_;
+    std::unique_ptr<Stream> socket_stream_;
+    Stream *stream_ = nullptr;
+    detail::BodyReader body_reader_;
+
+    std::unique_ptr<detail::decompressor> decompressor_;
+    std::string decompress_buffer_;
+    size_t decompress_offset_ = 0;
+    size_t decompressed_bytes_read_ = 0;
+  };
+
+  // clang-format off
+  Result Get(const std::string &path, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Head(const std::string &path);
+  Result Head(const std::string &path, const Headers &headers);
+
+  Result Post(const std::string &path);
+  Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Params &params);
+  Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers);
+  Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const Params &params);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Put(const std::string &path);
+  Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Params &params);
+  Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers);
+  Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const Params &params);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Patch(const std::string &path);
+  Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Params &params);
+  Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const Params &params);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Delete(const std::string &path, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Params &params, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const Params &params, DownloadProgress progress = nullptr);
+
+  Result Options(const std::string &path);
+  Result Options(const std::string &path, const Headers &headers);
+  // clang-format on
+
+  // Streaming API: Open a stream for reading response body incrementally
+  // Socket ownership is transferred to StreamHandle for true streaming
+  // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)
+  StreamHandle open_stream(const std::string &method, const std::string &path,
+                           const Params &params = {},
+                           const Headers &headers = {},
+                           const std::string &body = {},
+                           const std::string &content_type = {});
+
+  bool send(Request &req, Response &res, Error &error);
+  Result send(const Request &req);
+
+  void stop();
+
+  std::string host() const;
+  int port() const;
+
+  size_t is_socket_open() const;
+  socket_t socket() const;
+
+  void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+
+  void set_default_headers(Headers headers);
+
+  void
+  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);
+
+  void set_address_family(int family);
+  void set_tcp_nodelay(bool on);
+  void set_ipv6_v6only(bool on);
+  void set_socket_options(SocketOptions socket_options);
+
+  void set_connection_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void
+  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_max_timeout(time_t msec);
+  template <class Rep, class Period>
+  void set_max_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_basic_auth(const std::string &username, const std::string &password);
+  void set_bearer_token_auth(const std::string &token);
+
+  void set_keep_alive(bool on);
+  void set_follow_location(bool on);
+
+  void set_path_encode(bool on);
+
+  void set_compress(bool on);
+
+  void set_decompress(bool on);
+
+  void set_payload_max_length(size_t length);
+
+  void set_interface(const std::string &intf);
+
+  void set_proxy(const std::string &host, int port);
+  void set_proxy_basic_auth(const std::string &username,
+                            const std::string &password);
+  void set_proxy_bearer_token_auth(const std::string &token);
+
+  void set_logger(Logger logger);
+  void set_error_logger(ErrorLogger error_logger);
+
+protected:
+  struct Socket {
+    socket_t sock = INVALID_SOCKET;
+
+    // For Mbed TLS compatibility: start_time for request timeout tracking
+    std::chrono::time_point<std::chrono::steady_clock> start_time_;
+
+    bool is_open() const { return sock != INVALID_SOCKET; }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    tls::session_t ssl = nullptr;
+#endif
+  };
+
+  virtual bool create_and_connect_socket(Socket &socket, Error &error);
+  virtual bool ensure_socket_connection(Socket &socket, Error &error);
+  virtual bool setup_proxy_connection(
+      Socket &socket,
+      std::chrono::time_point<std::chrono::steady_clock> start_time,
+      Response &res, bool &success, Error &error);
+
+  // All of:
+  //   shutdown_ssl
+  //   shutdown_socket
+  //   close_socket
+  // should ONLY be called when socket_mutex_ is locked.
+  // Also, shutdown_ssl and close_socket should also NOT be called concurrently
+  // with a DIFFERENT thread sending requests using that socket.
+  virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);
+  void shutdown_socket(Socket &socket) const;
+  void close_socket(Socket &socket);
+
+  bool process_request(Stream &strm, Request &req, Response &res,
+                       bool close_connection, Error &error);
+
+  bool write_content_with_provider(Stream &strm, const Request &req,
+                                   Error &error) const;
+
+  void copy_settings(const ClientImpl &rhs);
+
+  void output_log(const Request &req, const Response &res) const;
+  void output_error_log(const Error &err, const Request *req) const;
+
+  // Socket endpoint information
+  const std::string host_;
+  const int port_;
+
+  // Current open socket
+  Socket socket_;
+  mutable std::mutex socket_mutex_;
+  std::recursive_mutex request_mutex_;
+
+  // These are all protected under socket_mutex
+  size_t socket_requests_in_flight_ = 0;
+  std::thread::id socket_requests_are_from_thread_ = std::thread::id();
+  bool socket_should_be_closed_when_request_is_done_ = false;
+
+  // Hostname-IP map
+  std::map<std::string, std::string> addr_map_;
+
+  // Default headers
+  Headers default_headers_;
+
+  // Header writer
+  std::function<ssize_t(Stream &, Headers &)> header_writer_ =
+      detail::write_headers;
+
+  // Settings
+  std::string client_cert_path_;
+  std::string client_key_path_;
+
+  time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND;
+  time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
+  time_t read_timeout_sec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND;
+  time_t read_timeout_usec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND;
+  time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
+  time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
+  time_t max_timeout_msec_ = CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND;
+
+  std::string basic_auth_username_;
+  std::string basic_auth_password_;
+  std::string bearer_token_auth_token_;
+
+  bool keep_alive_ = false;
+  bool follow_location_ = false;
+
+  bool path_encode_ = true;
+
+  int address_family_ = AF_UNSPEC;
+  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
+  bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;
+  SocketOptions socket_options_ = nullptr;
+
+  bool compress_ = false;
+  bool decompress_ = true;
+
+  size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  bool has_payload_max_length_ = false;
+
+  std::string interface_;
+
+  std::string proxy_host_;
+  int proxy_port_ = -1;
+
+  std::string proxy_basic_auth_username_;
+  std::string proxy_basic_auth_password_;
+  std::string proxy_bearer_token_auth_token_;
+
+  mutable std::mutex logger_mutex_;
+  Logger logger_;
+  ErrorLogger error_logger_;
+
+private:
+  bool send_(Request &req, Response &res, Error &error);
+  Result send_(Request &&req);
+
+  socket_t create_client_socket(Error &error) const;
+  bool read_response_line(Stream &strm, const Request &req, Response &res,
+                          bool skip_100_continue = true) const;
+  bool write_request(Stream &strm, Request &req, bool close_connection,
+                     Error &error, bool skip_body = false);
+  bool write_request_body(Stream &strm, Request &req, Error &error);
+  void prepare_default_headers(Request &r, bool for_stream,
+                               const std::string &ct);
+  bool redirect(Request &req, Response &res, Error &error);
+  bool create_redirect_client(const std::string &scheme,
+                              const std::string &host, int port, Request &req,
+                              Response &res, const std::string &path,
+                              const std::string &location, Error &error);
+  template <typename ClientType> void setup_redirect_client(ClientType &client);
+  bool handle_request(Stream &strm, Request &req, Response &res,
+                      bool close_connection, Error &error);
+  std::unique_ptr<Response> send_with_content_provider_and_receiver(
+      Request &req, const char *body, size_t content_length,
+      ContentProvider content_provider,
+      ContentProviderWithoutLength content_provider_without_length,
+      const std::string &content_type, ContentReceiver content_receiver,
+      Error &error);
+  Result send_with_content_provider_and_receiver(
+      const std::string &method, const std::string &path,
+      const Headers &headers, const char *body, size_t content_length,
+      ContentProvider content_provider,
+      ContentProviderWithoutLength content_provider_without_length,
+      const std::string &content_type, ContentReceiver content_receiver,
+      UploadProgress progress);
+  ContentProviderWithoutLength get_multipart_content_provider(
+      const std::string &boundary, const UploadFormDataItems &items,
+      const FormDataProviderItems &provider_items) const;
+
+  virtual bool
+  process_socket(const Socket &socket,
+                 std::chrono::time_point<std::chrono::steady_clock> start_time,
+                 std::function<bool(Stream &strm)> callback);
+  virtual bool is_ssl() const;
+
+  void transfer_socket_ownership_to_handle(StreamHandle &handle);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+public:
+  void set_digest_auth(const std::string &username,
+                       const std::string &password);
+  void set_proxy_digest_auth(const std::string &username,
+                             const std::string &password);
+  void set_ca_cert_path(const std::string &ca_cert_file_path,
+                        const std::string &ca_cert_dir_path = std::string());
+  void enable_server_certificate_verification(bool enabled);
+  void enable_server_hostname_verification(bool enabled);
+
+protected:
+  std::string digest_auth_username_;
+  std::string digest_auth_password_;
+  std::string proxy_digest_auth_username_;
+  std::string proxy_digest_auth_password_;
+  std::string ca_cert_file_path_;
+  std::string ca_cert_dir_path_;
+  bool server_certificate_verification_ = true;
+  bool server_hostname_verification_ = true;
+  std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer
+  int last_ssl_error_ = 0;
+  uint64_t last_backend_error_ = 0;
+#endif
+};
+
+class Client {
+public:
+  // Universal interface
+  explicit Client(const std::string &scheme_host_port);
+
+  explicit Client(const std::string &scheme_host_port,
+                  const std::string &client_cert_path,
+                  const std::string &client_key_path);
+
+  // HTTP only interface
+  explicit Client(const std::string &host, int port);
+
+  explicit Client(const std::string &host, int port,
+                  const std::string &client_cert_path,
+                  const std::string &client_key_path);
+
+  Client(Client &&) = default;
+  Client &operator=(Client &&) = default;
+
+  ~Client();
+
+  bool is_valid() const;
+
+  // clang-format off
+  Result Get(const std::string &path, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Get(const std::string &path, const Params &params, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Head(const std::string &path);
+  Result Head(const std::string &path, const Headers &headers);
+
+  Result Post(const std::string &path);
+  Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Params &params);
+  Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers);
+  Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const Params &params);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Put(const std::string &path);
+  Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Params &params);
+  Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers);
+  Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const Params &params);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Patch(const std::string &path);
+  Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Params &params);
+  Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers);
+  Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const Params &params);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr);
+  Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
+
+  Result Delete(const std::string &path, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Params &params, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr);
+  Result Delete(const std::string &path, const Headers &headers, const Params &params, DownloadProgress progress = nullptr);
+
+  Result Options(const std::string &path);
+  Result Options(const std::string &path, const Headers &headers);
+  // clang-format on
+
+  // Streaming API: Open a stream for reading response body incrementally
+  // Socket ownership is transferred to StreamHandle for true streaming
+  // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)
+  ClientImpl::StreamHandle open_stream(const std::string &method,
+                                       const std::string &path,
+                                       const Params &params = {},
+                                       const Headers &headers = {},
+                                       const std::string &body = {},
+                                       const std::string &content_type = {});
+
+  bool send(Request &req, Response &res, Error &error);
+  Result send(const Request &req);
+
+  void stop();
+
+  std::string host() const;
+  int port() const;
+
+  size_t is_socket_open() const;
+  socket_t socket() const;
+
+  void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+
+  void set_default_headers(Headers headers);
+
+  void
+  set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);
+
+  void set_address_family(int family);
+  void set_tcp_nodelay(bool on);
+  void set_socket_options(SocketOptions socket_options);
+
+  void set_connection_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void
+  set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_read_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_write_timeout(time_t sec, time_t usec = 0);
+  template <class Rep, class Period>
+  void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_max_timeout(time_t msec);
+  template <class Rep, class Period>
+  void set_max_timeout(const std::chrono::duration<Rep, Period> &duration);
+
+  void set_basic_auth(const std::string &username, const std::string &password);
+  void set_bearer_token_auth(const std::string &token);
+
+  void set_keep_alive(bool on);
+  void set_follow_location(bool on);
+
+  void set_path_encode(bool on);
+
+  void set_compress(bool on);
+
+  void set_decompress(bool on);
+
+  void set_payload_max_length(size_t length);
+
+  void set_interface(const std::string &intf);
+
+  void set_proxy(const std::string &host, int port);
+  void set_proxy_basic_auth(const std::string &username,
+                            const std::string &password);
+  void set_proxy_bearer_token_auth(const std::string &token);
+  void set_logger(Logger logger);
+  void set_error_logger(ErrorLogger error_logger);
+
+private:
+  std::unique_ptr<ClientImpl> cli_;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+public:
+  void set_digest_auth(const std::string &username,
+                       const std::string &password);
+  void set_proxy_digest_auth(const std::string &username,
+                             const std::string &password);
+  void enable_server_certificate_verification(bool enabled);
+  void enable_server_hostname_verification(bool enabled);
+  void set_ca_cert_path(const std::string &ca_cert_file_path,
+                        const std::string &ca_cert_dir_path = std::string());
+
+  void set_ca_cert_store(tls::ca_store_t ca_cert_store);
+  void load_ca_cert_store(const char *ca_cert, std::size_t size);
+
+  void set_server_certificate_verifier(tls::VerifyCallback verifier);
+
+  void set_session_verifier(
+      std::function<SSLVerifierResponse(tls::session_t)> verifier);
+
+  tls::ctx_t tls_context() const;
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+  void enable_windows_certificate_verification(bool enabled);
+#endif
+
+private:
+  bool is_ssl_ = false;
+#endif
+};
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+class SSLServer : public Server {
+public:
+  SSLServer(const char *cert_path, const char *private_key_path,
+            const char *client_ca_cert_file_path = nullptr,
+            const char *client_ca_cert_dir_path = nullptr,
+            const char *private_key_password = nullptr);
+
+  struct PemMemory {
+    const char *cert_pem;
+    size_t cert_pem_len;
+    const char *key_pem;
+    size_t key_pem_len;
+    const char *client_ca_pem;
+    size_t client_ca_pem_len;
+    const char *private_key_password;
+  };
+  explicit SSLServer(const PemMemory &pem);
+
+  // The callback receives the ctx_t handle which can be cast to the
+  // appropriate backend type (SSL_CTX* for OpenSSL,
+  // tls::impl::MbedTlsContext* for Mbed TLS)
+  explicit SSLServer(const tls::ContextSetupCallback &setup_callback);
+
+  ~SSLServer() override;
+
+  bool is_valid() const override;
+
+  bool update_certs_pem(const char *cert_pem, const char *key_pem,
+                        const char *client_ca_pem = nullptr,
+                        const char *password = nullptr);
+
+  tls::ctx_t tls_context() const { return ctx_; }
+
+  int ssl_last_error() const { return last_ssl_error_; }
+
+private:
+  bool process_and_close_socket(socket_t sock) override;
+
+  tls::ctx_t ctx_ = nullptr;
+  std::mutex ctx_mutex_;
+
+  int last_ssl_error_ = 0;
+};
+
+class SSLClient final : public ClientImpl {
+public:
+  explicit SSLClient(const std::string &host);
+
+  explicit SSLClient(const std::string &host, int port);
+
+  explicit SSLClient(const std::string &host, int port,
+                     const std::string &client_cert_path,
+                     const std::string &client_key_path,
+                     const std::string &private_key_password = std::string());
+
+  struct PemMemory {
+    const char *cert_pem;
+    size_t cert_pem_len;
+    const char *key_pem;
+    size_t key_pem_len;
+    const char *private_key_password;
+  };
+  explicit SSLClient(const std::string &host, int port, const PemMemory &pem);
+
+  ~SSLClient() override;
+
+  bool is_valid() const override;
+
+  void set_ca_cert_store(tls::ca_store_t ca_cert_store);
+  void load_ca_cert_store(const char *ca_cert, std::size_t size);
+
+  void set_server_certificate_verifier(tls::VerifyCallback verifier);
+
+  // Post-handshake session verifier (backend-independent)
+  void set_session_verifier(
+      std::function<SSLVerifierResponse(tls::session_t)> verifier);
+
+  tls::ctx_t tls_context() const { return ctx_; }
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+  void enable_windows_certificate_verification(bool enabled);
+#endif
+
+private:
+  bool create_and_connect_socket(Socket &socket, Error &error) override;
+  bool ensure_socket_connection(Socket &socket, Error &error) override;
+  void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override;
+  void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully);
+
+  bool
+  process_socket(const Socket &socket,
+                 std::chrono::time_point<std::chrono::steady_clock> start_time,
+                 std::function<bool(Stream &strm)> callback) override;
+  bool is_ssl() const override;
+
+  bool setup_proxy_connection(
+      Socket &socket,
+      std::chrono::time_point<std::chrono::steady_clock> start_time,
+      Response &res, bool &success, Error &error) override;
+  bool connect_with_proxy(
+      Socket &sock,
+      std::chrono::time_point<std::chrono::steady_clock> start_time,
+      Response &res, bool &success, Error &error);
+  bool initialize_ssl(Socket &socket, Error &error);
+
+  void init_ctx();
+  void reset_ctx_on_error();
+
+  bool load_certs();
+
+  tls::ctx_t ctx_ = nullptr;
+  std::mutex ctx_mutex_;
+  std::once_flag initialize_cert_;
+
+  long verify_result_ = 0;
+
+  std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+  bool enable_windows_cert_verification_ = true;
+#endif
+
+  friend class ClientImpl;
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+private:
+  bool verify_host(X509 *server_cert) const;
+  bool verify_host_with_subject_alt_name(X509 *server_cert) const;
+  bool verify_host_with_common_name(X509 *server_cert) const;
+#endif
+};
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+namespace detail {
+
+template <typename T, typename U>
+inline void duration_to_sec_and_usec(const T &duration, U callback) {
+  auto sec = std::chrono::duration_cast<std::chrono::seconds>(duration).count();
+  auto usec = std::chrono::duration_cast<std::chrono::microseconds>(
+                  duration - std::chrono::seconds(sec))
+                  .count();
+  callback(static_cast<time_t>(sec), static_cast<time_t>(usec));
+}
+
+template <size_t N> inline constexpr size_t str_len(const char (&)[N]) {
+  return N - 1;
+}
+
+inline bool is_numeric(const std::string &str) {
+  return !str.empty() &&
+         std::all_of(str.cbegin(), str.cend(),
+                     [](unsigned char c) { return std::isdigit(c); });
+}
+
+inline size_t get_header_value_u64(const Headers &headers,
+                                   const std::string &key, size_t def,
+                                   size_t id, bool &is_invalid_value) {
+  is_invalid_value = false;
+  auto rng = headers.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) {
+    if (is_numeric(it->second)) {
+      return static_cast<size_t>(std::strtoull(it->second.data(), nullptr, 10));
+    } else {
+      is_invalid_value = true;
+    }
+  }
+  return def;
+}
+
+inline size_t get_header_value_u64(const Headers &headers,
+                                   const std::string &key, size_t def,
+                                   size_t id) {
+  auto dummy = false;
+  return get_header_value_u64(headers, key, def, id, dummy);
+}
+
+} // namespace detail
+
+template <class Rep, class Period>
+inline Server &
+Server::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &
+Server::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &
+Server::set_idle_interval(const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); });
+  return *this;
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_connection_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) {
+    set_connection_timeout(sec, usec);
+  });
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_read_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_write_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(
+      duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); });
+}
+
+template <class Rep, class Period>
+inline void ClientImpl::set_max_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  auto msec =
+      std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
+  set_max_timeout(msec);
+}
+
+template <class Rep, class Period>
+inline void Client::set_connection_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_connection_timeout(duration);
+}
+
+template <class Rep, class Period>
+inline void
+Client::set_read_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_read_timeout(duration);
+}
+
+template <class Rep, class Period>
+inline void
+Client::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_write_timeout(duration);
+}
+
+inline void Client::set_max_timeout(time_t msec) {
+  cli_->set_max_timeout(msec);
+}
+
+template <class Rep, class Period>
+inline void
+Client::set_max_timeout(const std::chrono::duration<Rep, Period> &duration) {
+  cli_->set_max_timeout(duration);
+}
+
+/*
+ * Forward declarations and types that will be part of the .h file if split into
+ * .h + .cc.
+ */
+
+std::string hosted_at(const std::string &hostname);
+
+void hosted_at(const std::string &hostname, std::vector<std::string> &addrs);
+
+// JavaScript-style URL encoding/decoding functions
+std::string encode_uri_component(const std::string &value);
+std::string encode_uri(const std::string &value);
+std::string decode_uri_component(const std::string &value);
+std::string decode_uri(const std::string &value);
+
+// RFC 3986 compliant URL component encoding/decoding functions
+std::string encode_path_component(const std::string &component);
+std::string decode_path_component(const std::string &component);
+std::string encode_query_component(const std::string &component,
+                                   bool space_as_plus = true);
+std::string decode_query_component(const std::string &component,
+                                   bool plus_as_space = true);
+
+std::string sanitize_filename(const std::string &filename);
+
+std::string append_query_params(const std::string &path, const Params &params);
+
+std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
+
+std::pair<std::string, std::string>
+make_basic_authentication_header(const std::string &username,
+                                 const std::string &password,
+                                 bool is_proxy = false);
+
+namespace detail {
+
+#if defined(_WIN32)
+inline std::wstring u8string_to_wstring(const char *s) {
+  if (!s) { return std::wstring(); }
+
+  auto len = static_cast<int>(strlen(s));
+  if (!len) { return std::wstring(); }
+
+  auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0);
+  if (!wlen) { return std::wstring(); }
+
+  std::wstring ws;
+  ws.resize(wlen);
+  wlen = ::MultiByteToWideChar(
+      CP_UTF8, 0, s, len,
+      const_cast<LPWSTR>(reinterpret_cast<LPCWSTR>(ws.data())), wlen);
+  if (wlen != static_cast<int>(ws.size())) { ws.clear(); }
+  return ws;
+}
+#endif
+
+struct FileStat {
+  FileStat(const std::string &path);
+  bool is_file() const;
+  bool is_dir() const;
+  time_t mtime() const;
+  size_t size() const;
+
+private:
+#if defined(_WIN32)
+  struct _stat st_;
+#else
+  struct stat st_;
+#endif
+  int ret_ = -1;
+};
+
+std::string make_host_and_port_string(const std::string &host, int port,
+                                      bool is_ssl);
+
+std::string trim_copy(const std::string &s);
+
+void divide(
+    const char *data, std::size_t size, char d,
+    std::function<void(const char *, std::size_t, const char *, std::size_t)>
+        fn);
+
+void divide(
+    const std::string &str, char d,
+    std::function<void(const char *, std::size_t, const char *, std::size_t)>
+        fn);
+
+void split(const char *b, const char *e, char d,
+           std::function<void(const char *, const char *)> fn);
+
+void split(const char *b, const char *e, char d, size_t m,
+           std::function<void(const char *, const char *)> fn);
+
+bool process_client_socket(
+    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+    time_t write_timeout_sec, time_t write_timeout_usec,
+    time_t max_timeout_msec,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    std::function<bool(Stream &)> callback);
+
+socket_t create_client_socket(const std::string &host, const std::string &ip,
+                              int port, int address_family, bool tcp_nodelay,
+                              bool ipv6_v6only, SocketOptions socket_options,
+                              time_t connection_timeout_sec,
+                              time_t connection_timeout_usec,
+                              time_t read_timeout_sec, time_t read_timeout_usec,
+                              time_t write_timeout_sec,
+                              time_t write_timeout_usec,
+                              const std::string &intf, Error &error);
+
+const char *get_header_value(const Headers &headers, const std::string &key,
+                             const char *def, size_t id);
+
+std::string params_to_query_str(const Params &params);
+
+void parse_query_text(const char *data, std::size_t size, Params &params);
+
+void parse_query_text(const std::string &s, Params &params);
+
+bool parse_multipart_boundary(const std::string &content_type,
+                              std::string &boundary);
+
+bool parse_range_header(const std::string &s, Ranges &ranges);
+
+bool parse_accept_header(const std::string &s,
+                         std::vector<std::string> &content_types);
+
+int close_socket(socket_t sock);
+
+ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
+
+ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
+
+enum class EncodingType { None = 0, Gzip, Brotli, Zstd };
+
+EncodingType encoding_type(const Request &req, const Response &res);
+
+class BufferStream final : public Stream {
+public:
+  BufferStream() = default;
+  ~BufferStream() override = default;
+
+  bool is_readable() const override;
+  bool wait_readable() const override;
+  bool wait_writable() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  void get_local_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+  time_t duration() const override;
+
+  const std::string &get_buffer() const;
+
+private:
+  std::string buffer;
+  size_t position = 0;
+};
+
+class compressor {
+public:
+  virtual ~compressor() = default;
+
+  typedef std::function<bool(const char *data, size_t data_len)> Callback;
+  virtual bool compress(const char *data, size_t data_length, bool last,
+                        Callback callback) = 0;
+};
+
+class decompressor {
+public:
+  virtual ~decompressor() = default;
+
+  virtual bool is_valid() const = 0;
+
+  typedef std::function<bool(const char *data, size_t data_len)> Callback;
+  virtual bool decompress(const char *data, size_t data_length,
+                          Callback callback) = 0;
+};
+
+class nocompressor final : public compressor {
+public:
+  ~nocompressor() override = default;
+
+  bool compress(const char *data, size_t data_length, bool /*last*/,
+                Callback callback) override;
+};
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+class gzip_compressor final : public compressor {
+public:
+  gzip_compressor();
+  ~gzip_compressor() override;
+
+  bool compress(const char *data, size_t data_length, bool last,
+                Callback callback) override;
+
+private:
+  bool is_valid_ = false;
+  z_stream strm_;
+};
+
+class gzip_decompressor final : public decompressor {
+public:
+  gzip_decompressor();
+  ~gzip_decompressor() override;
+
+  bool is_valid() const override;
+
+  bool decompress(const char *data, size_t data_length,
+                  Callback callback) override;
+
+private:
+  bool is_valid_ = false;
+  z_stream strm_;
+};
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+class brotli_compressor final : public compressor {
+public:
+  brotli_compressor();
+  ~brotli_compressor();
+
+  bool compress(const char *data, size_t data_length, bool last,
+                Callback callback) override;
+
+private:
+  BrotliEncoderState *state_ = nullptr;
+};
+
+class brotli_decompressor final : public decompressor {
+public:
+  brotli_decompressor();
+  ~brotli_decompressor();
+
+  bool is_valid() const override;
+
+  bool decompress(const char *data, size_t data_length,
+                  Callback callback) override;
+
+private:
+  BrotliDecoderResult decoder_r;
+  BrotliDecoderState *decoder_s = nullptr;
+};
+#endif
+
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+class zstd_compressor : public compressor {
+public:
+  zstd_compressor();
+  ~zstd_compressor();
+
+  bool compress(const char *data, size_t data_length, bool last,
+                Callback callback) override;
+
+private:
+  ZSTD_CCtx *ctx_ = nullptr;
+};
+
+class zstd_decompressor : public decompressor {
+public:
+  zstd_decompressor();
+  ~zstd_decompressor();
+
+  bool is_valid() const override;
+
+  bool decompress(const char *data, size_t data_length,
+                  Callback callback) override;
+
+private:
+  ZSTD_DCtx *ctx_ = nullptr;
+};
+#endif
+
+// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer`
+// to store data. The call can set memory on stack for performance.
+class stream_line_reader {
+public:
+  stream_line_reader(Stream &strm, char *fixed_buffer,
+                     size_t fixed_buffer_size);
+  const char *ptr() const;
+  size_t size() const;
+  bool end_with_crlf() const;
+  bool getline();
+
+private:
+  void append(char c);
+
+  Stream &strm_;
+  char *fixed_buffer_;
+  const size_t fixed_buffer_size_;
+  size_t fixed_buffer_used_size_ = 0;
+  std::string growable_buffer_;
+};
+
+bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
+                    const Headers &src_headers);
+
+struct ChunkedDecoder {
+  Stream &strm;
+  size_t chunk_remaining = 0;
+  bool finished = false;
+  char line_buf[64];
+  size_t last_chunk_total = 0;
+  size_t last_chunk_offset = 0;
+
+  explicit ChunkedDecoder(Stream &s);
+
+  ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset,
+                       size_t &out_chunk_total);
+
+  bool parse_trailers_into(Headers &dest, const Headers &src_headers);
+};
+
+class mmap {
+public:
+  mmap(const char *path);
+  ~mmap();
+
+  bool open(const char *path);
+  void close();
+
+  bool is_open() const;
+  size_t size() const;
+  const char *data() const;
+
+private:
+#if defined(_WIN32)
+  HANDLE hFile_ = NULL;
+  HANDLE hMapping_ = NULL;
+#else
+  int fd_ = -1;
+#endif
+  size_t size_ = 0;
+  void *addr_ = nullptr;
+  bool is_open_empty_file = false;
+};
+
+// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
+namespace fields {
+
+bool is_token_char(char c);
+bool is_token(const std::string &s);
+bool is_field_name(const std::string &s);
+bool is_vchar(char c);
+bool is_obs_text(char c);
+bool is_field_vchar(char c);
+bool is_field_content(const std::string &s);
+bool is_field_value(const std::string &s);
+
+} // namespace fields
+} // namespace detail
+
+/*
+ * TLS Abstraction Layer Declarations
+ */
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+// TLS abstraction layer - backend-specific type declarations
+#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
+namespace tls {
+namespace impl {
+
+// Mbed TLS context wrapper (holds config, entropy, DRBG, CA chain, own
+// cert/key). This struct is accessible via tls::impl for use in SSL context
+// setup callbacks (cast ctx_t to tls::impl::MbedTlsContext*).
+struct MbedTlsContext {
+  mbedtls_ssl_config conf;
+  mbedtls_entropy_context entropy;
+  mbedtls_ctr_drbg_context ctr_drbg;
+  mbedtls_x509_crt ca_chain;
+  mbedtls_x509_crt own_cert;
+  mbedtls_pk_context own_key;
+  bool is_server = false;
+  bool verify_client = false;
+  bool has_verify_callback = false;
+
+  MbedTlsContext();
+  ~MbedTlsContext();
+
+  MbedTlsContext(const MbedTlsContext &) = delete;
+  MbedTlsContext &operator=(const MbedTlsContext &) = delete;
+};
+
+} // namespace impl
+} // namespace tls
+#endif
+
+#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT
+namespace tls {
+namespace impl {
+
+// wolfSSL context wrapper (holds WOLFSSL_CTX and related state).
+// This struct is accessible via tls::impl for use in SSL context
+// setup callbacks (cast ctx_t to tls::impl::WolfSSLContext*).
+struct WolfSSLContext {
+  WOLFSSL_CTX *ctx = nullptr;
+  bool is_server = false;
+  bool verify_client = false;
+  bool has_verify_callback = false;
+  std::string ca_pem_data_; // accumulated PEM for get_ca_names/get_ca_certs
+
+  WolfSSLContext();
+  ~WolfSSLContext();
+
+  WolfSSLContext(const WolfSSLContext &) = delete;
+  WolfSSLContext &operator=(const WolfSSLContext &) = delete;
+};
+
+// CA store for wolfSSL: holds raw PEM bytes to allow reloading into any ctx
+struct WolfSSLCAStore {
+  std::string pem_data;
+};
+
+} // namespace impl
+} // namespace tls
+#endif
+
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+namespace stream {
+
+class Result {
+public:
+  Result();
+  explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192);
+  Result(Result &&other) noexcept;
+  Result &operator=(Result &&other) noexcept;
+  Result(const Result &) = delete;
+  Result &operator=(const Result &) = delete;
+
+  // Response info
+  bool is_valid() const;
+  explicit operator bool() const;
+  int status() const;
+  const Headers &headers() const;
+  std::string get_header_value(const std::string &key,
+                               const char *def = "") const;
+  bool has_header(const std::string &key) const;
+  Error error() const;
+  Error read_error() const;
+  bool has_read_error() const;
+
+  // Stream reading
+  bool next();
+  const char *data() const;
+  size_t size() const;
+  std::string read_all();
+
+private:
+  ClientImpl::StreamHandle handle_;
+  std::string buffer_;
+  size_t current_size_ = 0;
+  size_t chunk_size_;
+  bool finished_ = false;
+};
+
+// GET
+template <typename ClientType>
+inline Result Get(ClientType &cli, const std::string &path,
+                  size_t chunk_size = 8192) {
+  return Result{cli.open_stream("GET", path), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Get(ClientType &cli, const std::string &path,
+                  const Headers &headers, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("GET", path, {}, headers), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Get(ClientType &cli, const std::string &path,
+                  const Params &params, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("GET", path, params), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Get(ClientType &cli, const std::string &path,
+                  const Params &params, const Headers &headers,
+                  size_t chunk_size = 8192) {
+  return Result{cli.open_stream("GET", path, params, headers), chunk_size};
+}
+
+// POST
+template <typename ClientType>
+inline Result Post(ClientType &cli, const std::string &path,
+                   const std::string &body, const std::string &content_type,
+                   size_t chunk_size = 8192) {
+  return Result{cli.open_stream("POST", path, {}, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Post(ClientType &cli, const std::string &path,
+                   const Headers &headers, const std::string &body,
+                   const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("POST", path, {}, headers, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Post(ClientType &cli, const std::string &path,
+                   const Params &params, const std::string &body,
+                   const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("POST", path, params, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Post(ClientType &cli, const std::string &path,
+                   const Params &params, const Headers &headers,
+                   const std::string &body, const std::string &content_type,
+                   size_t chunk_size = 8192) {
+  return Result{
+      cli.open_stream("POST", path, params, headers, body, content_type),
+      chunk_size};
+}
+
+// PUT
+template <typename ClientType>
+inline Result Put(ClientType &cli, const std::string &path,
+                  const std::string &body, const std::string &content_type,
+                  size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PUT", path, {}, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Put(ClientType &cli, const std::string &path,
+                  const Headers &headers, const std::string &body,
+                  const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PUT", path, {}, headers, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Put(ClientType &cli, const std::string &path,
+                  const Params &params, const std::string &body,
+                  const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PUT", path, params, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Put(ClientType &cli, const std::string &path,
+                  const Params &params, const Headers &headers,
+                  const std::string &body, const std::string &content_type,
+                  size_t chunk_size = 8192) {
+  return Result{
+      cli.open_stream("PUT", path, params, headers, body, content_type),
+      chunk_size};
+}
+
+// PATCH
+template <typename ClientType>
+inline Result Patch(ClientType &cli, const std::string &path,
+                    const std::string &body, const std::string &content_type,
+                    size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PATCH", path, {}, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Patch(ClientType &cli, const std::string &path,
+                    const Headers &headers, const std::string &body,
+                    const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PATCH", path, {}, headers, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Patch(ClientType &cli, const std::string &path,
+                    const Params &params, const std::string &body,
+                    const std::string &content_type, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("PATCH", path, params, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Patch(ClientType &cli, const std::string &path,
+                    const Params &params, const Headers &headers,
+                    const std::string &body, const std::string &content_type,
+                    size_t chunk_size = 8192) {
+  return Result{
+      cli.open_stream("PATCH", path, params, headers, body, content_type),
+      chunk_size};
+}
+
+// DELETE
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Headers &headers, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path, {}, headers), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const std::string &body, const std::string &content_type,
+                     size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path, {}, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Headers &headers, const std::string &body,
+                     const std::string &content_type,
+                     size_t chunk_size = 8192) {
+  return Result{
+      cli.open_stream("DELETE", path, {}, headers, body, content_type),
+      chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Params &params, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path, params), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Params &params, const Headers &headers,
+                     size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path, params, headers), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Params &params, const std::string &body,
+                     const std::string &content_type,
+                     size_t chunk_size = 8192) {
+  return Result{cli.open_stream("DELETE", path, params, {}, body, content_type),
+                chunk_size};
+}
+
+template <typename ClientType>
+inline Result Delete(ClientType &cli, const std::string &path,
+                     const Params &params, const Headers &headers,
+                     const std::string &body, const std::string &content_type,
+                     size_t chunk_size = 8192) {
+  return Result{
+      cli.open_stream("DELETE", path, params, headers, body, content_type),
+      chunk_size};
+}
+
+// HEAD
+template <typename ClientType>
+inline Result Head(ClientType &cli, const std::string &path,
+                   size_t chunk_size = 8192) {
+  return Result{cli.open_stream("HEAD", path), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Head(ClientType &cli, const std::string &path,
+                   const Headers &headers, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("HEAD", path, {}, headers), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Head(ClientType &cli, const std::string &path,
+                   const Params &params, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("HEAD", path, params), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Head(ClientType &cli, const std::string &path,
+                   const Params &params, const Headers &headers,
+                   size_t chunk_size = 8192) {
+  return Result{cli.open_stream("HEAD", path, params, headers), chunk_size};
+}
+
+// OPTIONS
+template <typename ClientType>
+inline Result Options(ClientType &cli, const std::string &path,
+                      size_t chunk_size = 8192) {
+  return Result{cli.open_stream("OPTIONS", path), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Options(ClientType &cli, const std::string &path,
+                      const Headers &headers, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("OPTIONS", path, {}, headers), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Options(ClientType &cli, const std::string &path,
+                      const Params &params, size_t chunk_size = 8192) {
+  return Result{cli.open_stream("OPTIONS", path, params), chunk_size};
+}
+
+template <typename ClientType>
+inline Result Options(ClientType &cli, const std::string &path,
+                      const Params &params, const Headers &headers,
+                      size_t chunk_size = 8192) {
+  return Result{cli.open_stream("OPTIONS", path, params, headers), chunk_size};
+}
+
+} // namespace stream
+
+namespace sse {
+
+struct SSEMessage {
+  std::string event; // Event type (default: "message")
+  std::string data;  // Event payload
+  std::string id;    // Event ID for Last-Event-ID header
+
+  SSEMessage();
+  void clear();
+};
+
+class SSEClient {
+public:
+  using MessageHandler = std::function<void(const SSEMessage &)>;
+  using ErrorHandler = std::function<void(Error)>;
+  using OpenHandler = std::function<void()>;
+
+  SSEClient(Client &client, const std::string &path);
+  SSEClient(Client &client, const std::string &path, const Headers &headers);
+  ~SSEClient();
+
+  SSEClient(const SSEClient &) = delete;
+  SSEClient &operator=(const SSEClient &) = delete;
+
+  // Event handlers
+  SSEClient &on_message(MessageHandler handler);
+  SSEClient &on_event(const std::string &type, MessageHandler handler);
+  SSEClient &on_open(OpenHandler handler);
+  SSEClient &on_error(ErrorHandler handler);
+  SSEClient &set_reconnect_interval(int ms);
+  SSEClient &set_max_reconnect_attempts(int n);
+
+  // Update headers (thread-safe)
+  SSEClient &set_headers(const Headers &headers);
+
+  // State accessors
+  bool is_connected() const;
+  const std::string &last_event_id() const;
+
+  // Blocking start - runs event loop with auto-reconnect
+  void start();
+
+  // Non-blocking start - runs in background thread
+  void start_async();
+
+  // Stop the client (thread-safe)
+  void stop();
+
+private:
+  bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
+  void run_event_loop();
+  void dispatch_event(const SSEMessage &msg);
+  bool should_reconnect(int count) const;
+  void wait_for_reconnect();
+
+  // Client and path
+  Client &client_;
+  std::string path_;
+  Headers headers_;
+  mutable std::mutex headers_mutex_;
+
+  // Callbacks
+  MessageHandler on_message_;
+  std::map<std::string, MessageHandler> event_handlers_;
+  OpenHandler on_open_;
+  ErrorHandler on_error_;
+
+  // Configuration
+  int reconnect_interval_ms_ = 3000;
+  int max_reconnect_attempts_ = 0; // 0 = unlimited
+
+  // State
+  std::atomic<bool> running_{false};
+  std::atomic<bool> connected_{false};
+  std::string last_event_id_;
+
+  // Async support
+  std::thread async_thread_;
+};
+
+} // namespace sse
+
+namespace ws {
+
+enum class Opcode : uint8_t {
+  Continuation = 0x0,
+  Text = 0x1,
+  Binary = 0x2,
+  Close = 0x8,
+  Ping = 0x9,
+  Pong = 0xA,
+};
+
+enum class CloseStatus : uint16_t {
+  Normal = 1000,
+  GoingAway = 1001,
+  ProtocolError = 1002,
+  UnsupportedData = 1003,
+  NoStatus = 1005,
+  Abnormal = 1006,
+  InvalidPayload = 1007,
+  PolicyViolation = 1008,
+  MessageTooBig = 1009,
+  MandatoryExtension = 1010,
+  InternalError = 1011,
+};
+
+enum ReadResult : int { Fail = 0, Text = 1, Binary = 2 };
+
+class WebSocket {
+public:
+  WebSocket(const WebSocket &) = delete;
+  WebSocket &operator=(const WebSocket &) = delete;
+  ~WebSocket();
+
+  ReadResult read(std::string &msg);
+  bool send(const std::string &data);
+  bool send(const char *data, size_t len);
+  void close(CloseStatus status = CloseStatus::Normal,
+             const std::string &reason = "");
+  const Request &request() const;
+  bool is_open() const;
+
+private:
+  friend class httplib::Server;
+  friend class WebSocketClient;
+
+  WebSocket(
+      Stream &strm, const Request &req, bool is_server,
+      time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND,
+      int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS)
+      : strm_(strm), req_(req), is_server_(is_server),
+        ping_interval_sec_(ping_interval_sec),
+        max_missed_pongs_(max_missed_pongs) {
+    start_heartbeat();
+  }
+
+  WebSocket(
+      std::unique_ptr<Stream> &&owned_strm, const Request &req, bool is_server,
+      time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND,
+      int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS)
+      : strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req),
+        is_server_(is_server), ping_interval_sec_(ping_interval_sec),
+        max_missed_pongs_(max_missed_pongs) {
+    start_heartbeat();
+  }
+
+  void start_heartbeat();
+  bool send_frame(Opcode op, const char *data, size_t len, bool fin = true);
+
+  Stream &strm_;
+  std::unique_ptr<Stream> owned_strm_;
+  Request req_;
+  bool is_server_;
+  time_t ping_interval_sec_;
+  int max_missed_pongs_;
+  int unacked_pings_ = 0;
+  std::atomic<bool> closed_{false};
+  std::mutex write_mutex_;
+  std::thread ping_thread_;
+  std::mutex ping_mutex_;
+  std::condition_variable ping_cv_;
+};
+
+class WebSocketClient {
+public:
+  explicit WebSocketClient(const std::string &scheme_host_port_path,
+                           const Headers &headers = {});
+
+  ~WebSocketClient();
+  WebSocketClient(const WebSocketClient &) = delete;
+  WebSocketClient &operator=(const WebSocketClient &) = delete;
+
+  bool is_valid() const;
+
+  bool connect();
+  ReadResult read(std::string &msg);
+  bool send(const std::string &data);
+  bool send(const char *data, size_t len);
+  void close(CloseStatus status = CloseStatus::Normal,
+             const std::string &reason = "");
+  bool is_open() const;
+  const std::string &subprotocol() const;
+  void set_read_timeout(time_t sec, time_t usec = 0);
+  void set_write_timeout(time_t sec, time_t usec = 0);
+  void set_websocket_ping_interval(time_t sec);
+  void set_websocket_max_missed_pongs(int count);
+  void set_tcp_nodelay(bool on);
+  void set_address_family(int family);
+  void set_ipv6_v6only(bool on);
+  void set_socket_options(SocketOptions socket_options);
+  void set_connection_timeout(time_t sec, time_t usec = 0);
+  void set_interface(const std::string &intf);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  void set_ca_cert_path(const std::string &path);
+  void set_ca_cert_store(tls::ca_store_t store);
+  void enable_server_certificate_verification(bool enabled);
+#endif
+
+private:
+  void shutdown_and_close();
+  bool create_stream(std::unique_ptr<Stream> &strm);
+
+  std::string host_;
+  int port_;
+  std::string path_;
+  Headers headers_;
+  std::string subprotocol_;
+  bool is_valid_ = false;
+  socket_t sock_ = INVALID_SOCKET;
+  std::unique_ptr<WebSocket> ws_;
+  time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND;
+  time_t read_timeout_usec_ = 0;
+  time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
+  time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
+  time_t websocket_ping_interval_sec_ =
+      CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
+  int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS;
+  int address_family_ = AF_UNSPEC;
+  bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
+  bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;
+  SocketOptions socket_options_ = nullptr;
+  time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND;
+  time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
+  std::string interface_;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  bool is_ssl_ = false;
+  tls::ctx_t tls_ctx_ = nullptr;
+  tls::session_t tls_session_ = nullptr;
+  std::string ca_cert_file_path_;
+  tls::ca_store_t ca_cert_store_ = nullptr;
+  bool server_certificate_verification_ = true;
+#endif
+};
+
+namespace impl {
+
+bool is_valid_utf8(const std::string &s);
+
+bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload,
+                          bool &fin, bool expect_masked, size_t max_len);
+
+} // namespace impl
+
+} // namespace ws
+
+// ----------------------------------------------------------------------------
+
+/*
+ * Implementation that will be part of the .cc file if split into .h + .cc.
+ */
+
+namespace stream {
+
+// stream::Result implementations
+inline Result::Result() : chunk_size_(8192) {}
+
+inline Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size)
+    : handle_(std::move(handle)), chunk_size_(chunk_size) {}
+
+inline Result::Result(Result &&other) noexcept
+    : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
+      current_size_(other.current_size_), chunk_size_(other.chunk_size_),
+      finished_(other.finished_) {
+  other.current_size_ = 0;
+  other.finished_ = true;
+}
+
+inline Result &Result::operator=(Result &&other) noexcept {
+  if (this != &other) {
+    handle_ = std::move(other.handle_);
+    buffer_ = std::move(other.buffer_);
+    current_size_ = other.current_size_;
+    chunk_size_ = other.chunk_size_;
+    finished_ = other.finished_;
+    other.current_size_ = 0;
+    other.finished_ = true;
+  }
+  return *this;
+}
+
+inline bool Result::is_valid() const { return handle_.is_valid(); }
+inline Result::operator bool() const { return is_valid(); }
+
+inline int Result::status() const {
+  return handle_.response ? handle_.response->status : -1;
+}
+
+inline const Headers &Result::headers() const {
+  static const Headers empty_headers;
+  return handle_.response ? handle_.response->headers : empty_headers;
+}
+
+inline std::string Result::get_header_value(const std::string &key,
+                                            const char *def) const {
+  return handle_.response ? handle_.response->get_header_value(key, def) : def;
+}
+
+inline bool Result::has_header(const std::string &key) const {
+  return handle_.response ? handle_.response->has_header(key) : false;
+}
+
+inline Error Result::error() const { return handle_.error; }
+inline Error Result::read_error() const { return handle_.get_read_error(); }
+inline bool Result::has_read_error() const { return handle_.has_read_error(); }
+
+inline bool Result::next() {
+  if (!handle_.is_valid() || finished_) { return false; }
+
+  if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
+
+  ssize_t n = handle_.read(&buffer_[0], chunk_size_);
+  if (n > 0) {
+    current_size_ = static_cast<size_t>(n);
+    return true;
+  }
+
+  current_size_ = 0;
+  finished_ = true;
+  return false;
+}
+
+inline const char *Result::data() const { return buffer_.data(); }
+inline size_t Result::size() const { return current_size_; }
+
+inline std::string Result::read_all() {
+  std::string result;
+  while (next()) {
+    result.append(data(), size());
+  }
+  return result;
+}
+
+} // namespace stream
+
+namespace sse {
+
+// SSEMessage implementations
+inline SSEMessage::SSEMessage() : event("message") {}
+
+inline void SSEMessage::clear() {
+  event = "message";
+  data.clear();
+  id.clear();
+}
+
+// SSEClient implementations
+inline SSEClient::SSEClient(Client &client, const std::string &path)
+    : client_(client), path_(path) {}
+
+inline SSEClient::SSEClient(Client &client, const std::string &path,
+                            const Headers &headers)
+    : client_(client), path_(path), headers_(headers) {}
+
+inline SSEClient::~SSEClient() { stop(); }
+
+inline SSEClient &SSEClient::on_message(MessageHandler handler) {
+  on_message_ = std::move(handler);
+  return *this;
+}
+
+inline SSEClient &SSEClient::on_event(const std::string &type,
+                                      MessageHandler handler) {
+  event_handlers_[type] = std::move(handler);
+  return *this;
+}
+
+inline SSEClient &SSEClient::on_open(OpenHandler handler) {
+  on_open_ = std::move(handler);
+  return *this;
+}
+
+inline SSEClient &SSEClient::on_error(ErrorHandler handler) {
+  on_error_ = std::move(handler);
+  return *this;
+}
+
+inline SSEClient &SSEClient::set_reconnect_interval(int ms) {
+  reconnect_interval_ms_ = ms;
+  return *this;
+}
+
+inline SSEClient &SSEClient::set_max_reconnect_attempts(int n) {
+  max_reconnect_attempts_ = n;
+  return *this;
+}
+
+inline SSEClient &SSEClient::set_headers(const Headers &headers) {
+  std::lock_guard<std::mutex> lock(headers_mutex_);
+  headers_ = headers;
+  return *this;
+}
+
+inline bool SSEClient::is_connected() const { return connected_.load(); }
+
+inline const std::string &SSEClient::last_event_id() const {
+  return last_event_id_;
+}
+
+inline void SSEClient::start() {
+  running_.store(true);
+  run_event_loop();
+}
+
+inline void SSEClient::start_async() {
+  running_.store(true);
+  async_thread_ = std::thread([this]() { run_event_loop(); });
+}
+
+inline void SSEClient::stop() {
+  running_.store(false);
+  client_.stop(); // Cancel any pending operations
+  if (async_thread_.joinable()) { async_thread_.join(); }
+}
+
+inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
+                                      int &retry_ms) {
+  // Blank line signals end of event
+  if (line.empty() || line == "\r") { return true; }
+
+  // Lines starting with ':' are comments (ignored)
+  if (!line.empty() && line[0] == ':') { return false; }
+
+  // Find the colon separator
+  auto colon_pos = line.find(':');
+  if (colon_pos == std::string::npos) {
+    // Line with no colon is treated as field name with empty value
+    return false;
+  }
+
+  auto field = line.substr(0, colon_pos);
+  std::string value;
+
+  // Value starts after colon, skip optional single space
+  if (colon_pos + 1 < line.size()) {
+    auto value_start = colon_pos + 1;
+    if (line[value_start] == ' ') { value_start++; }
+    value = line.substr(value_start);
+    // Remove trailing \r if present
+    if (!value.empty() && value.back() == '\r') { value.pop_back(); }
+  }
+
+  // Handle known fields
+  if (field == "event") {
+    msg.event = value;
+  } else if (field == "data") {
+    // Multiple data lines are concatenated with newlines
+    if (!msg.data.empty()) { msg.data += "\n"; }
+    msg.data += value;
+  } else if (field == "id") {
+    // Empty id is valid (clears the last event ID)
+    msg.id = value;
+  } else if (field == "retry") {
+    // Parse retry interval in milliseconds
+    {
+      int v = 0;
+      auto res =
+          detail::from_chars(value.data(), value.data() + value.size(), v);
+      if (res.ec == std::errc{}) { retry_ms = v; }
+    }
+  }
+  // Unknown fields are ignored per SSE spec
+
+  return false;
+}
+
+inline void SSEClient::run_event_loop() {
+  auto reconnect_count = 0;
+
+  while (running_.load()) {
+    // Build headers, including Last-Event-ID if we have one
+    Headers request_headers;
+    {
+      std::lock_guard<std::mutex> lock(headers_mutex_);
+      request_headers = headers_;
+    }
+    if (!last_event_id_.empty()) {
+      request_headers.emplace("Last-Event-ID", last_event_id_);
+    }
+
+    // Open streaming connection
+    auto result = stream::Get(client_, path_, request_headers);
+
+    // Connection error handling
+    if (!result) {
+      connected_.store(false);
+      if (on_error_) { on_error_(result.error()); }
+
+      if (!should_reconnect(reconnect_count)) { break; }
+      wait_for_reconnect();
+      reconnect_count++;
+      continue;
+    }
+
+    if (result.status() != StatusCode::OK_200) {
+      connected_.store(false);
+      if (on_error_) { on_error_(Error::Connection); }
+
+      // For certain errors, don't reconnect.
+      // Note: 401 is intentionally absent so that handlers can refresh
+      // credentials via set_headers() and let the client reconnect.
+      if (result.status() == StatusCode::NoContent_204 ||
+          result.status() == StatusCode::NotFound_404 ||
+          result.status() == StatusCode::Forbidden_403) {
+        break;
+      }
+
+      if (!should_reconnect(reconnect_count)) { break; }
+      wait_for_reconnect();
+      reconnect_count++;
+      continue;
+    }
+
+    // Connection successful
+    connected_.store(true);
+    reconnect_count = 0;
+    if (on_open_) { on_open_(); }
+
+    // Event receiving loop
+    std::string buffer;
+    SSEMessage current_msg;
+
+    while (running_.load() && result.next()) {
+      buffer.append(result.data(), result.size());
+
+      // Process complete lines in the buffer
+      size_t line_start = 0;
+      size_t newline_pos;
+
+      while ((newline_pos = buffer.find('\n', line_start)) !=
+             std::string::npos) {
+        auto line = buffer.substr(line_start, newline_pos - line_start);
+        line_start = newline_pos + 1;
+
+        // Parse the line and check if event is complete
+        auto event_complete =
+            parse_sse_line(line, current_msg, reconnect_interval_ms_);
+
+        if (event_complete && !current_msg.data.empty()) {
+          // Update last_event_id for reconnection
+          if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
+
+          // Dispatch event to appropriate handler
+          dispatch_event(current_msg);
+
+          current_msg.clear();
+        }
+      }
+
+      // Keep unprocessed data in buffer
+      buffer.erase(0, line_start);
+    }
+
+    // Connection ended
+    connected_.store(false);
+
+    if (!running_.load()) { break; }
+
+    // Check for read errors
+    if (result.has_read_error()) {
+      if (on_error_) { on_error_(result.read_error()); }
+    }
+
+    if (!should_reconnect(reconnect_count)) { break; }
+    wait_for_reconnect();
+    reconnect_count++;
+  }
+
+  connected_.store(false);
+}
+
+inline void SSEClient::dispatch_event(const SSEMessage &msg) {
+  // Check for specific event type handler first
+  auto it = event_handlers_.find(msg.event);
+  if (it != event_handlers_.end()) {
+    it->second(msg);
+    return;
+  }
+
+  // Fall back to generic message handler
+  if (on_message_) { on_message_(msg); }
+}
+
+inline bool SSEClient::should_reconnect(int count) const {
+  if (!running_.load()) { return false; }
+  if (max_reconnect_attempts_ == 0) { return true; } // unlimited
+  return count < max_reconnect_attempts_;
+}
+
+inline void SSEClient::wait_for_reconnect() {
+  // Use small increments to check running_ flag frequently
+  auto waited = 0;
+  while (running_.load() && waited < reconnect_interval_ms_) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    waited += 100;
+  }
+}
+
+} // namespace sse
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+/*
+ * TLS abstraction layer - internal function declarations
+ * These are implementation details and not part of the public API.
+ */
+namespace tls {
+
+// Client context
+ctx_t create_client_context();
+void free_context(ctx_t ctx);
+bool set_min_version(ctx_t ctx, Version version);
+bool load_ca_pem(ctx_t ctx, const char *pem, size_t len);
+bool load_ca_file(ctx_t ctx, const char *file_path);
+bool load_ca_dir(ctx_t ctx, const char *dir_path);
+bool load_system_certs(ctx_t ctx);
+bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                         const char *password);
+bool set_client_cert_file(ctx_t ctx, const char *cert_path,
+                          const char *key_path, const char *password);
+
+// Server context
+ctx_t create_server_context();
+bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                         const char *password);
+bool set_server_cert_file(ctx_t ctx, const char *cert_path,
+                          const char *key_path, const char *password);
+bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir);
+void set_verify_client(ctx_t ctx, bool require);
+
+// Session management
+session_t create_session(ctx_t ctx, socket_t sock);
+void free_session(session_t session);
+bool set_sni(session_t session, const char *hostname);
+bool set_hostname(session_t session, const char *hostname);
+
+// Handshake (non-blocking capable)
+TlsError connect(session_t session);
+TlsError accept(session_t session);
+
+// Handshake with timeout (blocking until timeout)
+bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
+                         time_t timeout_usec, TlsError *err);
+bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
+                        time_t timeout_usec, TlsError *err);
+
+// I/O (non-blocking capable)
+ssize_t read(session_t session, void *buf, size_t len, TlsError &err);
+ssize_t write(session_t session, const void *buf, size_t len, TlsError &err);
+int pending(const_session_t session);
+void shutdown(session_t session, bool graceful);
+
+// Connection state
+bool is_peer_closed(session_t session, socket_t sock);
+
+// Certificate verification
+cert_t get_peer_cert(const_session_t session);
+void free_cert(cert_t cert);
+bool verify_hostname(cert_t cert, const char *hostname);
+uint64_t hostname_mismatch_code();
+long get_verify_result(const_session_t session);
+
+// Certificate introspection
+std::string get_cert_subject_cn(cert_t cert);
+std::string get_cert_issuer_name(cert_t cert);
+bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans);
+bool get_cert_validity(cert_t cert, time_t &not_before, time_t &not_after);
+std::string get_cert_serial(cert_t cert);
+bool get_cert_der(cert_t cert, std::vector<unsigned char> &der);
+const char *get_sni(const_session_t session);
+
+// CA store management
+ca_store_t create_ca_store(const char *pem, size_t len);
+void free_ca_store(ca_store_t store);
+bool set_ca_store(ctx_t ctx, ca_store_t store);
+size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs);
+std::vector<std::string> get_ca_names(ctx_t ctx);
+
+// Dynamic certificate update (for servers)
+bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem,
+                        const char *password);
+bool update_server_client_ca(ctx_t ctx, const char *ca_pem);
+
+// Certificate verification callback
+bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
+long get_verify_error(const_session_t session);
+std::string verify_error_string(long error_code);
+
+// TlsError information
+uint64_t peek_error();
+uint64_t get_error();
+std::string error_string(uint64_t code);
+
+} // namespace tls
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 1: detail namespace - Non-SSL utilities
+ */
+
+namespace detail {
+
+inline bool set_socket_opt_impl(socket_t sock, int level, int optname,
+                                const void *optval, socklen_t optlen) {
+  return setsockopt(sock, level, optname,
+#ifdef _WIN32
+                    reinterpret_cast<const char *>(optval),
+#else
+                    optval,
+#endif
+                    optlen) == 0;
+}
+
+inline bool set_socket_opt_time(socket_t sock, int level, int optname,
+                                time_t sec, time_t usec) {
+#ifdef _WIN32
+  auto timeout = static_cast<uint32_t>(sec * 1000 + usec / 1000);
+#else
+  timeval timeout;
+  timeout.tv_sec = static_cast<long>(sec);
+  timeout.tv_usec = static_cast<decltype(timeout.tv_usec)>(usec);
+#endif
+  return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout));
+}
+
+inline bool is_hex(char c, int &v) {
+  if (isdigit(c)) {
+    v = c - '0';
+    return true;
+  } else if ('A' <= c && c <= 'F') {
+    v = c - 'A' + 10;
+    return true;
+  } else if ('a' <= c && c <= 'f') {
+    v = c - 'a' + 10;
+    return true;
+  }
+  return false;
+}
+
+inline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt,
+                          int &val) {
+  if (i >= s.size()) { return false; }
+
+  val = 0;
+  for (; cnt; i++, cnt--) {
+    if (!s[i]) { return false; }
+    auto v = 0;
+    if (is_hex(s[i], v)) {
+      val = val * 16 + v;
+    } else {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline std::string from_i_to_hex(size_t n) {
+  static const auto charset = "0123456789abcdef";
+  std::string ret;
+  do {
+    ret = charset[n & 15] + ret;
+    n >>= 4;
+  } while (n > 0);
+  return ret;
+}
+
+inline std::string compute_etag(const FileStat &fs) {
+  if (!fs.is_file()) { return std::string(); }
+
+  // If mtime cannot be determined (negative value indicates an error
+  // or sentinel), do not generate an ETag. Returning a neutral / fixed
+  // value like 0 could collide with a real file that legitimately has
+  // mtime == 0 (epoch) and lead to misleading validators.
+  auto mtime_raw = fs.mtime();
+  if (mtime_raw < 0) { return std::string(); }
+
+  auto mtime = static_cast<size_t>(mtime_raw);
+  auto size = fs.size();
+
+  return std::string("W/\"") + from_i_to_hex(mtime) + "-" +
+         from_i_to_hex(size) + "\"";
+}
+
+// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994
+// 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks
+// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds.
+inline std::string file_mtime_to_http_date(time_t mtime) {
+  if (mtime < 0) { return std::string(); }
+
+  struct tm tm_buf;
+#ifdef _WIN32
+  if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); }
+#else
+  if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); }
+#endif
+  char buf[64];
+  if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) {
+    return std::string();
+  }
+
+  return std::string(buf);
+}
+
+// Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure.
+inline time_t parse_http_date(const std::string &date_str) {
+  struct tm tm_buf;
+
+  // Create a classic locale object once for all parsing attempts
+  const std::locale classic_locale = std::locale::classic();
+
+  // Try to parse using std::get_time (C++11, cross-platform)
+  auto try_parse = [&](const char *fmt) -> bool {
+    std::istringstream ss(date_str);
+    ss.imbue(classic_locale);
+
+    memset(&tm_buf, 0, sizeof(tm_buf));
+    ss >> std::get_time(&tm_buf, fmt);
+
+    return !ss.fail();
+  };
+
+  // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT"
+  if (!try_parse("%a, %d %b %Y %H:%M:%S")) {
+    // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT"
+    if (!try_parse("%A, %d-%b-%y %H:%M:%S")) {
+      // asctime format: "Sun Nov  6 08:49:37 1994"
+      if (!try_parse("%a %b %d %H:%M:%S %Y")) {
+        return static_cast<time_t>(-1);
+      }
+    }
+  }
+
+#ifdef _WIN32
+  return _mkgmtime(&tm_buf);
+#elif defined _AIX
+  return mktime(&tm_buf);
+#else
+  return timegm(&tm_buf);
+#endif
+}
+
+inline bool is_weak_etag(const std::string &s) {
+  // Check if the string is a weak ETag (starts with 'W/"')
+  return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"';
+}
+
+inline bool is_strong_etag(const std::string &s) {
+  // Check if the string is a strong ETag (starts and ends with '"', at least 2
+  // chars)
+  return s.size() >= 2 && s[0] == '"' && s.back() == '"';
+}
+
+inline size_t to_utf8(int code, char *buff) {
+  if (code < 0x0080) {
+    buff[0] = static_cast<char>(code & 0x7F);
+    return 1;
+  } else if (code < 0x0800) {
+    buff[0] = static_cast<char>(0xC0 | ((code >> 6) & 0x1F));
+    buff[1] = static_cast<char>(0x80 | (code & 0x3F));
+    return 2;
+  } else if (code < 0xD800) {
+    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));
+    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | (code & 0x3F));
+    return 3;
+  } else if (code < 0xE000) { // D800 - DFFF is invalid...
+    return 0;
+  } else if (code < 0x10000) {
+    buff[0] = static_cast<char>(0xE0 | ((code >> 12) & 0xF));
+    buff[1] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | (code & 0x3F));
+    return 3;
+  } else if (code < 0x110000) {
+    buff[0] = static_cast<char>(0xF0 | ((code >> 18) & 0x7));
+    buff[1] = static_cast<char>(0x80 | ((code >> 12) & 0x3F));
+    buff[2] = static_cast<char>(0x80 | ((code >> 6) & 0x3F));
+    buff[3] = static_cast<char>(0x80 | (code & 0x3F));
+    return 4;
+  }
+
+  // NOTREACHED
+  return 0;
+}
+
+} // namespace detail
+
+namespace ws {
+namespace impl {
+
+inline bool is_valid_utf8(const std::string &s) {
+  size_t i = 0;
+  auto n = s.size();
+  while (i < n) {
+    auto c = static_cast<unsigned char>(s[i]);
+    size_t len;
+    uint32_t cp;
+    if (c < 0x80) {
+      i++;
+      continue;
+    } else if ((c & 0xE0) == 0xC0) {
+      len = 2;
+      cp = c & 0x1F;
+    } else if ((c & 0xF0) == 0xE0) {
+      len = 3;
+      cp = c & 0x0F;
+    } else if ((c & 0xF8) == 0xF0) {
+      len = 4;
+      cp = c & 0x07;
+    } else {
+      return false;
+    }
+    if (i + len > n) { return false; }
+    for (size_t j = 1; j < len; j++) {
+      auto b = static_cast<unsigned char>(s[i + j]);
+      if ((b & 0xC0) != 0x80) { return false; }
+      cp = (cp << 6) | (b & 0x3F);
+    }
+    // Overlong encoding check
+    if (len == 2 && cp < 0x80) { return false; }
+    if (len == 3 && cp < 0x800) { return false; }
+    if (len == 4 && cp < 0x10000) { return false; }
+    // Surrogate halves (U+D800..U+DFFF) and beyond U+10FFFF are invalid
+    if (cp >= 0xD800 && cp <= 0xDFFF) { return false; }
+    if (cp > 0x10FFFF) { return false; }
+    i += len;
+  }
+  return true;
+}
+
+} // namespace impl
+} // namespace ws
+
+namespace detail {
+
+// NOTE: This code came up with the following stackoverflow post:
+// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c
+inline std::string base64_encode(const std::string &in) {
+  static const auto lookup =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+  std::string out;
+  out.reserve(in.size());
+
+  auto val = 0;
+  auto valb = -6;
+
+  for (auto c : in) {
+    val = (val << 8) + static_cast<uint8_t>(c);
+    valb += 8;
+    while (valb >= 0) {
+      out.push_back(lookup[(val >> valb) & 0x3F]);
+      valb -= 6;
+    }
+  }
+
+  if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
+
+  while (out.size() % 4) {
+    out.push_back('=');
+  }
+
+  return out;
+}
+
+inline std::string sha1(const std::string &input) {
+  // RFC 3174 SHA-1 implementation
+  auto left_rotate = [](uint32_t x, uint32_t n) -> uint32_t {
+    return (x << n) | (x >> (32 - n));
+  };
+
+  uint32_t h0 = 0x67452301;
+  uint32_t h1 = 0xEFCDAB89;
+  uint32_t h2 = 0x98BADCFE;
+  uint32_t h3 = 0x10325476;
+  uint32_t h4 = 0xC3D2E1F0;
+
+  // Pre-processing: adding padding bits
+  std::string msg = input;
+  uint64_t original_bit_len = static_cast<uint64_t>(msg.size()) * 8;
+  msg.push_back(static_cast<char>(0x80));
+  while (msg.size() % 64 != 56) {
+    msg.push_back(0);
+  }
+
+  // Append original length in bits as 64-bit big-endian
+  for (int i = 56; i >= 0; i -= 8) {
+    msg.push_back(static_cast<char>((original_bit_len >> i) & 0xFF));
+  }
+
+  // Process each 512-bit chunk
+  for (size_t offset = 0; offset < msg.size(); offset += 64) {
+    uint32_t w[80];
+
+    for (size_t i = 0; i < 16; i++) {
+      w[i] =
+          (static_cast<uint32_t>(static_cast<uint8_t>(msg[offset + i * 4]))
+           << 24) |
+          (static_cast<uint32_t>(static_cast<uint8_t>(msg[offset + i * 4 + 1]))
+           << 16) |
+          (static_cast<uint32_t>(static_cast<uint8_t>(msg[offset + i * 4 + 2]))
+           << 8) |
+          (static_cast<uint32_t>(
+              static_cast<uint8_t>(msg[offset + i * 4 + 3])));
+    }
+
+    for (int i = 16; i < 80; i++) {
+      w[i] = left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
+    }
+
+    uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;
+
+    for (int i = 0; i < 80; i++) {
+      uint32_t f, k;
+      if (i < 20) {
+        f = (b & c) | ((~b) & d);
+        k = 0x5A827999;
+      } else if (i < 40) {
+        f = b ^ c ^ d;
+        k = 0x6ED9EBA1;
+      } else if (i < 60) {
+        f = (b & c) | (b & d) | (c & d);
+        k = 0x8F1BBCDC;
+      } else {
+        f = b ^ c ^ d;
+        k = 0xCA62C1D6;
+      }
+
+      uint32_t temp = left_rotate(a, 5) + f + e + k + w[i];
+      e = d;
+      d = c;
+      c = left_rotate(b, 30);
+      b = a;
+      a = temp;
+    }
+
+    h0 += a;
+    h1 += b;
+    h2 += c;
+    h3 += d;
+    h4 += e;
+  }
+
+  // Produce the final hash as a 20-byte binary string
+  std::string hash(20, '\0');
+  for (size_t i = 0; i < 4; i++) {
+    hash[i] = static_cast<char>((h0 >> (24 - i * 8)) & 0xFF);
+    hash[4 + i] = static_cast<char>((h1 >> (24 - i * 8)) & 0xFF);
+    hash[8 + i] = static_cast<char>((h2 >> (24 - i * 8)) & 0xFF);
+    hash[12 + i] = static_cast<char>((h3 >> (24 - i * 8)) & 0xFF);
+    hash[16 + i] = static_cast<char>((h4 >> (24 - i * 8)) & 0xFF);
+  }
+  return hash;
+}
+
+inline std::string websocket_accept_key(const std::string &client_key) {
+  const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+  return base64_encode(sha1(client_key + magic));
+}
+
+inline bool is_websocket_upgrade(const Request &req) {
+  if (req.method != "GET") { return false; }
+
+  // Check Upgrade: websocket (case-insensitive)
+  auto upgrade_it = req.headers.find("Upgrade");
+  if (upgrade_it == req.headers.end()) { return false; }
+  auto upgrade_val = case_ignore::to_lower(upgrade_it->second);
+  if (upgrade_val != "websocket") { return false; }
+
+  // Check Connection header contains "Upgrade"
+  auto connection_it = req.headers.find("Connection");
+  if (connection_it == req.headers.end()) { return false; }
+  auto connection_val = case_ignore::to_lower(connection_it->second);
+  if (connection_val.find("upgrade") == std::string::npos) { return false; }
+
+  // Check Sec-WebSocket-Key is a valid base64-encoded 16-byte value (24 chars)
+  // RFC 6455 Section 4.2.1
+  auto ws_key = req.get_header_value("Sec-WebSocket-Key");
+  if (ws_key.size() != 24 || ws_key[22] != '=' || ws_key[23] != '=') {
+    return false;
+  }
+  static const std::string b64chars =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  for (size_t i = 0; i < 22; i++) {
+    if (b64chars.find(ws_key[i]) == std::string::npos) { return false; }
+  }
+
+  // Check Sec-WebSocket-Version: 13
+  auto version = req.get_header_value("Sec-WebSocket-Version");
+  if (version != "13") { return false; }
+
+  return true;
+}
+
+inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
+                                  const char *data, size_t len, bool fin,
+                                  bool mask) {
+  // First byte: FIN + opcode
+  uint8_t header[2];
+  header[0] = static_cast<uint8_t>((fin ? 0x80 : 0x00) |
+                                   (static_cast<uint8_t>(opcode) & 0x0F));
+
+  // Second byte: MASK + payload length
+  if (len < 126) {
+    header[1] = static_cast<uint8_t>(len);
+    if (mask) { header[1] |= 0x80; }
+    if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+  } else if (len <= 0xFFFF) {
+    header[1] = 126;
+    if (mask) { header[1] |= 0x80; }
+    if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+    uint8_t ext[2];
+    ext[0] = static_cast<uint8_t>((len >> 8) & 0xFF);
+    ext[1] = static_cast<uint8_t>(len & 0xFF);
+    if (strm.write(reinterpret_cast<char *>(ext), 2) < 0) { return false; }
+  } else {
+    header[1] = 127;
+    if (mask) { header[1] |= 0x80; }
+    if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+    uint8_t ext[8];
+    for (int i = 7; i >= 0; i--) {
+      ext[7 - i] =
+          static_cast<uint8_t>((static_cast<uint64_t>(len) >> (i * 8)) & 0xFF);
+    }
+    if (strm.write(reinterpret_cast<char *>(ext), 8) < 0) { return false; }
+  }
+
+  if (mask) {
+    // Generate random mask key
+    thread_local std::mt19937 rng(std::random_device{}());
+    uint8_t mask_key[4];
+    auto r = rng();
+    std::memcpy(mask_key, &r, 4);
+    if (strm.write(reinterpret_cast<char *>(mask_key), 4) < 0) { return false; }
+
+    // Write masked payload in chunks
+    const size_t chunk_size = 4096;
+    std::vector<char> buf((std::min)(len, chunk_size));
+    for (size_t offset = 0; offset < len; offset += chunk_size) {
+      size_t n = (std::min)(chunk_size, len - offset);
+      for (size_t i = 0; i < n; i++) {
+        buf[i] =
+            data[offset + i] ^ static_cast<char>(mask_key[(offset + i) % 4]);
+      }
+      if (strm.write(buf.data(), n) < 0) { return false; }
+    }
+  } else {
+    if (len > 0) {
+      if (strm.write(data, len) < 0) { return false; }
+    }
+  }
+
+  return true;
+}
+
+} // namespace detail
+
+namespace ws {
+namespace impl {
+
+inline bool read_websocket_frame(Stream &strm, Opcode &opcode,
+                                 std::string &payload, bool &fin,
+                                 bool expect_masked, size_t max_len) {
+  // Read first 2 bytes
+  uint8_t header[2];
+  if (strm.read(reinterpret_cast<char *>(header), 2) != 2) { return false; }
+
+  fin = (header[0] & 0x80) != 0;
+
+  // RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
+  if (header[0] & 0x70) { return false; }
+
+  opcode = static_cast<Opcode>(header[0] & 0x0F);
+  bool masked = (header[1] & 0x80) != 0;
+  uint64_t payload_len = header[1] & 0x7F;
+
+  // RFC 6455 Section 5.5: control frames MUST NOT be fragmented and
+  // MUST have a payload length of 125 bytes or less
+  bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0;
+  if (is_control) {
+    if (!fin) { return false; }
+    if (payload_len > 125) { return false; }
+  }
+
+  if (masked != expect_masked) { return false; }
+
+  // Extended payload length
+  if (payload_len == 126) {
+    uint8_t ext[2];
+    if (strm.read(reinterpret_cast<char *>(ext), 2) != 2) { return false; }
+    payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
+  } else if (payload_len == 127) {
+    uint8_t ext[8];
+    if (strm.read(reinterpret_cast<char *>(ext), 8) != 8) { return false; }
+    // RFC 6455 Section 5.2: the most significant bit MUST be 0
+    if (ext[0] & 0x80) { return false; }
+    payload_len = 0;
+    for (int i = 0; i < 8; i++) {
+      payload_len = (payload_len << 8) | ext[i];
+    }
+  }
+
+  if (payload_len > max_len) { return false; }
+
+  // Read mask key if present
+  uint8_t mask_key[4] = {0};
+  if (masked) {
+    if (strm.read(reinterpret_cast<char *>(mask_key), 4) != 4) { return false; }
+  }
+
+  // Read payload
+  payload.resize(static_cast<size_t>(payload_len));
+  if (payload_len > 0) {
+    size_t total_read = 0;
+    while (total_read < payload_len) {
+      auto n = strm.read(&payload[total_read],
+                         static_cast<size_t>(payload_len - total_read));
+      if (n <= 0) { return false; }
+      total_read += static_cast<size_t>(n);
+    }
+  }
+
+  // Unmask if needed
+  if (masked) {
+    for (size_t i = 0; i < payload.size(); i++) {
+      payload[i] ^= static_cast<char>(mask_key[i % 4]);
+    }
+  }
+
+  return true;
+}
+
+} // namespace impl
+} // namespace ws
+
+namespace detail {
+
+inline bool is_valid_path(const std::string &path) {
+  size_t level = 0;
+  size_t i = 0;
+
+  // Skip slash
+  while (i < path.size() && path[i] == '/') {
+    i++;
+  }
+
+  while (i < path.size()) {
+    // Read component
+    auto beg = i;
+    while (i < path.size() && path[i] != '/') {
+      if (path[i] == '\0') {
+        return false;
+      } else if (path[i] == '\\') {
+        return false;
+      }
+      i++;
+    }
+
+    auto len = i - beg;
+    assert(len > 0);
+
+    if (!path.compare(beg, len, ".")) {
+      ;
+    } else if (!path.compare(beg, len, "..")) {
+      if (level == 0) { return false; }
+      level--;
+    } else {
+      level++;
+    }
+
+    // Skip slash
+    while (i < path.size() && path[i] == '/') {
+      i++;
+    }
+  }
+
+  return true;
+}
+
+inline bool canonicalize_path(const char *path, std::string &resolved) {
+#if defined(_WIN32)
+  char buf[_MAX_PATH];
+  if (_fullpath(buf, path, _MAX_PATH) == nullptr) { return false; }
+  resolved = buf;
+#elif defined(PATH_MAX)
+  char buf[PATH_MAX];
+  if (realpath(path, buf) == nullptr) { return false; }
+  resolved = buf;
+#else
+  auto buf = realpath(path, nullptr);
+  auto guard = scope_exit([&]() { std::free(buf); });
+  if (buf == nullptr) { return false; }
+  resolved = buf;
+#endif
+  return true;
+}
+
+inline bool is_path_within_base(const std::string &resolved_path,
+                                const std::string &resolved_base) {
+#if defined(_WIN32)
+  return _strnicmp(resolved_path.c_str(), resolved_base.c_str(),
+                   resolved_base.size()) == 0;
+#else
+  return strncmp(resolved_path.c_str(), resolved_base.c_str(),
+                 resolved_base.size()) == 0;
+#endif
+}
+
+inline FileStat::FileStat(const std::string &path) {
+#if defined(_WIN32)
+  auto wpath = u8string_to_wstring(path.c_str());
+  ret_ = _wstat(wpath.c_str(), &st_);
+#else
+  ret_ = stat(path.c_str(), &st_);
+#endif
+}
+inline bool FileStat::is_file() const {
+  return ret_ >= 0 && S_ISREG(st_.st_mode);
+}
+inline bool FileStat::is_dir() const {
+  return ret_ >= 0 && S_ISDIR(st_.st_mode);
+}
+
+inline time_t FileStat::mtime() const {
+  return ret_ >= 0 ? static_cast<time_t>(st_.st_mtime)
+                   : static_cast<time_t>(-1);
+}
+
+inline size_t FileStat::size() const {
+  return ret_ >= 0 ? static_cast<size_t>(st_.st_size) : 0;
+}
+
+inline std::string encode_path(const std::string &s) {
+  std::string result;
+  result.reserve(s.size());
+
+  for (size_t i = 0; s[i]; i++) {
+    switch (s[i]) {
+    case ' ': result += "%20"; break;
+    case '+': result += "%2B"; break;
+    case '\r': result += "%0D"; break;
+    case '\n': result += "%0A"; break;
+    case '\'': result += "%27"; break;
+    case ',': result += "%2C"; break;
+    // case ':': result += "%3A"; break; // ok? probably...
+    case ';': result += "%3B"; break;
+    default:
+      auto c = static_cast<uint8_t>(s[i]);
+      if (c >= 0x80) {
+        result += '%';
+        char hex[4];
+        auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
+        assert(len == 2);
+        result.append(hex, static_cast<size_t>(len));
+      } else {
+        result += s[i];
+      }
+      break;
+    }
+  }
+
+  return result;
+}
+
+inline std::string file_extension(const std::string &path) {
+  std::smatch m;
+  thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$");
+  if (std::regex_search(path, m, re)) { return m[1].str(); }
+  return std::string();
+}
+
+inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; }
+
+template <typename T>
+inline bool parse_header(const char *beg, const char *end, T fn);
+
+template <typename T>
+inline bool parse_header(const char *beg, const char *end, T fn) {
+  // Skip trailing spaces and tabs.
+  while (beg < end && is_space_or_tab(end[-1])) {
+    end--;
+  }
+
+  auto p = beg;
+  while (p < end && *p != ':') {
+    p++;
+  }
+
+  auto name = std::string(beg, p);
+  if (!detail::fields::is_field_name(name)) { return false; }
+
+  if (p == end) { return false; }
+
+  auto key_end = p;
+
+  if (*p++ != ':') { return false; }
+
+  while (p < end && is_space_or_tab(*p)) {
+    p++;
+  }
+
+  if (p <= end) {
+    auto key_len = key_end - beg;
+    if (!key_len) { return false; }
+
+    auto key = std::string(beg, key_end);
+    auto val = std::string(p, end);
+
+    if (!detail::fields::is_field_value(val)) { return false; }
+
+    if (case_ignore::equal(key, "Location") ||
+        case_ignore::equal(key, "Referer")) {
+      fn(key, val);
+    } else {
+      fn(key, decode_path_component(val));
+    }
+
+    return true;
+  }
+
+  return false;
+}
+
+inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
+                           const Headers &src_headers) {
+  // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked
+  // transfer coding is complete when a chunk with a chunk-size of zero is
+  // received, possibly followed by a trailer section, and finally terminated by
+  // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1
+  //
+  // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section
+  // doesn't care for the existence of the final CRLF. In other words, it seems
+  // to be ok whether the final CRLF exists or not in the chunked data.
+  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3
+  //
+  // According to the reference code in RFC 9112, cpp-httplib now allows
+  // chunked transfer coding data without the final CRLF.
+
+  // RFC 7230 Section 4.1.2 - Headers prohibited in trailers
+  thread_local case_ignore::unordered_set<std::string> prohibited_trailers = {
+      "transfer-encoding",
+      "content-length",
+      "host",
+      "authorization",
+      "www-authenticate",
+      "proxy-authenticate",
+      "proxy-authorization",
+      "cookie",
+      "set-cookie",
+      "cache-control",
+      "expect",
+      "max-forwards",
+      "pragma",
+      "range",
+      "te",
+      "age",
+      "expires",
+      "date",
+      "location",
+      "retry-after",
+      "vary",
+      "warning",
+      "content-encoding",
+      "content-type",
+      "content-range",
+      "trailer"};
+
+  case_ignore::unordered_set<std::string> declared_trailers;
+  auto trailer_header = get_header_value(src_headers, "Trailer", "", 0);
+  if (trailer_header && std::strlen(trailer_header)) {
+    auto len = std::strlen(trailer_header);
+    split(trailer_header, trailer_header + len, ',',
+          [&](const char *b, const char *e) {
+            const char *kbeg = b;
+            const char *kend = e;
+            while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
+              ++kbeg;
+            }
+            while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) {
+              --kend;
+            }
+            std::string key(kbeg, static_cast<size_t>(kend - kbeg));
+            if (!key.empty() &&
+                prohibited_trailers.find(key) == prohibited_trailers.end()) {
+              declared_trailers.insert(key);
+            }
+          });
+  }
+
+  size_t trailer_header_count = 0;
+  while (strcmp(line_reader.ptr(), "\r\n") != 0) {
+    if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
+    if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
+
+    constexpr auto line_terminator_len = 2;
+    auto line_beg = line_reader.ptr();
+    auto line_end =
+        line_reader.ptr() + line_reader.size() - line_terminator_len;
+
+    if (!parse_header(line_beg, line_end,
+                      [&](const std::string &key, const std::string &val) {
+                        if (declared_trailers.find(key) !=
+                            declared_trailers.end()) {
+                          dest.emplace(key, val);
+                          trailer_header_count++;
+                        }
+                      })) {
+      return false;
+    }
+
+    if (!line_reader.getline()) { return false; }
+  }
+
+  return true;
+}
+
+inline std::pair<size_t, size_t> trim(const char *b, const char *e, size_t left,
+                                      size_t right) {
+  while (b + left < e && is_space_or_tab(b[left])) {
+    left++;
+  }
+  while (right > 0 && is_space_or_tab(b[right - 1])) {
+    right--;
+  }
+  return std::make_pair(left, right);
+}
+
+inline std::string trim_copy(const std::string &s) {
+  auto r = trim(s.data(), s.data() + s.size(), 0, s.size());
+  return s.substr(r.first, r.second - r.first);
+}
+
+inline std::string trim_double_quotes_copy(const std::string &s) {
+  if (s.length() >= 2 && s.front() == '"' && s.back() == '"') {
+    return s.substr(1, s.size() - 2);
+  }
+  return s;
+}
+
+inline void
+divide(const char *data, std::size_t size, char d,
+       std::function<void(const char *, std::size_t, const char *, std::size_t)>
+           fn) {
+  const auto it = std::find(data, data + size, d);
+  const auto found = static_cast<std::size_t>(it != data + size);
+  const auto lhs_data = data;
+  const auto lhs_size = static_cast<std::size_t>(it - data);
+  const auto rhs_data = it + found;
+  const auto rhs_size = size - lhs_size - found;
+
+  fn(lhs_data, lhs_size, rhs_data, rhs_size);
+}
+
+inline void
+divide(const std::string &str, char d,
+       std::function<void(const char *, std::size_t, const char *, std::size_t)>
+           fn) {
+  divide(str.data(), str.size(), d, std::move(fn));
+}
+
+inline void split(const char *b, const char *e, char d,
+                  std::function<void(const char *, const char *)> fn) {
+  return split(b, e, d, (std::numeric_limits<size_t>::max)(), std::move(fn));
+}
+
+inline void split(const char *b, const char *e, char d, size_t m,
+                  std::function<void(const char *, const char *)> fn) {
+  size_t i = 0;
+  size_t beg = 0;
+  size_t count = 1;
+
+  while (e ? (b + i < e) : (b[i] != '\0')) {
+    if (b[i] == d && count < m) {
+      auto r = trim(b, e, beg, i);
+      if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+      beg = i + 1;
+      count++;
+    }
+    i++;
+  }
+
+  if (i) {
+    auto r = trim(b, e, beg, i);
+    if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+  }
+}
+
+inline bool split_find(const char *b, const char *e, char d, size_t m,
+                       std::function<bool(const char *, const char *)> fn) {
+  size_t i = 0;
+  size_t beg = 0;
+  size_t count = 1;
+
+  while (e ? (b + i < e) : (b[i] != '\0')) {
+    if (b[i] == d && count < m) {
+      auto r = trim(b, e, beg, i);
+      if (r.first < r.second) {
+        auto found = fn(&b[r.first], &b[r.second]);
+        if (found) { return true; }
+      }
+      beg = i + 1;
+      count++;
+    }
+    i++;
+  }
+
+  if (i) {
+    auto r = trim(b, e, beg, i);
+    if (r.first < r.second) {
+      auto found = fn(&b[r.first], &b[r.second]);
+      if (found) { return true; }
+    }
+  }
+
+  return false;
+}
+
+inline bool split_find(const char *b, const char *e, char d,
+                       std::function<bool(const char *, const char *)> fn) {
+  return split_find(b, e, d, (std::numeric_limits<size_t>::max)(),
+                    std::move(fn));
+}
+
+inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer,
+                                              size_t fixed_buffer_size)
+    : strm_(strm), fixed_buffer_(fixed_buffer),
+      fixed_buffer_size_(fixed_buffer_size) {}
+
+inline const char *stream_line_reader::ptr() const {
+  if (growable_buffer_.empty()) {
+    return fixed_buffer_;
+  } else {
+    return growable_buffer_.data();
+  }
+}
+
+inline size_t stream_line_reader::size() const {
+  if (growable_buffer_.empty()) {
+    return fixed_buffer_used_size_;
+  } else {
+    return growable_buffer_.size();
+  }
+}
+
+inline bool stream_line_reader::end_with_crlf() const {
+  auto end = ptr() + size();
+  return size() >= 2 && end[-2] == '\r' && end[-1] == '\n';
+}
+
+inline bool stream_line_reader::getline() {
+  fixed_buffer_used_size_ = 0;
+  growable_buffer_.clear();
+
+#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+  char prev_byte = 0;
+#endif
+
+  for (size_t i = 0;; i++) {
+    if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
+      // Treat exceptionally long lines as an error to
+      // prevent infinite loops/memory exhaustion
+      return false;
+    }
+    char byte;
+    auto n = strm_.read(&byte, 1);
+
+    if (n < 0) {
+      return false;
+    } else if (n == 0) {
+      if (i == 0) {
+        return false;
+      } else {
+        break;
+      }
+    }
+
+    append(byte);
+
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+    if (byte == '\n') { break; }
+#else
+    if (prev_byte == '\r' && byte == '\n') { break; }
+    prev_byte = byte;
+#endif
+  }
+
+  return true;
+}
+
+inline void stream_line_reader::append(char c) {
+  if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
+    fixed_buffer_[fixed_buffer_used_size_++] = c;
+    fixed_buffer_[fixed_buffer_used_size_] = '\0';
+  } else {
+    if (growable_buffer_.empty()) {
+      assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
+      growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
+    }
+    growable_buffer_ += c;
+  }
+}
+
+inline mmap::mmap(const char *path) { open(path); }
+
+inline mmap::~mmap() { close(); }
+
+inline bool mmap::open(const char *path) {
+  close();
+
+#if defined(_WIN32)
+  auto wpath = u8string_to_wstring(path);
+  if (wpath.empty()) { return false; }
+
+  hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ,
+                         OPEN_EXISTING, NULL);
+
+  if (hFile_ == INVALID_HANDLE_VALUE) { return false; }
+
+  LARGE_INTEGER size{};
+  if (!::GetFileSizeEx(hFile_, &size)) { return false; }
+  // If the following line doesn't compile due to QuadPart, update Windows SDK.
+  // See:
+  // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721
+  if (static_cast<ULONGLONG>(size.QuadPart) >
+      (std::numeric_limits<decltype(size_)>::max)()) {
+    // `size_t` might be 32-bits, on 32-bits Windows.
+    return false;
+  }
+  size_ = static_cast<size_t>(size.QuadPart);
+
+  hMapping_ =
+      ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL);
+
+  // Special treatment for an empty file...
+  if (hMapping_ == NULL && size_ == 0) {
+    close();
+    is_open_empty_file = true;
+    return true;
+  }
+
+  if (hMapping_ == NULL) {
+    close();
+    return false;
+  }
+
+  addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0);
+
+  if (addr_ == nullptr) {
+    close();
+    return false;
+  }
+#else
+  fd_ = ::open(path, O_RDONLY);
+  if (fd_ == -1) { return false; }
+
+  struct stat sb;
+  if (fstat(fd_, &sb) == -1) {
+    close();
+    return false;
+  }
+  size_ = static_cast<size_t>(sb.st_size);
+
+  addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0);
+
+  // Special treatment for an empty file...
+  if (addr_ == MAP_FAILED && size_ == 0) {
+    close();
+    is_open_empty_file = true;
+    return false;
+  }
+#endif
+
+  return true;
+}
+
+inline bool mmap::is_open() const {
+  return is_open_empty_file ? true : addr_ != nullptr;
+}
+
+inline size_t mmap::size() const { return size_; }
+
+inline const char *mmap::data() const {
+  return is_open_empty_file ? "" : static_cast<const char *>(addr_);
+}
+
+inline void mmap::close() {
+#if defined(_WIN32)
+  if (addr_) {
+    ::UnmapViewOfFile(addr_);
+    addr_ = nullptr;
+  }
+
+  if (hMapping_) {
+    ::CloseHandle(hMapping_);
+    hMapping_ = NULL;
+  }
+
+  if (hFile_ != INVALID_HANDLE_VALUE) {
+    ::CloseHandle(hFile_);
+    hFile_ = INVALID_HANDLE_VALUE;
+  }
+
+  is_open_empty_file = false;
+#else
+  if (addr_ != nullptr) {
+    munmap(addr_, size_);
+    addr_ = nullptr;
+  }
+
+  if (fd_ != -1) {
+    ::close(fd_);
+    fd_ = -1;
+  }
+#endif
+  size_ = 0;
+}
+inline int close_socket(socket_t sock) {
+#ifdef _WIN32
+  return closesocket(sock);
+#else
+  return close(sock);
+#endif
+}
+
+template <typename T> inline ssize_t handle_EINTR(T fn) {
+  ssize_t res = 0;
+  while (true) {
+    res = fn();
+    if (res < 0 && errno == EINTR) {
+      std::this_thread::sleep_for(std::chrono::microseconds{1});
+      continue;
+    }
+    break;
+  }
+  return res;
+}
+
+inline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) {
+  return handle_EINTR([&]() {
+    return recv(sock,
+#ifdef _WIN32
+                static_cast<char *>(ptr), static_cast<int>(size),
+#else
+                ptr, size,
+#endif
+                flags);
+  });
+}
+
+inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size,
+                           int flags) {
+  return handle_EINTR([&]() {
+    return send(sock,
+#ifdef _WIN32
+                static_cast<const char *>(ptr), static_cast<int>(size),
+#else
+                ptr, size,
+#endif
+                flags);
+  });
+}
+
+inline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) {
+#ifdef _WIN32
+  return ::WSAPoll(fds, nfds, timeout);
+#else
+  return ::poll(fds, nfds, timeout);
+#endif
+}
+
+inline ssize_t select_impl(socket_t sock, short events, time_t sec,
+                           time_t usec) {
+  struct pollfd pfd;
+  pfd.fd = sock;
+  pfd.events = events;
+  pfd.revents = 0;
+
+  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
+
+  return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
+}
+
+inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) {
+  return select_impl(sock, POLLIN, sec, usec);
+}
+
+inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) {
+  return select_impl(sock, POLLOUT, sec, usec);
+}
+
+inline Error wait_until_socket_is_ready(socket_t sock, time_t sec,
+                                        time_t usec) {
+  struct pollfd pfd_read;
+  pfd_read.fd = sock;
+  pfd_read.events = POLLIN | POLLOUT;
+  pfd_read.revents = 0;
+
+  auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
+
+  auto poll_res =
+      handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); });
+
+  if (poll_res == 0) { return Error::ConnectionTimeout; }
+
+  if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) {
+    auto error = 0;
+    socklen_t len = sizeof(error);
+    auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,
+                          reinterpret_cast<char *>(&error), &len);
+    auto successful = res >= 0 && !error;
+    return successful ? Error::Success : Error::Connection;
+  }
+
+  return Error::Connection;
+}
+
+inline bool is_socket_alive(socket_t sock) {
+  const auto val = detail::select_read(sock, 0, 0);
+  if (val == 0) {
+    return true;
+  } else if (val < 0 && errno == EBADF) {
+    return false;
+  }
+  char buf[1];
+  return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0;
+}
+
+class SocketStream final : public Stream {
+public:
+  SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+               time_t write_timeout_sec, time_t write_timeout_usec,
+               time_t max_timeout_msec = 0,
+               std::chrono::time_point<std::chrono::steady_clock> start_time =
+                   (std::chrono::steady_clock::time_point::min)());
+  ~SocketStream() override;
+
+  bool is_readable() const override;
+  bool wait_readable() const override;
+  bool wait_writable() const override;
+  bool is_peer_alive() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  void get_local_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+  time_t duration() const override;
+  void set_read_timeout(time_t sec, time_t usec = 0) override;
+
+private:
+  socket_t sock_;
+  time_t read_timeout_sec_;
+  time_t read_timeout_usec_;
+  time_t write_timeout_sec_;
+  time_t write_timeout_usec_;
+  time_t max_timeout_msec_;
+  const std::chrono::time_point<std::chrono::steady_clock> start_time_;
+
+  std::vector<char> read_buff_;
+  size_t read_buff_off_ = 0;
+  size_t read_buff_content_size_ = 0;
+
+  static const size_t read_buff_size_ = 1024l * 4;
+};
+
+inline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
+                       time_t keep_alive_timeout_sec) {
+  using namespace std::chrono;
+
+  const auto interval_usec =
+      CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND;
+
+  // Avoid expensive `steady_clock::now()` call for the first time
+  if (select_read(sock, 0, interval_usec) > 0) { return true; }
+
+  const auto start = steady_clock::now() - microseconds{interval_usec};
+  const auto timeout = seconds{keep_alive_timeout_sec};
+
+  while (true) {
+    if (svr_sock == INVALID_SOCKET) {
+      break; // Server socket is closed
+    }
+
+    auto val = select_read(sock, 0, interval_usec);
+    if (val < 0) {
+      break; // Ssocket error
+    } else if (val == 0) {
+      if (steady_clock::now() - start > timeout) {
+        break; // Timeout
+      }
+    } else {
+      return true; // Ready for read
+    }
+  }
+
+  return false;
+}
+
+template <typename T>
+inline bool
+process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
+                           size_t keep_alive_max_count,
+                           time_t keep_alive_timeout_sec, T callback) {
+  assert(keep_alive_max_count > 0);
+  auto ret = false;
+  auto count = keep_alive_max_count;
+  while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {
+    auto close_connection = count == 1;
+    auto connection_closed = false;
+    ret = callback(close_connection, connection_closed);
+    if (!ret || connection_closed) { break; }
+    count--;
+  }
+  return ret;
+}
+
+template <typename T>
+inline bool
+process_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,
+                      size_t keep_alive_max_count,
+                      time_t keep_alive_timeout_sec, time_t read_timeout_sec,
+                      time_t read_timeout_usec, time_t write_timeout_sec,
+                      time_t write_timeout_usec, T callback) {
+  return process_server_socket_core(
+      svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
+      [&](bool close_connection, bool &connection_closed) {
+        SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+                          write_timeout_sec, write_timeout_usec);
+        return callback(strm, close_connection, connection_closed);
+      });
+}
+
+inline bool process_client_socket(
+    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+    time_t write_timeout_sec, time_t write_timeout_usec,
+    time_t max_timeout_msec,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    std::function<bool(Stream &)> callback) {
+  SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+                    write_timeout_sec, write_timeout_usec, max_timeout_msec,
+                    start_time);
+  return callback(strm);
+}
+
+inline int shutdown_socket(socket_t sock) {
+#ifdef _WIN32
+  return shutdown(sock, SD_BOTH);
+#else
+  return shutdown(sock, SHUT_RDWR);
+#endif
+}
+
+inline std::string escape_abstract_namespace_unix_domain(const std::string &s) {
+  if (s.size() > 1 && s[0] == '\0') {
+    auto ret = s;
+    ret[0] = '@';
+    return ret;
+  }
+  return s;
+}
+
+inline std::string
+unescape_abstract_namespace_unix_domain(const std::string &s) {
+  if (s.size() > 1 && s[0] == '@') {
+    auto ret = s;
+    ret[0] = '\0';
+    return ret;
+  }
+  return s;
+}
+
+inline int getaddrinfo_with_timeout(const char *node, const char *service,
+                                    const struct addrinfo *hints,
+                                    struct addrinfo **res, time_t timeout_sec) {
+#ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
+  if (timeout_sec <= 0) {
+    // No timeout specified, use standard getaddrinfo
+    return getaddrinfo(node, service, hints, res);
+  }
+
+#ifdef _WIN32
+  // Windows-specific implementation using GetAddrInfoEx with overlapped I/O
+  OVERLAPPED overlapped = {0};
+  HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+  if (!event) { return EAI_FAIL; }
+
+  overlapped.hEvent = event;
+
+  PADDRINFOEXW result_addrinfo = nullptr;
+  HANDLE cancel_handle = nullptr;
+
+  ADDRINFOEXW hints_ex = {0};
+  if (hints) {
+    hints_ex.ai_flags = hints->ai_flags;
+    hints_ex.ai_family = hints->ai_family;
+    hints_ex.ai_socktype = hints->ai_socktype;
+    hints_ex.ai_protocol = hints->ai_protocol;
+  }
+
+  auto wnode = u8string_to_wstring(node);
+  auto wservice = u8string_to_wstring(service);
+
+  auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr,
+                              hints ? &hints_ex : nullptr, &result_addrinfo,
+                              nullptr, &overlapped, nullptr, &cancel_handle);
+
+  if (ret == WSA_IO_PENDING) {
+    auto wait_result =
+        ::WaitForSingleObject(event, static_cast<DWORD>(timeout_sec * 1000));
+    if (wait_result == WAIT_TIMEOUT) {
+      if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }
+      ::CloseHandle(event);
+      return EAI_AGAIN;
+    }
+
+    DWORD bytes_returned;
+    if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped,
+                               &bytes_returned, FALSE)) {
+      ::CloseHandle(event);
+      return ::WSAGetLastError();
+    }
+  }
+
+  ::CloseHandle(event);
+
+  if (ret == NO_ERROR || ret == WSA_IO_PENDING) {
+    *res = reinterpret_cast<struct addrinfo *>(result_addrinfo);
+    return 0;
+  }
+
+  return ret;
+#elif TARGET_OS_MAC && defined(__clang__)
+  if (!node) { return EAI_NONAME; }
+  // macOS implementation using CFHost API for asynchronous DNS resolution
+  CFStringRef hostname_ref = CFStringCreateWithCString(
+      kCFAllocatorDefault, node, kCFStringEncodingUTF8);
+  if (!hostname_ref) { return EAI_MEMORY; }
+
+  CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref);
+  CFRelease(hostname_ref);
+  if (!host_ref) { return EAI_MEMORY; }
+
+  // Set up context for callback
+  struct CFHostContext {
+    bool completed = false;
+    bool success = false;
+    CFArrayRef addresses = nullptr;
+    std::mutex mutex;
+    std::condition_variable cv;
+  } context;
+
+  CFHostClientContext client_context;
+  memset(&client_context, 0, sizeof(client_context));
+  client_context.info = &context;
+
+  // Set callback
+  auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/,
+                     const CFStreamError *error, void *info) {
+    auto ctx = static_cast<CFHostContext *>(info);
+    std::lock_guard<std::mutex> lock(ctx->mutex);
+
+    if (error && error->error != 0) {
+      ctx->success = false;
+    } else {
+      Boolean hasBeenResolved;
+      ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved);
+      if (ctx->addresses && hasBeenResolved) {
+        CFRetain(ctx->addresses);
+        ctx->success = true;
+      } else {
+        ctx->success = false;
+      }
+    }
+    ctx->completed = true;
+    ctx->cv.notify_one();
+  };
+
+  if (!CFHostSetClient(host_ref, callback, &client_context)) {
+    CFRelease(host_ref);
+    return EAI_SYSTEM;
+  }
+
+  // Schedule on run loop
+  CFRunLoopRef run_loop = CFRunLoopGetCurrent();
+  CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+
+  // Start resolution
+  CFStreamError stream_error;
+  if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) {
+    CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+    CFRelease(host_ref);
+    return EAI_FAIL;
+  }
+
+  // Wait for completion with timeout
+  auto timeout_time =
+      std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
+  bool timed_out = false;
+
+  {
+    std::unique_lock<std::mutex> lock(context.mutex);
+
+    while (!context.completed) {
+      auto now = std::chrono::steady_clock::now();
+      if (now >= timeout_time) {
+        timed_out = true;
+        break;
+      }
+
+      // Run the runloop for a short time
+      lock.unlock();
+      CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);
+      lock.lock();
+    }
+  }
+
+  // Clean up
+  CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+  CFHostSetClient(host_ref, nullptr, nullptr);
+
+  if (timed_out || !context.completed) {
+    CFHostCancelInfoResolution(host_ref, kCFHostAddresses);
+    CFRelease(host_ref);
+    return EAI_AGAIN;
+  }
+
+  if (!context.success || !context.addresses) {
+    CFRelease(host_ref);
+    return EAI_NODATA;
+  }
+
+  // Convert CFArray to addrinfo
+  CFIndex count = CFArrayGetCount(context.addresses);
+  if (count == 0) {
+    CFRelease(context.addresses);
+    CFRelease(host_ref);
+    return EAI_NODATA;
+  }
+
+  struct addrinfo *result_addrinfo = nullptr;
+  struct addrinfo **current = &result_addrinfo;
+
+  for (CFIndex i = 0; i < count; i++) {
+    CFDataRef addr_data =
+        static_cast<CFDataRef>(CFArrayGetValueAtIndex(context.addresses, i));
+    if (!addr_data) continue;
+
+    const struct sockaddr *sockaddr_ptr =
+        reinterpret_cast<const struct sockaddr *>(CFDataGetBytePtr(addr_data));
+    socklen_t sockaddr_len = static_cast<socklen_t>(CFDataGetLength(addr_data));
+
+    // Allocate addrinfo structure
+    *current = static_cast<struct addrinfo *>(malloc(sizeof(struct addrinfo)));
+    if (!*current) {
+      freeaddrinfo(result_addrinfo);
+      CFRelease(context.addresses);
+      CFRelease(host_ref);
+      return EAI_MEMORY;
+    }
+
+    memset(*current, 0, sizeof(struct addrinfo));
+
+    // Set up addrinfo fields
+    (*current)->ai_family = sockaddr_ptr->sa_family;
+    (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM;
+    (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP;
+    (*current)->ai_addrlen = sockaddr_len;
+
+    // Copy sockaddr
+    (*current)->ai_addr = static_cast<struct sockaddr *>(malloc(sockaddr_len));
+    if (!(*current)->ai_addr) {
+      freeaddrinfo(result_addrinfo);
+      CFRelease(context.addresses);
+      CFRelease(host_ref);
+      return EAI_MEMORY;
+    }
+    memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);
+
+    // Set port if service is specified
+    if (service && *service) {
+      int port = 0;
+      if (parse_port(service, strlen(service), port)) {
+        if (sockaddr_ptr->sa_family == AF_INET) {
+          reinterpret_cast<struct sockaddr_in *>((*current)->ai_addr)
+              ->sin_port = htons(static_cast<uint16_t>(port));
+        } else if (sockaddr_ptr->sa_family == AF_INET6) {
+          reinterpret_cast<struct sockaddr_in6 *>((*current)->ai_addr)
+              ->sin6_port = htons(static_cast<uint16_t>(port));
+        }
+      }
+    }
+
+    current = &((*current)->ai_next);
+  }
+
+  CFRelease(context.addresses);
+  CFRelease(host_ref);
+
+  *res = result_addrinfo;
+  return 0;
+#elif defined(_GNU_SOURCE) && defined(__GLIBC__) &&                            \
+    (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2))
+  // Linux implementation using getaddrinfo_a for asynchronous DNS resolution
+  struct gaicb request;
+  struct gaicb *requests[1] = {&request};
+  struct sigevent sevp;
+  struct timespec timeout;
+
+  // Initialize the request structure
+  memset(&request, 0, sizeof(request));
+  request.ar_name = node;
+  request.ar_service = service;
+  request.ar_request = hints;
+
+  // Set up timeout
+  timeout.tv_sec = timeout_sec;
+  timeout.tv_nsec = 0;
+
+  // Initialize sigevent structure (not used, but required)
+  memset(&sevp, 0, sizeof(sevp));
+  sevp.sigev_notify = SIGEV_NONE;
+
+  // Start asynchronous resolution
+  int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);
+  if (start_result != 0) { return start_result; }
+
+  // Wait for completion with timeout
+  int wait_result =
+      gai_suspend((const struct gaicb *const *)requests, 1, &timeout);
+
+  if (wait_result == 0 || wait_result == EAI_ALLDONE) {
+    // Completed successfully, get the result
+    int gai_result = gai_error(&request);
+    if (gai_result == 0) {
+      *res = request.ar_result;
+      return 0;
+    } else {
+      // Clean up on error
+      if (request.ar_result) { freeaddrinfo(request.ar_result); }
+      return gai_result;
+    }
+  } else if (wait_result == EAI_AGAIN) {
+    // Timeout occurred, cancel the request
+    gai_cancel(&request);
+    return EAI_AGAIN;
+  } else {
+    // Other error occurred
+    gai_cancel(&request);
+    return wait_result;
+  }
+#else
+  // Fallback implementation using thread-based timeout for other Unix systems
+
+  struct GetAddrInfoState {
+    ~GetAddrInfoState() {
+      if (info) { freeaddrinfo(info); }
+    }
+
+    std::mutex mutex;
+    std::condition_variable result_cv;
+    bool completed = false;
+    int result = EAI_SYSTEM;
+    std::string node;
+    std::string service;
+    struct addrinfo hints;
+    struct addrinfo *info = nullptr;
+  };
+
+  // Allocate on the heap, so the resolver thread can keep using the data.
+  auto state = std::make_shared<GetAddrInfoState>();
+  if (node) { state->node = node; }
+  state->service = service;
+  state->hints = *hints;
+
+  std::thread resolve_thread([state]() {
+    auto thread_result =
+        getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints,
+                    &state->info);
+
+    std::lock_guard<std::mutex> lock(state->mutex);
+    state->result = thread_result;
+    state->completed = true;
+    state->result_cv.notify_one();
+  });
+
+  // Wait for completion or timeout
+  std::unique_lock<std::mutex> lock(state->mutex);
+  auto finished =
+      state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec),
+                                [&] { return state->completed; });
+
+  if (finished) {
+    // Operation completed within timeout
+    resolve_thread.join();
+    *res = state->info;
+    state->info = nullptr; // Pass ownership to caller
+    return state->result;
+  } else {
+    // Timeout occurred
+    resolve_thread.detach(); // Let the thread finish in background
+    return EAI_AGAIN;        // Return timeout error
+  }
+#endif
+#else
+  (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo
+  return getaddrinfo(node, service, hints, res);
+#endif
+}
+
+template <typename BindOrConnect>
+socket_t create_socket(const std::string &host, const std::string &ip, int port,
+                       int address_family, int socket_flags, bool tcp_nodelay,
+                       bool ipv6_v6only, SocketOptions socket_options,
+                       BindOrConnect bind_or_connect, time_t timeout_sec = 0) {
+  // Get address info
+  const char *node = nullptr;
+  struct addrinfo hints;
+  struct addrinfo *result;
+
+  memset(&hints, 0, sizeof(struct addrinfo));
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_protocol = IPPROTO_IP;
+
+  if (!ip.empty()) {
+    node = ip.c_str();
+    // Ask getaddrinfo to convert IP in c-string to address
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_flags = AI_NUMERICHOST;
+  } else {
+    if (!host.empty()) { node = host.c_str(); }
+    hints.ai_family = address_family;
+    hints.ai_flags = socket_flags;
+  }
+
+#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)
+  if (hints.ai_family == AF_UNIX) {
+    const auto addrlen = host.length();
+    if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }
+
+#ifdef SOCK_CLOEXEC
+    auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC,
+                       hints.ai_protocol);
+#else
+    auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol);
+#endif
+
+    if (sock != INVALID_SOCKET) {
+      sockaddr_un addr{};
+      addr.sun_family = AF_UNIX;
+
+      auto unescaped_host = unescape_abstract_namespace_unix_domain(host);
+      std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path);
+
+      hints.ai_addr = reinterpret_cast<sockaddr *>(&addr);
+      hints.ai_addrlen = static_cast<socklen_t>(
+          sizeof(addr) - sizeof(addr.sun_path) + addrlen);
+
+#ifndef SOCK_CLOEXEC
+#ifndef _WIN32
+      fcntl(sock, F_SETFD, FD_CLOEXEC);
+#endif
+#endif
+
+      if (socket_options) { socket_options(sock); }
+
+#ifdef _WIN32
+      // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so
+      // remove the option.
+      set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0);
+#endif
+
+      bool dummy;
+      if (!bind_or_connect(sock, hints, dummy)) {
+        close_socket(sock);
+        sock = INVALID_SOCKET;
+      }
+    }
+    return sock;
+  }
+#endif
+
+  auto service = std::to_string(port);
+
+  if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result,
+                               timeout_sec)) {
+#if defined __linux__ && !defined __ANDROID__
+    res_init();
+#endif
+    return INVALID_SOCKET;
+  }
+  auto se = detail::scope_exit([&] { freeaddrinfo(result); });
+
+  for (auto rp = result; rp; rp = rp->ai_next) {
+    // Create a socket
+#ifdef _WIN32
+    auto sock =
+        WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0,
+                   WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);
+    /**
+     * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1
+     * and above the socket creation fails on older Windows Systems.
+     *
+     * Let's try to create a socket the old way in this case.
+     *
+     * Reference:
+     * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa
+     *
+     * WSA_FLAG_NO_HANDLE_INHERIT:
+     * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with
+     * SP1, and later
+     *
+     */
+    if (sock == INVALID_SOCKET) {
+      sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+    }
+#else
+
+#ifdef SOCK_CLOEXEC
+    auto sock =
+        socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol);
+#else
+    auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+#endif
+
+#endif
+    if (sock == INVALID_SOCKET) { continue; }
+
+#if !defined _WIN32 && !defined SOCK_CLOEXEC
+    if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) {
+      close_socket(sock);
+      continue;
+    }
+#endif
+
+    if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); }
+
+    if (rp->ai_family == AF_INET6) {
+      set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0);
+    }
+
+    if (socket_options) { socket_options(sock); }
+
+    // bind or connect
+    auto quit = false;
+    if (bind_or_connect(sock, *rp, quit)) { return sock; }
+
+    close_socket(sock);
+
+    if (quit) { break; }
+  }
+
+  return INVALID_SOCKET;
+}
+
+inline void set_nonblocking(socket_t sock, bool nonblocking) {
+#ifdef _WIN32
+  auto flags = nonblocking ? 1UL : 0UL;
+  ioctlsocket(sock, FIONBIO, &flags);
+#else
+  auto flags = fcntl(sock, F_GETFL, 0);
+  fcntl(sock, F_SETFL,
+        nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK)));
+#endif
+}
+
+inline bool is_connection_error() {
+#ifdef _WIN32
+  return WSAGetLastError() != WSAEWOULDBLOCK;
+#else
+  return errno != EINPROGRESS;
+#endif
+}
+
+inline bool bind_ip_address(socket_t sock, const std::string &host) {
+  struct addrinfo hints;
+  struct addrinfo *result;
+
+  memset(&hints, 0, sizeof(struct addrinfo));
+  hints.ai_family = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_protocol = 0;
+
+  if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) {
+    return false;
+  }
+
+  auto se = detail::scope_exit([&] { freeaddrinfo(result); });
+
+  auto ret = false;
+  for (auto rp = result; rp; rp = rp->ai_next) {
+    const auto &ai = *rp;
+    if (!::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {
+      ret = true;
+      break;
+    }
+  }
+
+  return ret;
+}
+
+#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__
+#define USE_IF2IP
+#endif
+
+#ifdef USE_IF2IP
+inline std::string if2ip(int address_family, const std::string &ifn) {
+  struct ifaddrs *ifap;
+  getifaddrs(&ifap);
+  auto se = detail::scope_exit([&] { freeifaddrs(ifap); });
+
+  std::string addr_candidate;
+  for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {
+    if (ifa->ifa_addr && ifn == ifa->ifa_name &&
+        (AF_UNSPEC == address_family ||
+         ifa->ifa_addr->sa_family == address_family)) {
+      if (ifa->ifa_addr->sa_family == AF_INET) {
+        auto sa = reinterpret_cast<struct sockaddr_in *>(ifa->ifa_addr);
+        char buf[INET_ADDRSTRLEN];
+        if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) {
+          return std::string(buf, INET_ADDRSTRLEN);
+        }
+      } else if (ifa->ifa_addr->sa_family == AF_INET6) {
+        auto sa = reinterpret_cast<struct sockaddr_in6 *>(ifa->ifa_addr);
+        if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) {
+          char buf[INET6_ADDRSTRLEN] = {};
+          if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) {
+            // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL
+            auto s6_addr_head = sa->sin6_addr.s6_addr[0];
+            if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) {
+              addr_candidate = std::string(buf, INET6_ADDRSTRLEN);
+            } else {
+              return std::string(buf, INET6_ADDRSTRLEN);
+            }
+          }
+        }
+      }
+    }
+  }
+  return addr_candidate;
+}
+#endif
+
+inline socket_t create_client_socket(
+    const std::string &host, const std::string &ip, int port,
+    int address_family, bool tcp_nodelay, bool ipv6_v6only,
+    SocketOptions socket_options, time_t connection_timeout_sec,
+    time_t connection_timeout_usec, time_t read_timeout_sec,
+    time_t read_timeout_usec, time_t write_timeout_sec,
+    time_t write_timeout_usec, const std::string &intf, Error &error) {
+  auto sock = create_socket(
+      host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only,
+      std::move(socket_options),
+      [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool {
+        if (!intf.empty()) {
+#ifdef USE_IF2IP
+          auto ip_from_if = if2ip(address_family, intf);
+          if (ip_from_if.empty()) { ip_from_if = intf; }
+          if (!bind_ip_address(sock2, ip_from_if)) {
+            error = Error::BindIPAddress;
+            return false;
+          }
+#endif
+        }
+
+        set_nonblocking(sock2, true);
+
+        auto ret =
+            ::connect(sock2, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));
+
+        if (ret < 0) {
+          if (is_connection_error()) {
+            error = Error::Connection;
+            return false;
+          }
+          error = wait_until_socket_is_ready(sock2, connection_timeout_sec,
+                                             connection_timeout_usec);
+          if (error != Error::Success) {
+            if (error == Error::ConnectionTimeout) { quit = true; }
+            return false;
+          }
+        }
+
+        set_nonblocking(sock2, false);
+        set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec,
+                            read_timeout_usec);
+        set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec,
+                            write_timeout_usec);
+
+        error = Error::Success;
+        return true;
+      },
+      connection_timeout_sec); // Pass DNS timeout
+
+  if (sock != INVALID_SOCKET) {
+    error = Error::Success;
+  } else {
+    if (error == Error::Success) { error = Error::Connection; }
+  }
+
+  return sock;
+}
+
+inline bool get_ip_and_port(const struct sockaddr_storage &addr,
+                            socklen_t addr_len, std::string &ip, int &port) {
+  if (addr.ss_family == AF_INET) {
+    port = ntohs(reinterpret_cast<const struct sockaddr_in *>(&addr)->sin_port);
+  } else if (addr.ss_family == AF_INET6) {
+    port =
+        ntohs(reinterpret_cast<const struct sockaddr_in6 *>(&addr)->sin6_port);
+  } else {
+    return false;
+  }
+
+  std::array<char, NI_MAXHOST> ipstr{};
+  if (getnameinfo(reinterpret_cast<const struct sockaddr *>(&addr), addr_len,
+                  ipstr.data(), static_cast<socklen_t>(ipstr.size()), nullptr,
+                  0, NI_NUMERICHOST)) {
+    return false;
+  }
+
+  ip = ipstr.data();
+  return true;
+}
+
+inline void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) {
+  struct sockaddr_storage addr;
+  socklen_t addr_len = sizeof(addr);
+  if (!getsockname(sock, reinterpret_cast<struct sockaddr *>(&addr),
+                   &addr_len)) {
+    get_ip_and_port(addr, addr_len, ip, port);
+  }
+}
+
+inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
+  struct sockaddr_storage addr;
+  socklen_t addr_len = sizeof(addr);
+
+  if (!getpeername(sock, reinterpret_cast<struct sockaddr *>(&addr),
+                   &addr_len)) {
+#ifndef _WIN32
+    if (addr.ss_family == AF_UNIX) {
+#if defined(__linux__)
+      struct ucred ucred;
+      socklen_t len = sizeof(ucred);
+      if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) {
+        port = ucred.pid;
+      }
+#elif defined(SOL_LOCAL) && defined(SO_PEERPID)
+      pid_t pid;
+      socklen_t len = sizeof(pid);
+      if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) {
+        port = pid;
+      }
+#endif
+      return;
+    }
+#endif
+    get_ip_and_port(addr, addr_len, ip, port);
+  }
+}
+
+inline constexpr unsigned int str2tag_core(const char *s, size_t l,
+                                           unsigned int h) {
+  return (l == 0)
+             ? h
+             : str2tag_core(
+                   s + 1, l - 1,
+                   // Unsets the 6 high bits of h, therefore no overflow happens
+                   (((std::numeric_limits<unsigned int>::max)() >> 6) &
+                    h * 33) ^
+                       static_cast<unsigned char>(*s));
+}
+
+inline unsigned int str2tag(const std::string &s) {
+  return str2tag_core(s.data(), s.size(), 0);
+}
+
+namespace udl {
+
+inline constexpr unsigned int operator""_t(const char *s, size_t l) {
+  return str2tag_core(s, l, 0);
+}
+
+} // namespace udl
+
+inline std::string
+find_content_type(const std::string &path,
+                  const std::map<std::string, std::string> &user_data,
+                  const std::string &default_content_type) {
+  auto ext = file_extension(path);
+
+  auto it = user_data.find(ext);
+  if (it != user_data.end()) { return it->second; }
+
+  using udl::operator""_t;
+
+  switch (str2tag(ext)) {
+  default: return default_content_type;
+
+  case "css"_t: return "text/css";
+  case "csv"_t: return "text/csv";
+  case "htm"_t:
+  case "html"_t: return "text/html";
+  case "js"_t:
+  case "mjs"_t: return "text/javascript";
+  case "txt"_t: return "text/plain";
+  case "vtt"_t: return "text/vtt";
+
+  case "apng"_t: return "image/apng";
+  case "avif"_t: return "image/avif";
+  case "bmp"_t: return "image/bmp";
+  case "gif"_t: return "image/gif";
+  case "png"_t: return "image/png";
+  case "svg"_t: return "image/svg+xml";
+  case "webp"_t: return "image/webp";
+  case "ico"_t: return "image/x-icon";
+  case "tif"_t: return "image/tiff";
+  case "tiff"_t: return "image/tiff";
+  case "jpg"_t:
+  case "jpeg"_t: return "image/jpeg";
+
+  case "mp4"_t: return "video/mp4";
+  case "mpeg"_t: return "video/mpeg";
+  case "webm"_t: return "video/webm";
+
+  case "mp3"_t: return "audio/mp3";
+  case "mpga"_t: return "audio/mpeg";
+  case "weba"_t: return "audio/webm";
+  case "wav"_t: return "audio/wave";
+
+  case "otf"_t: return "font/otf";
+  case "ttf"_t: return "font/ttf";
+  case "woff"_t: return "font/woff";
+  case "woff2"_t: return "font/woff2";
+
+  case "7z"_t: return "application/x-7z-compressed";
+  case "atom"_t: return "application/atom+xml";
+  case "pdf"_t: return "application/pdf";
+  case "json"_t: return "application/json";
+  case "rss"_t: return "application/rss+xml";
+  case "tar"_t: return "application/x-tar";
+  case "xht"_t:
+  case "xhtml"_t: return "application/xhtml+xml";
+  case "xslt"_t: return "application/xslt+xml";
+  case "xml"_t: return "application/xml";
+  case "gz"_t: return "application/gzip";
+  case "zip"_t: return "application/zip";
+  case "wasm"_t: return "application/wasm";
+  }
+}
+
+inline std::string
+extract_media_type(const std::string &content_type,
+                   std::map<std::string, std::string> *params = nullptr) {
+  // Extract type/subtype from Content-Type value (RFC 2045)
+  // e.g. "application/json; charset=utf-8" -> "application/json"
+  auto media_type = content_type;
+  auto semicolon_pos = media_type.find(';');
+  if (semicolon_pos != std::string::npos) {
+    auto param_str = media_type.substr(semicolon_pos + 1);
+    media_type = media_type.substr(0, semicolon_pos);
+
+    if (params) {
+      // Parse parameters: key=value pairs separated by ';'
+      split(param_str.data(), param_str.data() + param_str.size(), ';',
+            [&](const char *b, const char *e) {
+              std::string key;
+              std::string val;
+              split(b, e, '=', [&](const char *b2, const char *e2) {
+                if (key.empty()) {
+                  key.assign(b2, e2);
+                } else {
+                  val.assign(b2, e2);
+                }
+              });
+              if (!key.empty()) {
+                params->emplace(trim_copy(key), trim_double_quotes_copy(val));
+              }
+            });
+    }
+  }
+
+  // Trim whitespace from media type
+  return trim_copy(media_type);
+}
+
+inline bool can_compress_content_type(const std::string &content_type) {
+  using udl::operator""_t;
+
+  auto mime_type = extract_media_type(content_type);
+  auto tag = str2tag(mime_type);
+
+  switch (tag) {
+  case "image/svg+xml"_t:
+  case "application/javascript"_t:
+  case "application/x-javascript"_t:
+  case "application/json"_t:
+  case "application/ld+json"_t:
+  case "application/xml"_t:
+  case "application/xhtml+xml"_t:
+  case "application/rss+xml"_t:
+  case "application/atom+xml"_t:
+  case "application/xslt+xml"_t:
+  case "application/protobuf"_t: return true;
+
+  case "text/event-stream"_t: return false;
+
+  default: return !mime_type.rfind("text/", 0);
+  }
+}
+
+inline bool parse_quality(const char *b, const char *e, std::string &token,
+                          double &quality) {
+  quality = 1.0;
+  token.clear();
+
+  // Split on first ';': left = token name, right = parameters
+  const char *params_b = nullptr;
+  std::size_t params_len = 0;
+
+  divide(
+      b, static_cast<std::size_t>(e - b), ';',
+      [&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
+        auto r = trim(lb, lb + llen, 0, llen);
+        if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
+        params_b = rb;
+        params_len = rlen;
+      });
+
+  if (token.empty()) { return false; }
+  if (params_len == 0) { return true; }
+
+  // Scan parameters for q= (stops on first match)
+  bool invalid = false;
+  split_find(params_b, params_b + params_len, ';',
+             (std::numeric_limits<size_t>::max)(),
+             [&](const char *pb, const char *pe) -> bool {
+               // Match exactly "q=" or "Q=" (not "query=" etc.)
+               auto len = static_cast<size_t>(pe - pb);
+               if (len < 2) { return false; }
+               if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
+                 return false;
+               }
+
+               // Trim the value portion
+               auto r = trim(pb, pe, 2, len);
+               if (r.first >= r.second) {
+                 invalid = true;
+                 return true;
+               }
+
+               double v = 0.0;
+               auto res = from_chars(pb + r.first, pb + r.second, v);
+               if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
+                 invalid = true;
+                 return true;
+               }
+               quality = v;
+               return true;
+             });
+
+  return !invalid;
+}
+
+inline EncodingType encoding_type(const Request &req, const Response &res) {
+  if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
+    return EncodingType::None;
+  }
+
+  const auto &s = req.get_header_value("Accept-Encoding");
+  if (s.empty()) { return EncodingType::None; }
+
+  // Single-pass: iterate tokens and track the best supported encoding.
+  // Server preference breaks ties (br > gzip > zstd).
+  EncodingType best = EncodingType::None;
+  double best_q = 0.0; // q=0 means "not acceptable"
+
+  // Server preference: Brotli > Gzip > Zstd (lower = more preferred)
+  auto priority = [](EncodingType t) -> int {
+    switch (t) {
+    case EncodingType::Brotli: return 0;
+    case EncodingType::Gzip: return 1;
+    case EncodingType::Zstd: return 2;
+    default: return 3;
+    }
+  };
+
+  std::string name;
+  split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
+    double quality = 1.0;
+    if (!parse_quality(b, e, name, quality)) { return; }
+    if (quality <= 0.0) { return; }
+
+    EncodingType type = EncodingType::None;
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+    if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
+#endif
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+    if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
+      type = EncodingType::Gzip;
+    }
+#endif
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+    if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
+      type = EncodingType::Zstd;
+    }
+#endif
+
+    if (type == EncodingType::None) { return; }
+
+    // Higher q-value wins; for equal q, server preference breaks ties
+    if (quality > best_q ||
+        (quality == best_q && priority(type) < priority(best))) {
+      best_q = quality;
+      best = type;
+    }
+  });
+
+  return best;
+}
+
+inline std::unique_ptr<compressor> make_compressor(EncodingType type) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+  if (type == EncodingType::Gzip) {
+    return detail::make_unique<gzip_compressor>();
+  }
+#endif
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  if (type == EncodingType::Brotli) {
+    return detail::make_unique<brotli_compressor>();
+  }
+#endif
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+  if (type == EncodingType::Zstd) {
+    return detail::make_unique<zstd_compressor>();
+  }
+#endif
+  (void)type;
+  return nullptr;
+}
+
+inline const char *encoding_name(EncodingType type) {
+  switch (type) {
+  case EncodingType::Gzip: return "gzip";
+  case EncodingType::Brotli: return "br";
+  case EncodingType::Zstd: return "zstd";
+  default: return "";
+  }
+}
+
+inline bool nocompressor::compress(const char *data, size_t data_length,
+                                   bool /*last*/, Callback callback) {
+  if (!data_length) { return true; }
+  return callback(data, data_length);
+}
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+inline gzip_compressor::gzip_compressor() {
+  std::memset(&strm_, 0, sizeof(strm_));
+  strm_.zalloc = Z_NULL;
+  strm_.zfree = Z_NULL;
+  strm_.opaque = Z_NULL;
+
+  is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8,
+                           Z_DEFAULT_STRATEGY) == Z_OK;
+}
+
+inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); }
+
+inline bool gzip_compressor::compress(const char *data, size_t data_length,
+                                      bool last, Callback callback) {
+  assert(is_valid_);
+
+  do {
+    constexpr size_t max_avail_in =
+        (std::numeric_limits<decltype(strm_.avail_in)>::max)();
+
+    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(
+        (std::min)(data_length, max_avail_in));
+    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));
+
+    data_length -= strm_.avail_in;
+    data += strm_.avail_in;
+
+    auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH;
+    auto ret = Z_OK;
+
+    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+    do {
+      strm_.avail_out = static_cast<uInt>(buff.size());
+      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());
+
+      ret = deflate(&strm_, flush);
+      if (ret == Z_STREAM_ERROR) { return false; }
+
+      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+        return false;
+      }
+    } while (strm_.avail_out == 0);
+
+    assert((flush == Z_FINISH && ret == Z_STREAM_END) ||
+           (flush == Z_NO_FLUSH && ret == Z_OK));
+    assert(strm_.avail_in == 0);
+  } while (data_length > 0);
+
+  return true;
+}
+
+inline gzip_decompressor::gzip_decompressor() {
+  std::memset(&strm_, 0, sizeof(strm_));
+  strm_.zalloc = Z_NULL;
+  strm_.zfree = Z_NULL;
+  strm_.opaque = Z_NULL;
+
+  // 15 is the value of wbits, which should be at the maximum possible value
+  // to ensure that any gzip stream can be decoded. The offset of 32 specifies
+  // that the stream type should be automatically detected either gzip or
+  // deflate.
+  is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK;
+}
+
+inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); }
+
+inline bool gzip_decompressor::is_valid() const { return is_valid_; }
+
+inline bool gzip_decompressor::decompress(const char *data, size_t data_length,
+                                          Callback callback) {
+  assert(is_valid_);
+
+  auto ret = Z_OK;
+
+  do {
+    constexpr size_t max_avail_in =
+        (std::numeric_limits<decltype(strm_.avail_in)>::max)();
+
+    strm_.avail_in = static_cast<decltype(strm_.avail_in)>(
+        (std::min)(data_length, max_avail_in));
+    strm_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(data));
+
+    data_length -= strm_.avail_in;
+    data += strm_.avail_in;
+
+    std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+    while (strm_.avail_in > 0 && ret == Z_OK) {
+      strm_.avail_out = static_cast<uInt>(buff.size());
+      strm_.next_out = reinterpret_cast<Bytef *>(buff.data());
+
+      ret = inflate(&strm_, Z_NO_FLUSH);
+
+      assert(ret != Z_STREAM_ERROR);
+      switch (ret) {
+      case Z_NEED_DICT:
+      case Z_DATA_ERROR:
+      case Z_MEM_ERROR: inflateEnd(&strm_); return false;
+      }
+
+      if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+        return false;
+      }
+    }
+
+    if (ret != Z_OK && ret != Z_STREAM_END) { return false; }
+
+  } while (data_length > 0);
+
+  return true;
+}
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+inline brotli_compressor::brotli_compressor() {
+  state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
+}
+
+inline brotli_compressor::~brotli_compressor() {
+  BrotliEncoderDestroyInstance(state_);
+}
+
+inline bool brotli_compressor::compress(const char *data, size_t data_length,
+                                        bool last, Callback callback) {
+  std::array<uint8_t, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+
+  auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS;
+  auto available_in = data_length;
+  auto next_in = reinterpret_cast<const uint8_t *>(data);
+
+  for (;;) {
+    if (last) {
+      if (BrotliEncoderIsFinished(state_)) { break; }
+    } else {
+      if (!available_in) { break; }
+    }
+
+    auto available_out = buff.size();
+    auto next_out = buff.data();
+
+    if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in,
+                                     &available_out, &next_out, nullptr)) {
+      return false;
+    }
+
+    auto output_bytes = buff.size() - available_out;
+    if (output_bytes) {
+      callback(reinterpret_cast<const char *>(buff.data()), output_bytes);
+    }
+  }
+
+  return true;
+}
+
+inline brotli_decompressor::brotli_decompressor() {
+  decoder_s = BrotliDecoderCreateInstance(0, 0, 0);
+  decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT
+                        : BROTLI_DECODER_RESULT_ERROR;
+}
+
+inline brotli_decompressor::~brotli_decompressor() {
+  if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); }
+}
+
+inline bool brotli_decompressor::is_valid() const { return decoder_s; }
+
+inline bool brotli_decompressor::decompress(const char *data,
+                                            size_t data_length,
+                                            Callback callback) {
+  if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+      decoder_r == BROTLI_DECODER_RESULT_ERROR) {
+    return 0;
+  }
+
+  auto next_in = reinterpret_cast<const uint8_t *>(data);
+  size_t avail_in = data_length;
+  size_t total_out;
+
+  decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT;
+
+  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+  while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
+    char *next_out = buff.data();
+    size_t avail_out = buff.size();
+
+    decoder_r = BrotliDecoderDecompressStream(
+        decoder_s, &avail_in, &next_in, &avail_out,
+        reinterpret_cast<uint8_t **>(&next_out), &total_out);
+
+    if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; }
+
+    if (!callback(buff.data(), buff.size() - avail_out)) { return false; }
+  }
+
+  return decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+         decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT;
+}
+#endif
+
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+inline zstd_compressor::zstd_compressor() {
+  ctx_ = ZSTD_createCCtx();
+  ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast);
+}
+
+inline zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); }
+
+inline bool zstd_compressor::compress(const char *data, size_t data_length,
+                                      bool last, Callback callback) {
+  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+
+  ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue;
+  ZSTD_inBuffer input = {data, data_length, 0};
+
+  bool finished;
+  do {
+    ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};
+    size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode);
+
+    if (ZSTD_isError(remaining)) { return false; }
+
+    if (!callback(buff.data(), output.pos)) { return false; }
+
+    finished = last ? (remaining == 0) : (input.pos == input.size);
+
+  } while (!finished);
+
+  return true;
+}
+
+inline zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); }
+
+inline zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); }
+
+inline bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; }
+
+inline bool zstd_decompressor::decompress(const char *data, size_t data_length,
+                                          Callback callback) {
+  std::array<char, CPPHTTPLIB_COMPRESSION_BUFSIZ> buff{};
+  ZSTD_inBuffer input = {data, data_length, 0};
+
+  while (input.pos < input.size) {
+    ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};
+    size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input);
+
+    if (ZSTD_isError(remaining)) { return false; }
+
+    if (!callback(buff.data(), output.pos)) { return false; }
+  }
+
+  return true;
+}
+#endif
+
+inline std::unique_ptr<decompressor>
+create_decompressor(const std::string &encoding) {
+  std::unique_ptr<decompressor> decompressor;
+
+  if (encoding == "gzip" || encoding == "deflate") {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+    decompressor = detail::make_unique<gzip_decompressor>();
+#endif
+  } else if (encoding.find("br") != std::string::npos) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+    decompressor = detail::make_unique<brotli_decompressor>();
+#endif
+  } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+    decompressor = detail::make_unique<zstd_decompressor>();
+#endif
+  }
+
+  return decompressor;
+}
+
+// Returns the best available compressor and its Content-Encoding name.
+// Priority: Brotli > Gzip > Zstd (matches server-side preference).
+inline std::pair<std::unique_ptr<compressor>, const char *>
+create_compressor() {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  return {detail::make_unique<brotli_compressor>(), "br"};
+#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
+  return {detail::make_unique<gzip_compressor>(), "gzip"};
+#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
+  return {detail::make_unique<zstd_compressor>(), "zstd"};
+#else
+  return {nullptr, nullptr};
+#endif
+}
+
+inline bool is_prohibited_header_name(const std::string &name) {
+  using udl::operator""_t;
+
+  switch (str2tag(name)) {
+  case "REMOTE_ADDR"_t:
+  case "REMOTE_PORT"_t:
+  case "LOCAL_ADDR"_t:
+  case "LOCAL_PORT"_t: return true;
+  default: return false;
+  }
+}
+
+inline bool has_header(const Headers &headers, const std::string &key) {
+  if (is_prohibited_header_name(key)) { return false; }
+  return headers.find(key) != headers.end();
+}
+
+inline const char *get_header_value(const Headers &headers,
+                                    const std::string &key, const char *def,
+                                    size_t id) {
+  if (is_prohibited_header_name(key)) {
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+    std::string msg = "Prohibited header name '" + key + "' is specified.";
+    throw std::invalid_argument(msg);
+#else
+    return "";
+#endif
+  }
+
+  auto rng = headers.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) { return it->second.c_str(); }
+  return def;
+}
+
+inline size_t get_header_value_count(const Headers &headers,
+                                     const std::string &key) {
+  auto r = headers.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+template <typename Map>
+inline typename Map::mapped_type
+get_multimap_value(const Map &m, const std::string &key, size_t id) {
+  auto rng = m.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) { return it->second; }
+  return typename Map::mapped_type();
+}
+
+inline void set_header(Headers &headers, const std::string &key,
+                       const std::string &val) {
+  if (fields::is_field_name(key) && fields::is_field_value(val)) {
+    headers.emplace(key, val);
+  }
+}
+
+inline bool read_headers(Stream &strm, Headers &headers) {
+  const auto bufsiz = 2048;
+  char buf[bufsiz];
+  stream_line_reader line_reader(strm, buf, bufsiz);
+
+  size_t header_count = 0;
+
+  for (;;) {
+    if (!line_reader.getline()) { return false; }
+
+    // Check if the line ends with CRLF.
+    auto line_terminator_len = 2;
+    if (line_reader.end_with_crlf()) {
+      // Blank line indicates end of headers.
+      if (line_reader.size() == 2) { break; }
+    } else {
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+      // Blank line indicates end of headers.
+      if (line_reader.size() == 1) { break; }
+      line_terminator_len = 1;
+#else
+      continue; // Skip invalid line.
+#endif
+    }
+
+    if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
+
+    // Check header count limit
+    if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
+
+    // Exclude line terminator
+    auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
+
+    if (!parse_header(line_reader.ptr(), end,
+                      [&](const std::string &key, const std::string &val) {
+                        headers.emplace(key, val);
+                      })) {
+      return false;
+    }
+
+    header_count++;
+  }
+
+  // RFC 9110 Section 8.6: Reject requests with multiple Content-Length
+  // headers that have different values to prevent request smuggling.
+  auto cl_range = headers.equal_range("Content-Length");
+  if (cl_range.first != cl_range.second) {
+    const auto &first_val = cl_range.first->second;
+    for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) {
+      if (it->second != first_val) { return false; }
+    }
+  }
+
+  return true;
+}
+
+inline bool read_websocket_upgrade_response(Stream &strm,
+                                            const std::string &expected_accept,
+                                            std::string &selected_subprotocol) {
+  // Read status line
+  const auto bufsiz = 2048;
+  char buf[bufsiz];
+  stream_line_reader line_reader(strm, buf, bufsiz);
+  if (!line_reader.getline()) { return false; }
+
+  // Check for "HTTP/1.1 101"
+  auto line = std::string(line_reader.ptr(), line_reader.size());
+  if (line.find("HTTP/1.1 101") == std::string::npos) { return false; }
+
+  // Parse headers using existing read_headers
+  Headers headers;
+  if (!read_headers(strm, headers)) { return false; }
+
+  // Verify Upgrade: websocket (case-insensitive)
+  auto upgrade_it = headers.find("Upgrade");
+  if (upgrade_it == headers.end()) { return false; }
+  auto upgrade_val = case_ignore::to_lower(upgrade_it->second);
+  if (upgrade_val != "websocket") { return false; }
+
+  // Verify Connection header contains "Upgrade" (case-insensitive)
+  auto connection_it = headers.find("Connection");
+  if (connection_it == headers.end()) { return false; }
+  auto connection_val = case_ignore::to_lower(connection_it->second);
+  if (connection_val.find("upgrade") == std::string::npos) { return false; }
+
+  // Verify Sec-WebSocket-Accept header value
+  auto it = headers.find("Sec-WebSocket-Accept");
+  if (it == headers.end() || it->second != expected_accept) { return false; }
+
+  // Extract negotiated subprotocol
+  auto proto_it = headers.find("Sec-WebSocket-Protocol");
+  if (proto_it != headers.end()) { selected_subprotocol = proto_it->second; }
+
+  return true;
+}
+
+enum class ReadContentResult {
+  Success,         // Successfully read the content
+  PayloadTooLarge, // The content exceeds the specified payload limit
+  Error            // An error occurred while reading the content
+};
+
+inline ReadContentResult read_content_with_length(
+    Stream &strm, size_t len, DownloadProgress progress,
+    ContentReceiverWithProgress out,
+    size_t payload_max_length = (std::numeric_limits<size_t>::max)()) {
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+
+  detail::BodyReader br;
+  br.stream = &strm;
+  br.has_content_length = true;
+  br.content_length = len;
+  br.payload_max_length = payload_max_length;
+  br.chunked = false;
+  br.bytes_read = 0;
+  br.last_error = Error::Success;
+
+  size_t r = 0;
+  while (r < len) {
+    auto read_len = static_cast<size_t>(len - r);
+    auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ);
+    auto n = detail::read_body_content(&strm, br, buf, to_read);
+    if (n <= 0) {
+      // Check if it was a payload size error
+      if (br.last_error == Error::ExceedMaxPayloadSize) {
+        return ReadContentResult::PayloadTooLarge;
+      }
+      return ReadContentResult::Error;
+    }
+
+    if (!out(buf, static_cast<size_t>(n), r, len)) {
+      return ReadContentResult::Error;
+    }
+    r += static_cast<size_t>(n);
+
+    if (progress) {
+      if (!progress(r, len)) { return ReadContentResult::Error; }
+    }
+  }
+
+  return ReadContentResult::Success;
+}
+
+inline ReadContentResult
+read_content_without_length(Stream &strm, size_t payload_max_length,
+                            ContentReceiverWithProgress out) {
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+  size_t r = 0;
+  for (;;) {
+    auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ);
+    if (n == 0) { return ReadContentResult::Success; }
+    if (n < 0) { return ReadContentResult::Error; }
+
+    // Check if adding this data would exceed the payload limit
+    if (r > payload_max_length ||
+        payload_max_length - r < static_cast<size_t>(n)) {
+      return ReadContentResult::PayloadTooLarge;
+    }
+
+    if (!out(buf, static_cast<size_t>(n), r, 0)) {
+      return ReadContentResult::Error;
+    }
+    r += static_cast<size_t>(n);
+  }
+
+  return ReadContentResult::Success;
+}
+
+template <typename T>
+inline ReadContentResult read_content_chunked(Stream &strm, T &x,
+                                              size_t payload_max_length,
+                                              ContentReceiverWithProgress out) {
+  detail::ChunkedDecoder dec(strm);
+
+  char buf[CPPHTTPLIB_RECV_BUFSIZ];
+  size_t total_len = 0;
+
+  for (;;) {
+    size_t chunk_offset = 0;
+    size_t chunk_total = 0;
+    auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total);
+    if (n < 0) { return ReadContentResult::Error; }
+
+    if (n == 0) {
+      if (!dec.parse_trailers_into(x.trailers, x.headers)) {
+        return ReadContentResult::Error;
+      }
+      return ReadContentResult::Success;
+    }
+
+    if (total_len > payload_max_length ||
+        payload_max_length - total_len < static_cast<size_t>(n)) {
+      return ReadContentResult::PayloadTooLarge;
+    }
+
+    if (!out(buf, static_cast<size_t>(n), chunk_offset, chunk_total)) {
+      return ReadContentResult::Error;
+    }
+
+    total_len += static_cast<size_t>(n);
+  }
+}
+
+inline bool is_chunked_transfer_encoding(const Headers &headers) {
+  return case_ignore::equal(
+      get_header_value(headers, "Transfer-Encoding", "", 0), "chunked");
+}
+
+template <typename T, typename U>
+bool prepare_content_receiver(T &x, int &status,
+                              ContentReceiverWithProgress receiver,
+                              bool decompress, size_t payload_max_length,
+                              bool &exceed_payload_max_length, U callback) {
+  if (decompress) {
+    std::string encoding = x.get_header_value("Content-Encoding");
+    std::unique_ptr<decompressor> decompressor;
+
+    if (!encoding.empty()) {
+      decompressor = detail::create_decompressor(encoding);
+      if (!decompressor) {
+        // Unsupported encoding or no support compiled in
+        status = StatusCode::UnsupportedMediaType_415;
+        return false;
+      }
+    }
+
+    if (decompressor) {
+      if (decompressor->is_valid()) {
+        size_t decompressed_size = 0;
+        ContentReceiverWithProgress out = [&](const char *buf, size_t n,
+                                              size_t off, size_t len) {
+          return decompressor->decompress(
+              buf, n, [&](const char *buf2, size_t n2) {
+                // Guard against zip-bomb: check
+                // decompressed size against limit.
+                if (payload_max_length > 0 &&
+                    (decompressed_size >= payload_max_length ||
+                     n2 > payload_max_length - decompressed_size)) {
+                  exceed_payload_max_length = true;
+                  return false;
+                }
+                decompressed_size += n2;
+                return receiver(buf2, n2, off, len);
+              });
+        };
+        return callback(std::move(out));
+      } else {
+        status = StatusCode::InternalServerError_500;
+        return false;
+      }
+    }
+  }
+
+  ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off,
+                                        size_t len) {
+    return receiver(buf, n, off, len);
+  };
+  return callback(std::move(out));
+}
+
+template <typename T>
+bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
+                  DownloadProgress progress,
+                  ContentReceiverWithProgress receiver, bool decompress) {
+  bool exceed_payload_max_length = false;
+  return prepare_content_receiver(
+      x, status, std::move(receiver), decompress, payload_max_length,
+      exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) {
+        auto ret = true;
+        // Note: exceed_payload_max_length may also be set by the decompressor
+        // wrapper in prepare_content_receiver when the decompressed payload
+        // size exceeds the limit.
+
+        if (is_chunked_transfer_encoding(x.headers)) {
+          auto result = read_content_chunked(strm, x, payload_max_length, out);
+          if (result == ReadContentResult::Success) {
+            ret = true;
+          } else if (result == ReadContentResult::PayloadTooLarge) {
+            exceed_payload_max_length = true;
+            ret = false;
+          } else {
+            ret = false;
+          }
+        } else if (!has_header(x.headers, "Content-Length")) {
+          auto result =
+              read_content_without_length(strm, payload_max_length, out);
+          if (result == ReadContentResult::Success) {
+            ret = true;
+          } else if (result == ReadContentResult::PayloadTooLarge) {
+            exceed_payload_max_length = true;
+            ret = false;
+          } else {
+            ret = false;
+          }
+        } else {
+          auto is_invalid_value = false;
+          auto len = get_header_value_u64(x.headers, "Content-Length",
+                                          (std::numeric_limits<size_t>::max)(),
+                                          0, is_invalid_value);
+
+          if (is_invalid_value) {
+            ret = false;
+          } else if (len > 0) {
+            auto result = read_content_with_length(
+                strm, len, std::move(progress), out, payload_max_length);
+            ret = (result == ReadContentResult::Success);
+            if (result == ReadContentResult::PayloadTooLarge) {
+              exceed_payload_max_length = true;
+            }
+          }
+        }
+
+        if (!ret) {
+          status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413
+                                             : StatusCode::BadRequest_400;
+        }
+        return ret;
+      });
+}
+
+inline ssize_t write_request_line(Stream &strm, const std::string &method,
+                                  const std::string &path) {
+  std::string s = method;
+  s += ' ';
+  s += path;
+  s += " HTTP/1.1\r\n";
+  return strm.write(s.data(), s.size());
+}
+
+inline ssize_t write_response_line(Stream &strm, int status) {
+  std::string s = "HTTP/1.1 ";
+  s += std::to_string(status);
+  s += ' ';
+  s += httplib::status_message(status);
+  s += "\r\n";
+  return strm.write(s.data(), s.size());
+}
+
+inline ssize_t write_headers(Stream &strm, const Headers &headers) {
+  ssize_t write_len = 0;
+  for (const auto &x : headers) {
+    std::string s;
+    s = x.first;
+    s += ": ";
+    s += x.second;
+    s += "\r\n";
+
+    auto len = strm.write(s.data(), s.size());
+    if (len < 0) { return len; }
+    write_len += len;
+  }
+  auto len = strm.write("\r\n");
+  if (len < 0) { return len; }
+  write_len += len;
+  return write_len;
+}
+
+inline bool write_data(Stream &strm, const char *d, size_t l) {
+  size_t offset = 0;
+  while (offset < l) {
+    auto length = strm.write(d + offset, l - offset);
+    if (length < 0) { return false; }
+    offset += static_cast<size_t>(length);
+  }
+  return true;
+}
+
+template <typename T>
+inline bool write_content_with_progress(Stream &strm,
+                                        const ContentProvider &content_provider,
+                                        size_t offset, size_t length,
+                                        T is_shutting_down,
+                                        const UploadProgress &upload_progress,
+                                        Error &error) {
+  size_t end_offset = offset + length;
+  size_t start_offset = offset;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      if (write_data(strm, d, l)) {
+        offset += l;
+
+        if (upload_progress && length > 0) {
+          size_t current_written = offset - start_offset;
+          if (!upload_progress(current_written, length)) {
+            ok = false;
+            return false;
+          }
+        }
+      } else {
+        ok = false;
+      }
+    }
+    return ok;
+  };
+
+  data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+  while (offset < end_offset && !is_shutting_down()) {
+    if (!strm.wait_writable() || !strm.is_peer_alive()) {
+      error = Error::Write;
+      return false;
+    } else if (!content_provider(offset, end_offset - offset, data_sink)) {
+      error = Error::Canceled;
+      return false;
+    } else if (!ok) {
+      error = Error::Write;
+      return false;
+    }
+  }
+
+  if (offset < end_offset) { // exited due to is_shutting_down(), not completion
+    error = Error::Write;
+    return false;
+  }
+
+  error = Error::Success;
+  return true;
+}
+
+template <typename T>
+inline bool write_content(Stream &strm, const ContentProvider &content_provider,
+                          size_t offset, size_t length, T is_shutting_down,
+                          Error &error) {
+  return write_content_with_progress<T>(strm, content_provider, offset, length,
+                                        is_shutting_down, nullptr, error);
+}
+
+template <typename T>
+inline bool write_content(Stream &strm, const ContentProvider &content_provider,
+                          size_t offset, size_t length,
+                          const T &is_shutting_down) {
+  auto error = Error::Success;
+  return write_content(strm, content_provider, offset, length, is_shutting_down,
+                       error);
+}
+
+template <typename T>
+inline bool
+write_content_without_length(Stream &strm,
+                             const ContentProvider &content_provider,
+                             const T &is_shutting_down) {
+  size_t offset = 0;
+  auto data_available = true;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      offset += l;
+      if (!write_data(strm, d, l)) { ok = false; }
+    }
+    return ok;
+  };
+
+  data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+  data_sink.done = [&](void) { data_available = false; };
+
+  while (data_available && !is_shutting_down()) {
+    if (!strm.wait_writable() || !strm.is_peer_alive()) {
+      return false;
+    } else if (!content_provider(offset, 0, data_sink)) {
+      return false;
+    } else if (!ok) {
+      return false;
+    }
+  }
+  return !data_available; // true only if done() was called, false if shutting
+                          // down
+}
+
+template <typename T, typename U>
+inline bool
+write_content_chunked(Stream &strm, const ContentProvider &content_provider,
+                      const T &is_shutting_down, U &compressor, Error &error) {
+  size_t offset = 0;
+  auto data_available = true;
+  auto ok = true;
+  DataSink data_sink;
+
+  data_sink.write = [&](const char *d, size_t l) -> bool {
+    if (ok) {
+      data_available = l > 0;
+      offset += l;
+
+      std::string payload;
+      if (compressor.compress(d, l, false,
+                              [&](const char *data, size_t data_len) {
+                                payload.append(data, data_len);
+                                return true;
+                              })) {
+        if (!payload.empty()) {
+          // Emit chunked response header and footer for each chunk
+          auto chunk =
+              from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+          if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; }
+        }
+      } else {
+        ok = false;
+      }
+    }
+    return ok;
+  };
+
+  data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+  auto done_with_trailer = [&](const Headers *trailer) {
+    if (!ok) { return; }
+
+    data_available = false;
+
+    std::string payload;
+    if (!compressor.compress(nullptr, 0, true,
+                             [&](const char *data, size_t data_len) {
+                               payload.append(data, data_len);
+                               return true;
+                             })) {
+      ok = false;
+      return;
+    }
+
+    if (!payload.empty()) {
+      // Emit chunked response header and footer for each chunk
+      auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+      if (!write_data(strm, chunk.data(), chunk.size())) {
+        ok = false;
+        return;
+      }
+    }
+
+    constexpr const char done_marker[] = "0\r\n";
+    if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; }
+
+    // Trailer
+    if (trailer) {
+      for (const auto &kv : *trailer) {
+        std::string field_line = kv.first + ": " + kv.second + "\r\n";
+        if (!write_data(strm, field_line.data(), field_line.size())) {
+          ok = false;
+        }
+      }
+    }
+
+    constexpr const char crlf[] = "\r\n";
+    if (!write_data(strm, crlf, str_len(crlf))) { ok = false; }
+  };
+
+  data_sink.done = [&](void) { done_with_trailer(nullptr); };
+
+  data_sink.done_with_trailer = [&](const Headers &trailer) {
+    done_with_trailer(&trailer);
+  };
+
+  while (data_available && !is_shutting_down()) {
+    if (!strm.wait_writable() || !strm.is_peer_alive()) {
+      error = Error::Write;
+      return false;
+    } else if (!content_provider(offset, 0, data_sink)) {
+      error = Error::Canceled;
+      return false;
+    } else if (!ok) {
+      error = Error::Write;
+      return false;
+    }
+  }
+
+  if (data_available) { // exited due to is_shutting_down(), not done()
+    error = Error::Write;
+    return false;
+  }
+
+  error = Error::Success;
+  return true;
+}
+
+template <typename T, typename U>
+inline bool write_content_chunked(Stream &strm,
+                                  const ContentProvider &content_provider,
+                                  const T &is_shutting_down, U &compressor) {
+  auto error = Error::Success;
+  return write_content_chunked(strm, content_provider, is_shutting_down,
+                               compressor, error);
+}
+
+template <typename T>
+inline bool redirect(T &cli, Request &req, Response &res,
+                     const std::string &path, const std::string &location,
+                     Error &error) {
+  Request new_req = req;
+  new_req.path = path;
+  new_req.redirect_count_ -= 1;
+
+  if (res.status == StatusCode::SeeOther_303 &&
+      (req.method != "GET" && req.method != "HEAD")) {
+    new_req.method = "GET";
+    new_req.body.clear();
+    new_req.headers.clear();
+  }
+
+  Response new_res;
+
+  auto ret = cli.send(new_req, new_res, error);
+  if (ret) {
+    req = std::move(new_req);
+    res = std::move(new_res);
+
+    if (res.location.empty()) { res.location = location; }
+  }
+  return ret;
+}
+
+inline std::string params_to_query_str(const Params &params) {
+  std::string query;
+
+  for (auto it = params.begin(); it != params.end(); ++it) {
+    if (it != params.begin()) { query += '&'; }
+    query += encode_query_component(it->first);
+    query += '=';
+    query += encode_query_component(it->second);
+  }
+  return query;
+}
+
+inline void parse_query_text(const char *data, std::size_t size,
+                             Params &params) {
+  std::set<std::string> cache;
+  split(data, data + size, '&', [&](const char *b, const char *e) {
+    std::string kv(b, e);
+    if (cache.find(kv) != cache.end()) { return; }
+    cache.insert(std::move(kv));
+
+    std::string key;
+    std::string val;
+    divide(b, static_cast<std::size_t>(e - b), '=',
+           [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
+               std::size_t rhs_size) {
+             key.assign(lhs_data, lhs_size);
+             val.assign(rhs_data, rhs_size);
+           });
+
+    if (!key.empty()) {
+      params.emplace(decode_query_component(key), decode_query_component(val));
+    }
+  });
+}
+
+inline void parse_query_text(const std::string &s, Params &params) {
+  parse_query_text(s.data(), s.size(), params);
+}
+
+// Normalize a query string by decoding and re-encoding each key/value pair
+// while preserving the original parameter order. This avoids double-encoding
+// and ensures consistent encoding without reordering (unlike Params which
+// uses std::multimap and sorts keys).
+inline std::string normalize_query_string(const std::string &query) {
+  std::string result;
+  split(query.data(), query.data() + query.size(), '&',
+        [&](const char *b, const char *e) {
+          std::string key;
+          std::string val;
+          divide(b, static_cast<std::size_t>(e - b), '=',
+                 [&](const char *lhs_data, std::size_t lhs_size,
+                     const char *rhs_data, std::size_t rhs_size) {
+                   key.assign(lhs_data, lhs_size);
+                   val.assign(rhs_data, rhs_size);
+                 });
+
+          if (!key.empty()) {
+            auto dec_key = decode_query_component(key);
+            auto dec_val = decode_query_component(val);
+
+            if (!result.empty()) { result += '&'; }
+            result += encode_query_component(dec_key);
+            if (!val.empty() || std::find(b, e, '=') != e) {
+              result += '=';
+              result += encode_query_component(dec_val);
+            }
+          }
+        });
+  return result;
+}
+
+inline bool parse_multipart_boundary(const std::string &content_type,
+                                     std::string &boundary) {
+  std::map<std::string, std::string> params;
+  extract_media_type(content_type, &params);
+  auto it = params.find("boundary");
+  if (it == params.end()) { return false; }
+  boundary = it->second;
+  return !boundary.empty();
+}
+
+inline void parse_disposition_params(const std::string &s, Params &params) {
+  std::set<std::string> cache;
+  split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {
+    std::string kv(b, e);
+    if (cache.find(kv) != cache.end()) { return; }
+    cache.insert(kv);
+
+    std::string key;
+    std::string val;
+    split(b, e, '=', [&](const char *b2, const char *e2) {
+      if (key.empty()) {
+        key.assign(b2, e2);
+      } else {
+        val.assign(b2, e2);
+      }
+    });
+
+    if (!key.empty()) {
+      params.emplace(trim_double_quotes_copy((key)),
+                     trim_double_quotes_copy((val)));
+    }
+  });
+}
+
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+inline bool parse_range_header(const std::string &s, Ranges &ranges) {
+#else
+inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
+#endif
+  auto is_valid = [](const std::string &str) {
+    return std::all_of(str.cbegin(), str.cend(),
+                       [](unsigned char c) { return std::isdigit(c); });
+  };
+
+  if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
+    const auto pos = static_cast<size_t>(6);
+    const auto len = static_cast<size_t>(s.size() - 6);
+    auto all_valid_ranges = true;
+    split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {
+      if (!all_valid_ranges) { return; }
+
+      const auto it = std::find(b, e, '-');
+      if (it == e) {
+        all_valid_ranges = false;
+        return;
+      }
+
+      const auto lhs = std::string(b, it);
+      const auto rhs = std::string(it + 1, e);
+      if (!is_valid(lhs) || !is_valid(rhs)) {
+        all_valid_ranges = false;
+        return;
+      }
+
+      ssize_t first = -1;
+      if (!lhs.empty()) {
+        ssize_t v;
+        auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v);
+        if (res.ec == std::errc{}) { first = v; }
+      }
+
+      ssize_t last = -1;
+      if (!rhs.empty()) {
+        ssize_t v;
+        auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v);
+        if (res.ec == std::errc{}) { last = v; }
+      }
+
+      if ((first == -1 && last == -1) ||
+          (first != -1 && last != -1 && first > last)) {
+        all_valid_ranges = false;
+        return;
+      }
+
+      ranges.emplace_back(first, last);
+    });
+    return all_valid_ranges && !ranges.empty();
+  }
+  return false;
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+}
+#else
+} catch (...) { return false; }
+#endif
+
+inline bool parse_accept_header(const std::string &s,
+                                std::vector<std::string> &content_types) {
+  content_types.clear();
+
+  // Empty string is considered valid (no preference)
+  if (s.empty()) { return true; }
+
+  // Check for invalid patterns: leading/trailing commas or consecutive commas
+  if (s.front() == ',' || s.back() == ',' ||
+      s.find(",,") != std::string::npos) {
+    return false;
+  }
+
+  struct AcceptEntry {
+    std::string media_type;
+    double quality;
+    int order;
+  };
+
+  std::vector<AcceptEntry> entries;
+  int order = 0;
+  bool has_invalid_entry = false;
+
+  // Split by comma and parse each entry
+  split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
+    std::string entry(b, e);
+    entry = trim_copy(entry);
+
+    if (entry.empty()) {
+      has_invalid_entry = true;
+      return;
+    }
+
+    AcceptEntry accept_entry;
+    accept_entry.order = order++;
+
+    if (!parse_quality(entry.data(), entry.data() + entry.size(),
+                       accept_entry.media_type, accept_entry.quality)) {
+      has_invalid_entry = true;
+      return;
+    }
+
+    // Remove additional parameters from media type
+    accept_entry.media_type = extract_media_type(accept_entry.media_type);
+
+    // Basic validation of media type format
+    if (accept_entry.media_type.empty()) {
+      has_invalid_entry = true;
+      return;
+    }
+
+    // Check for basic media type format (should contain '/' or be '*')
+    if (accept_entry.media_type != "*" &&
+        accept_entry.media_type.find('/') == std::string::npos) {
+      has_invalid_entry = true;
+      return;
+    }
+
+    entries.push_back(std::move(accept_entry));
+  });
+
+  // Return false if any invalid entry was found
+  if (has_invalid_entry) { return false; }
+
+  // Sort by quality (descending), then by original order (ascending)
+  std::sort(entries.begin(), entries.end(),
+            [](const AcceptEntry &a, const AcceptEntry &b) {
+              if (a.quality != b.quality) {
+                return a.quality > b.quality; // Higher quality first
+              }
+              return a.order < b.order; // Earlier order first for same quality
+            });
+
+  // Extract sorted media types
+  content_types.reserve(entries.size());
+  for (auto &entry : entries) {
+    content_types.push_back(std::move(entry.media_type));
+  }
+
+  return true;
+}
+
+class FormDataParser {
+public:
+  FormDataParser() = default;
+
+  void set_boundary(std::string &&boundary) {
+    boundary_ = std::move(boundary);
+    dash_boundary_crlf_ = dash_ + boundary_ + crlf_;
+    crlf_dash_boundary_ = crlf_ + dash_ + boundary_;
+  }
+
+  bool is_valid() const { return is_valid_; }
+
+  bool parse(const char *buf, size_t n, const FormDataHeader &header_callback,
+             const ContentReceiver &content_callback) {
+
+    buf_append(buf, n);
+
+    while (buf_size() > 0) {
+      switch (state_) {
+      case 0: { // Initial boundary
+        auto pos = buf_find(dash_boundary_crlf_);
+        if (pos == buf_size()) { return true; }
+        buf_erase(pos + dash_boundary_crlf_.size());
+        state_ = 1;
+        break;
+      }
+      case 1: { // New entry
+        clear_file_info();
+        state_ = 2;
+        break;
+      }
+      case 2: { // Headers
+        auto pos = buf_find(crlf_);
+        if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
+        while (pos < buf_size()) {
+          // Empty line
+          if (pos == 0) {
+            if (!header_callback(file_)) {
+              is_valid_ = false;
+              return false;
+            }
+            buf_erase(crlf_.size());
+            state_ = 3;
+            break;
+          }
+
+          const auto header = buf_head(pos);
+
+          if (!parse_header(header.data(), header.data() + header.size(),
+                            [&](const std::string &, const std::string &) {})) {
+            is_valid_ = false;
+            return false;
+          }
+
+          // Parse and emplace space trimmed headers into a map
+          if (!parse_header(
+                  header.data(), header.data() + header.size(),
+                  [&](const std::string &key, const std::string &val) {
+                    file_.headers.emplace(key, val);
+                  })) {
+            is_valid_ = false;
+            return false;
+          }
+
+          constexpr const char header_content_type[] = "Content-Type:";
+
+          if (start_with_case_ignore(header, header_content_type)) {
+            file_.content_type =
+                trim_copy(header.substr(str_len(header_content_type)));
+          } else {
+            std::string disposition_params;
+            if (parse_content_disposition(header, disposition_params)) {
+              Params params;
+              parse_disposition_params(disposition_params, params);
+
+              auto it = params.find("name");
+              if (it != params.end()) {
+                file_.name = it->second;
+              } else {
+                is_valid_ = false;
+                return false;
+              }
+
+              it = params.find("filename");
+              if (it != params.end()) { file_.filename = it->second; }
+
+              it = params.find("filename*");
+              if (it != params.end()) {
+                // RFC 5987: only UTF-8 encoding is allowed
+                const auto &val = it->second;
+                constexpr const char utf8_prefix[] = "UTF-8''";
+                constexpr size_t prefix_len = str_len(utf8_prefix);
+                if (val.size() > prefix_len &&
+                    start_with_case_ignore(val, utf8_prefix)) {
+                  file_.filename = decode_path_component(
+                      val.substr(prefix_len)); // override...
+                } else {
+                  is_valid_ = false;
+                  return false;
+                }
+              }
+            }
+          }
+          buf_erase(pos + crlf_.size());
+          pos = buf_find(crlf_);
+        }
+        if (state_ != 3) { return true; }
+        break;
+      }
+      case 3: { // Body
+        if (crlf_dash_boundary_.size() > buf_size()) { return true; }
+        auto pos = buf_find(crlf_dash_boundary_);
+        if (pos < buf_size()) {
+          if (!content_callback(buf_data(), pos)) {
+            is_valid_ = false;
+            return false;
+          }
+          buf_erase(pos + crlf_dash_boundary_.size());
+          state_ = 4;
+        } else {
+          auto len = buf_size() - crlf_dash_boundary_.size();
+          if (len > 0) {
+            if (!content_callback(buf_data(), len)) {
+              is_valid_ = false;
+              return false;
+            }
+            buf_erase(len);
+          }
+          return true;
+        }
+        break;
+      }
+      case 4: { // Boundary
+        if (crlf_.size() > buf_size()) { return true; }
+        if (buf_start_with(crlf_)) {
+          buf_erase(crlf_.size());
+          state_ = 1;
+        } else {
+          if (dash_.size() > buf_size()) { return true; }
+          if (buf_start_with(dash_)) {
+            buf_erase(dash_.size());
+            is_valid_ = true;
+            buf_erase(buf_size()); // Remove epilogue
+          } else {
+            return true;
+          }
+        }
+        break;
+      }
+      }
+    }
+
+    return true;
+  }
+
+private:
+  void clear_file_info() {
+    file_.name.clear();
+    file_.filename.clear();
+    file_.content_type.clear();
+    file_.headers.clear();
+  }
+
+  bool start_with_case_ignore(const std::string &a, const char *b,
+                              size_t offset = 0) const {
+    const auto b_len = strlen(b);
+    if (a.size() < offset + b_len) { return false; }
+    for (size_t i = 0; i < b_len; i++) {
+      if (case_ignore::to_lower(a[offset + i]) != case_ignore::to_lower(b[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // Parses "Content-Disposition: form-data; <params>" without std::regex.
+  // Returns true if header matches, with the params portion in `params_out`.
+  bool parse_content_disposition(const std::string &header,
+                                 std::string &params_out) const {
+    constexpr const char prefix[] = "Content-Disposition:";
+    constexpr size_t prefix_len = str_len(prefix);
+
+    if (!start_with_case_ignore(header, prefix)) { return false; }
+
+    // Skip whitespace after "Content-Disposition:"
+    auto pos = prefix_len;
+    while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) {
+      pos++;
+    }
+
+    // Match "form-data;" (case-insensitive)
+    constexpr const char form_data[] = "form-data;";
+    constexpr size_t form_data_len = str_len(form_data);
+    if (!start_with_case_ignore(header, form_data, pos)) { return false; }
+    pos += form_data_len;
+
+    // Skip whitespace after "form-data;"
+    while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) {
+      pos++;
+    }
+
+    params_out = header.substr(pos);
+    return true;
+  }
+
+  const std::string dash_ = "--";
+  const std::string crlf_ = "\r\n";
+  std::string boundary_;
+  std::string dash_boundary_crlf_;
+  std::string crlf_dash_boundary_;
+
+  size_t state_ = 0;
+  bool is_valid_ = false;
+  FormData file_;
+
+  // Buffer
+  bool start_with(const std::string &a, size_t spos, size_t epos,
+                  const std::string &b) const {
+    if (epos - spos < b.size()) { return false; }
+    for (size_t i = 0; i < b.size(); i++) {
+      if (a[i + spos] != b[i]) { return false; }
+    }
+    return true;
+  }
+
+  size_t buf_size() const { return buf_epos_ - buf_spos_; }
+
+  const char *buf_data() const { return &buf_[buf_spos_]; }
+
+  std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); }
+
+  bool buf_start_with(const std::string &s) const {
+    return start_with(buf_, buf_spos_, buf_epos_, s);
+  }
+
+  size_t buf_find(const std::string &s) const {
+    auto c = s.front();
+
+    size_t off = buf_spos_;
+    while (off < buf_epos_) {
+      auto pos = off;
+      while (true) {
+        if (pos == buf_epos_) { return buf_size(); }
+        if (buf_[pos] == c) { break; }
+        pos++;
+      }
+
+      auto remaining_size = buf_epos_ - pos;
+      if (s.size() > remaining_size) { return buf_size(); }
+
+      if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; }
+
+      off = pos + 1;
+    }
+
+    return buf_size();
+  }
+
+  void buf_append(const char *data, size_t n) {
+    auto remaining_size = buf_size();
+    if (remaining_size > 0 && buf_spos_ > 0) {
+      for (size_t i = 0; i < remaining_size; i++) {
+        buf_[i] = buf_[buf_spos_ + i];
+      }
+    }
+    buf_spos_ = 0;
+    buf_epos_ = remaining_size;
+
+    if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); }
+
+    for (size_t i = 0; i < n; i++) {
+      buf_[buf_epos_ + i] = data[i];
+    }
+    buf_epos_ += n;
+  }
+
+  void buf_erase(size_t size) { buf_spos_ += size; }
+
+  std::string buf_;
+  size_t buf_spos_ = 0;
+  size_t buf_epos_ = 0;
+};
+
+inline std::string random_string(size_t length) {
+  constexpr const char data[] =
+      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+  thread_local auto engine([]() {
+    // std::random_device might actually be deterministic on some
+    // platforms, but due to lack of support in the c++ standard library,
+    // doing better requires either some ugly hacks or breaking portability.
+    std::random_device seed_gen;
+    // Request 128 bits of entropy for initialization
+    std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};
+    return std::mt19937(seed_sequence);
+  }());
+
+  std::string result;
+  for (size_t i = 0; i < length; i++) {
+    result += data[engine() % (sizeof(data) - 1)];
+  }
+  return result;
+}
+
+inline std::string make_multipart_data_boundary() {
+  return "--cpp-httplib-multipart-data-" + detail::random_string(16);
+}
+
+inline bool is_multipart_boundary_chars_valid(const std::string &boundary) {
+  auto valid = true;
+  for (size_t i = 0; i < boundary.size(); i++) {
+    auto c = boundary[i];
+    if (!std::isalnum(c) && c != '-' && c != '_') {
+      valid = false;
+      break;
+    }
+  }
+  return valid;
+}
+
+template <typename T>
+inline std::string
+serialize_multipart_formdata_item_begin(const T &item,
+                                        const std::string &boundary) {
+  std::string body = "--" + boundary + "\r\n";
+  body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
+  if (!item.filename.empty()) {
+    body += "; filename=\"" + item.filename + "\"";
+  }
+  body += "\r\n";
+  if (!item.content_type.empty()) {
+    body += "Content-Type: " + item.content_type + "\r\n";
+  }
+  body += "\r\n";
+
+  return body;
+}
+
+inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; }
+
+inline std::string
+serialize_multipart_formdata_finish(const std::string &boundary) {
+  return "--" + boundary + "--\r\n";
+}
+
+inline std::string
+serialize_multipart_formdata_get_content_type(const std::string &boundary) {
+  return "multipart/form-data; boundary=" + boundary;
+}
+
+inline std::string
+serialize_multipart_formdata(const UploadFormDataItems &items,
+                             const std::string &boundary, bool finish = true) {
+  std::string body;
+
+  for (const auto &item : items) {
+    body += serialize_multipart_formdata_item_begin(item, boundary);
+    body += item.content + serialize_multipart_formdata_item_end();
+  }
+
+  if (finish) { body += serialize_multipart_formdata_finish(boundary); }
+
+  return body;
+}
+
+inline size_t get_multipart_content_length(const UploadFormDataItems &items,
+                                           const std::string &boundary) {
+  size_t total = 0;
+  for (const auto &item : items) {
+    total += serialize_multipart_formdata_item_begin(item, boundary).size();
+    total += item.content.size();
+    total += serialize_multipart_formdata_item_end().size();
+  }
+  total += serialize_multipart_formdata_finish(boundary).size();
+  return total;
+}
+
+struct MultipartSegment {
+  const char *data;
+  size_t size;
+};
+
+// NOTE: items must outlive the returned ContentProvider
+//       (safe for synchronous use inside Post/Put/Patch)
+inline ContentProvider
+make_multipart_content_provider(const UploadFormDataItems &items,
+                                const std::string &boundary) {
+  // Own the per-item header strings and the finish string
+  std::vector<std::string> owned;
+  owned.reserve(items.size() + 1);
+  for (const auto &item : items)
+    owned.push_back(serialize_multipart_formdata_item_begin(item, boundary));
+  owned.push_back(serialize_multipart_formdata_finish(boundary));
+
+  // Flat segment list: [header, content, "\r\n"] * N + [finish]
+  std::vector<MultipartSegment> segs;
+  segs.reserve(items.size() * 3 + 1);
+  static const char crlf[] = "\r\n";
+  for (size_t i = 0; i < items.size(); i++) {
+    segs.push_back({owned[i].data(), owned[i].size()});
+    segs.push_back({items[i].content.data(), items[i].content.size()});
+    segs.push_back({crlf, 2});
+  }
+  segs.push_back({owned.back().data(), owned.back().size()});
+
+  struct MultipartState {
+    std::vector<std::string> owned;
+    std::vector<MultipartSegment> segs;
+    std::vector<char> buf = std::vector<char>(CPPHTTPLIB_SEND_BUFSIZ);
+  };
+  auto state = std::make_shared<MultipartState>();
+  state->owned = std::move(owned);
+  // `segs` holds raw pointers into owned strings; std::string move preserves
+  // the data pointer, so these pointers remain valid after the move above.
+  state->segs = std::move(segs);
+
+  return [state](size_t offset, size_t length, DataSink &sink) -> bool {
+    // Buffer multiple small segments into fewer, larger writes to avoid
+    // excessive TCP packets when there are many form data items (#2410)
+    auto &buf = state->buf;
+    auto buf_size = buf.size();
+    size_t buf_len = 0;
+    size_t remaining = length;
+
+    // Find the first segment containing 'offset'
+    size_t pos = 0;
+    size_t seg_idx = 0;
+    for (; seg_idx < state->segs.size(); seg_idx++) {
+      const auto &seg = state->segs[seg_idx];
+      if (seg.size > 0 && offset - pos < seg.size) { break; }
+      pos += seg.size;
+    }
+
+    size_t seg_offset = (seg_idx < state->segs.size()) ? offset - pos : 0;
+
+    for (; seg_idx < state->segs.size() && remaining > 0; seg_idx++) {
+      const auto &seg = state->segs[seg_idx];
+      size_t available = seg.size - seg_offset;
+      size_t to_copy = (std::min)(available, remaining);
+      const char *src = seg.data + seg_offset;
+      seg_offset = 0; // only the first segment has a non-zero offset
+
+      while (to_copy > 0) {
+        size_t space = buf_size - buf_len;
+        size_t chunk = (std::min)(to_copy, space);
+        std::memcpy(buf.data() + buf_len, src, chunk);
+        buf_len += chunk;
+        src += chunk;
+        to_copy -= chunk;
+        remaining -= chunk;
+
+        if (buf_len == buf_size) {
+          if (!sink.write(buf.data(), buf_len)) { return false; }
+          buf_len = 0;
+        }
+      }
+    }
+
+    if (buf_len > 0) { return sink.write(buf.data(), buf_len); }
+    return true;
+  };
+}
+
+inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
+  if (ranges.size() <= 1) return;
+
+  // Sort ranges by start position
+  std::sort(ranges.begin(), ranges.end(),
+            [](const Range &a, const Range &b) { return a.first < b.first; });
+
+  Ranges coalesced;
+  coalesced.reserve(ranges.size());
+
+  for (auto &r : ranges) {
+    auto first_pos = r.first;
+    auto last_pos = r.second;
+
+    // Handle special cases like in range_error
+    if (first_pos == -1 && last_pos == -1) {
+      first_pos = 0;
+      last_pos = static_cast<ssize_t>(content_length);
+    }
+
+    if (first_pos == -1) {
+      first_pos = static_cast<ssize_t>(content_length) - last_pos;
+      last_pos = static_cast<ssize_t>(content_length) - 1;
+    }
+
+    if (last_pos == -1 || last_pos >= static_cast<ssize_t>(content_length)) {
+      last_pos = static_cast<ssize_t>(content_length) - 1;
+    }
+
+    // Skip invalid ranges
+    if (!(0 <= first_pos && first_pos <= last_pos &&
+          last_pos < static_cast<ssize_t>(content_length))) {
+      continue;
+    }
+
+    // Coalesce with previous range if overlapping or adjacent (but not
+    // identical)
+    if (!coalesced.empty()) {
+      auto &prev = coalesced.back();
+      // Check if current range overlaps or is adjacent to previous range
+      // but don't coalesce identical ranges (allow duplicates)
+      if (first_pos <= prev.second + 1 &&
+          !(first_pos == prev.first && last_pos == prev.second)) {
+        // Extend the previous range
+        prev.second = (std::max)(prev.second, last_pos);
+        continue;
+      }
+    }
+
+    // Add new range
+    coalesced.emplace_back(first_pos, last_pos);
+  }
+
+  ranges = std::move(coalesced);
+}
+
+inline bool range_error(Request &req, Response &res) {
+  if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
+    ssize_t content_len = static_cast<ssize_t>(
+        res.content_length_ ? res.content_length_ : res.body.size());
+
+    std::vector<std::pair<ssize_t, ssize_t>> processed_ranges;
+    size_t overwrapping_count = 0;
+
+    // NOTE: The following Range check is based on '14.2. Range' in RFC 9110
+    // 'HTTP Semantics' to avoid potential denial-of-service attacks.
+    // https://www.rfc-editor.org/rfc/rfc9110#section-14.2
+
+    // Too many ranges
+    if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; }
+
+    for (auto &r : req.ranges) {
+      auto &first_pos = r.first;
+      auto &last_pos = r.second;
+
+      if (first_pos == -1 && last_pos == -1) {
+        first_pos = 0;
+        last_pos = content_len;
+      }
+
+      if (first_pos == -1) {
+        first_pos = content_len - last_pos;
+        last_pos = content_len - 1;
+      }
+
+      // NOTE: RFC-9110 '14.1.2. Byte Ranges':
+      // A client can limit the number of bytes requested without knowing the
+      // size of the selected representation. If the last-pos value is absent,
+      // or if the value is greater than or equal to the current length of the
+      // representation data, the byte range is interpreted as the remainder of
+      // the representation (i.e., the server replaces the value of last-pos
+      // with a value that is one less than the current length of the selected
+      // representation).
+      // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
+      if (last_pos == -1 || last_pos >= content_len) {
+        last_pos = content_len - 1;
+      }
+
+      // Range must be within content length
+      if (!(0 <= first_pos && first_pos <= last_pos &&
+            last_pos <= content_len - 1)) {
+        return true;
+      }
+
+      // Request must not have more than two overlapping ranges
+      for (const auto &processed_range : processed_ranges) {
+        if (!(last_pos < processed_range.first ||
+              first_pos > processed_range.second)) {
+          overwrapping_count++;
+          if (overwrapping_count > 2) { return true; }
+          break; // Only count once per range
+        }
+      }
+
+      processed_ranges.emplace_back(first_pos, last_pos);
+    }
+
+    // After validation, coalesce overlapping ranges as per RFC 9110
+    coalesce_ranges(req.ranges, static_cast<size_t>(content_len));
+  }
+
+  return false;
+}
+
+inline std::pair<size_t, size_t>
+get_range_offset_and_length(Range r, size_t content_length) {
+  assert(r.first != -1 && r.second != -1);
+  assert(0 <= r.first && r.first < static_cast<ssize_t>(content_length));
+  assert(r.first <= r.second &&
+         r.second < static_cast<ssize_t>(content_length));
+  (void)(content_length);
+  return std::make_pair(static_cast<size_t>(r.first),
+                        static_cast<size_t>(r.second - r.first) + 1);
+}
+
+inline std::string make_content_range_header_field(
+    const std::pair<size_t, size_t> &offset_and_length, size_t content_length) {
+  auto st = offset_and_length.first;
+  auto ed = st + offset_and_length.second - 1;
+
+  std::string field = "bytes ";
+  field += std::to_string(st);
+  field += '-';
+  field += std::to_string(ed);
+  field += '/';
+  field += std::to_string(content_length);
+  return field;
+}
+
+template <typename SToken, typename CToken, typename Content>
+bool process_multipart_ranges_data(const Request &req,
+                                   const std::string &boundary,
+                                   const std::string &content_type,
+                                   size_t content_length, SToken stoken,
+                                   CToken ctoken, Content content) {
+  for (size_t i = 0; i < req.ranges.size(); i++) {
+    ctoken("--");
+    stoken(boundary);
+    ctoken("\r\n");
+    if (!content_type.empty()) {
+      ctoken("Content-Type: ");
+      stoken(content_type);
+      ctoken("\r\n");
+    }
+
+    auto offset_and_length =
+        get_range_offset_and_length(req.ranges[i], content_length);
+
+    ctoken("Content-Range: ");
+    stoken(make_content_range_header_field(offset_and_length, content_length));
+    ctoken("\r\n");
+    ctoken("\r\n");
+
+    if (!content(offset_and_length.first, offset_and_length.second)) {
+      return false;
+    }
+    ctoken("\r\n");
+  }
+
+  ctoken("--");
+  stoken(boundary);
+  ctoken("--");
+
+  return true;
+}
+
+inline void make_multipart_ranges_data(const Request &req, Response &res,
+                                       const std::string &boundary,
+                                       const std::string &content_type,
+                                       size_t content_length,
+                                       std::string &data) {
+  process_multipart_ranges_data(
+      req, boundary, content_type, content_length,
+      [&](const std::string &token) { data += token; },
+      [&](const std::string &token) { data += token; },
+      [&](size_t offset, size_t length) {
+        assert(offset + length <= content_length);
+        data += res.body.substr(offset, length);
+        return true;
+      });
+}
+
+inline size_t get_multipart_ranges_data_length(const Request &req,
+                                               const std::string &boundary,
+                                               const std::string &content_type,
+                                               size_t content_length) {
+  size_t data_length = 0;
+
+  process_multipart_ranges_data(
+      req, boundary, content_type, content_length,
+      [&](const std::string &token) { data_length += token.size(); },
+      [&](const std::string &token) { data_length += token.size(); },
+      [&](size_t /*offset*/, size_t length) {
+        data_length += length;
+        return true;
+      });
+
+  return data_length;
+}
+
+template <typename T>
+inline bool
+write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
+                            const std::string &boundary,
+                            const std::string &content_type,
+                            size_t content_length, const T &is_shutting_down) {
+  return process_multipart_ranges_data(
+      req, boundary, content_type, content_length,
+      [&](const std::string &token) { strm.write(token); },
+      [&](const std::string &token) { strm.write(token); },
+      [&](size_t offset, size_t length) {
+        return write_content(strm, res.content_provider_, offset, length,
+                             is_shutting_down);
+      });
+}
+
+inline bool expect_content(const Request &req) {
+  if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
+      req.method == "DELETE") {
+    return true;
+  }
+  if (req.has_header("Content-Length") &&
+      req.get_header_value_u64("Content-Length") > 0) {
+    return true;
+  }
+  if (is_chunked_transfer_encoding(req.headers)) { return true; }
+  return false;
+}
+
+#ifdef _WIN32
+class WSInit {
+public:
+  WSInit() {
+    WSADATA wsaData;
+    if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true;
+  }
+
+  ~WSInit() {
+    if (is_valid_) WSACleanup();
+  }
+
+  bool is_valid_ = false;
+};
+
+static WSInit wsinit_;
+#endif
+
+inline bool parse_www_authenticate(const Response &res,
+                                   std::map<std::string, std::string> &auth,
+                                   bool is_proxy) {
+  auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
+  if (res.has_header(auth_key)) {
+    thread_local auto re =
+        std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~");
+    auto s = res.get_header_value(auth_key);
+    auto pos = s.find(' ');
+    if (pos != std::string::npos) {
+      auto type = s.substr(0, pos);
+      if (type == "Basic") {
+        return false;
+      } else if (type == "Digest") {
+        s = s.substr(pos + 1);
+        auto beg = std::sregex_iterator(s.begin(), s.end(), re);
+        for (auto i = beg; i != std::sregex_iterator(); ++i) {
+          const auto &m = *i;
+          auto key = s.substr(static_cast<size_t>(m.position(1)),
+                              static_cast<size_t>(m.length(1)));
+          auto val = m.length(2) > 0
+                         ? s.substr(static_cast<size_t>(m.position(2)),
+                                    static_cast<size_t>(m.length(2)))
+                         : s.substr(static_cast<size_t>(m.position(3)),
+                                    static_cast<size_t>(m.length(3)));
+          auth[std::move(key)] = std::move(val);
+        }
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+class ContentProviderAdapter {
+public:
+  explicit ContentProviderAdapter(
+      ContentProviderWithoutLength &&content_provider)
+      : content_provider_(std::move(content_provider)) {}
+
+  bool operator()(size_t offset, size_t, DataSink &sink) {
+    return content_provider_(offset, sink);
+  }
+
+private:
+  ContentProviderWithoutLength content_provider_;
+};
+
+// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
+namespace fields {
+
+inline bool is_token_char(char c) {
+  return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
+         c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
+         c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
+}
+
+inline bool is_token(const std::string &s) {
+  if (s.empty()) { return false; }
+  for (auto c : s) {
+    if (!is_token_char(c)) { return false; }
+  }
+  return true;
+}
+
+inline bool is_field_name(const std::string &s) { return is_token(s); }
+
+inline bool is_vchar(char c) { return c >= 33 && c <= 126; }
+
+inline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }
+
+inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }
+
+inline bool is_field_content(const std::string &s) {
+  if (s.empty()) { return true; }
+
+  if (s.size() == 1) {
+    return is_field_vchar(s[0]);
+  } else if (s.size() == 2) {
+    return is_field_vchar(s[0]) && is_field_vchar(s[1]);
+  } else {
+    size_t i = 0;
+
+    if (!is_field_vchar(s[i])) { return false; }
+    i++;
+
+    while (i < s.size() - 1) {
+      auto c = s[i++];
+      if (c == ' ' || c == '\t' || is_field_vchar(c)) {
+      } else {
+        return false;
+      }
+    }
+
+    return is_field_vchar(s[i]);
+  }
+}
+
+inline bool is_field_value(const std::string &s) { return is_field_content(s); }
+
+} // namespace fields
+
+inline bool perform_websocket_handshake(Stream &strm, const std::string &host,
+                                        int port, const std::string &path,
+                                        const Headers &headers,
+                                        std::string &selected_subprotocol) {
+  // Validate path and host
+  if (!fields::is_field_value(path) || !fields::is_field_value(host)) {
+    return false;
+  }
+
+  // Validate user-provided headers
+  for (const auto &h : headers) {
+    if (!fields::is_field_name(h.first) || !fields::is_field_value(h.second)) {
+      return false;
+    }
+  }
+
+  // Generate random Sec-WebSocket-Key
+  thread_local std::mt19937 rng(std::random_device{}());
+  std::string key_bytes(16, '\0');
+  for (size_t i = 0; i < 16; i += 4) {
+    auto r = rng();
+    std::memcpy(&key_bytes[i], &r, (std::min)(size_t(4), size_t(16 - i)));
+  }
+  auto client_key = base64_encode(key_bytes);
+
+  // Build upgrade request
+  std::string req_str = "GET " + path + " HTTP/1.1\r\n";
+  req_str += "Host: " + host + ":" + std::to_string(port) + "\r\n";
+  req_str += "Upgrade: websocket\r\n";
+  req_str += "Connection: Upgrade\r\n";
+  req_str += "Sec-WebSocket-Key: " + client_key + "\r\n";
+  req_str += "Sec-WebSocket-Version: 13\r\n";
+  for (const auto &h : headers) {
+    req_str += h.first + ": " + h.second + "\r\n";
+  }
+  req_str += "\r\n";
+
+  if (strm.write(req_str.data(), req_str.size()) < 0) { return false; }
+
+  // Verify 101 response and Sec-WebSocket-Accept header
+  auto expected_accept = websocket_accept_key(client_key);
+  return read_websocket_upgrade_response(strm, expected_accept,
+                                         selected_subprotocol);
+}
+
+} // namespace detail
+
+/*
+ * Group 2: detail namespace - SSL common utilities
+ */
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+namespace detail {
+
+class SSLSocketStream final : public Stream {
+public:
+  SSLSocketStream(
+      socket_t sock, tls::session_t session, time_t read_timeout_sec,
+      time_t read_timeout_usec, time_t write_timeout_sec,
+      time_t write_timeout_usec, time_t max_timeout_msec = 0,
+      std::chrono::time_point<std::chrono::steady_clock> start_time =
+          (std::chrono::steady_clock::time_point::min)());
+  ~SSLSocketStream() override;
+
+  bool is_readable() const override;
+  bool wait_readable() const override;
+  bool wait_writable() const override;
+  bool is_peer_alive() const override;
+  ssize_t read(char *ptr, size_t size) override;
+  ssize_t write(const char *ptr, size_t size) override;
+  void get_remote_ip_and_port(std::string &ip, int &port) const override;
+  void get_local_ip_and_port(std::string &ip, int &port) const override;
+  socket_t socket() const override;
+  time_t duration() const override;
+  void set_read_timeout(time_t sec, time_t usec = 0) override;
+
+private:
+  socket_t sock_;
+  tls::session_t session_;
+  time_t read_timeout_sec_;
+  time_t read_timeout_usec_;
+  time_t write_timeout_sec_;
+  time_t write_timeout_usec_;
+  time_t max_timeout_msec_;
+  const std::chrono::time_point<std::chrono::steady_clock> start_time_;
+};
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+inline std::string message_digest(const std::string &s, const EVP_MD *algo) {
+  auto context = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
+      EVP_MD_CTX_new(), EVP_MD_CTX_free);
+
+  unsigned int hash_length = 0;
+  unsigned char hash[EVP_MAX_MD_SIZE];
+
+  EVP_DigestInit_ex(context.get(), algo, nullptr);
+  EVP_DigestUpdate(context.get(), s.c_str(), s.size());
+  EVP_DigestFinal_ex(context.get(), hash, &hash_length);
+
+  std::stringstream ss;
+  for (auto i = 0u; i < hash_length; ++i) {
+    ss << std::hex << std::setw(2) << std::setfill('0')
+       << static_cast<unsigned int>(hash[i]);
+  }
+
+  return ss.str();
+}
+
+inline std::string MD5(const std::string &s) {
+  return message_digest(s, EVP_md5());
+}
+
+inline std::string SHA_256(const std::string &s) {
+  return message_digest(s, EVP_sha256());
+}
+
+inline std::string SHA_512(const std::string &s) {
+  return message_digest(s, EVP_sha512());
+}
+#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT)
+namespace {
+template <size_t N>
+inline std::string hash_to_hex(const unsigned char (&hash)[N]) {
+  std::stringstream ss;
+  for (size_t i = 0; i < N; ++i) {
+    ss << std::hex << std::setw(2) << std::setfill('0')
+       << static_cast<unsigned int>(hash[i]);
+  }
+  return ss.str();
+}
+} // namespace
+
+inline std::string MD5(const std::string &s) {
+  unsigned char hash[16];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  mbedtls_md5(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+              hash);
+#else
+  mbedtls_md5_ret(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+                  hash);
+#endif
+  return hash_to_hex(hash);
+}
+
+inline std::string SHA_256(const std::string &s) {
+  unsigned char hash[32];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  mbedtls_sha256(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+                 hash, 0);
+#else
+  mbedtls_sha256_ret(reinterpret_cast<const unsigned char *>(s.c_str()),
+                     s.size(), hash, 0);
+#endif
+  return hash_to_hex(hash);
+}
+
+inline std::string SHA_512(const std::string &s) {
+  unsigned char hash[64];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  mbedtls_sha512(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+                 hash, 0);
+#else
+  mbedtls_sha512_ret(reinterpret_cast<const unsigned char *>(s.c_str()),
+                     s.size(), hash, 0);
+#endif
+  return hash_to_hex(hash);
+}
+#elif defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
+namespace {
+template <size_t N>
+inline std::string hash_to_hex(const unsigned char (&hash)[N]) {
+  std::stringstream ss;
+  for (size_t i = 0; i < N; ++i) {
+    ss << std::hex << std::setw(2) << std::setfill('0')
+       << static_cast<unsigned int>(hash[i]);
+  }
+  return ss.str();
+}
+} // namespace
+
+inline std::string MD5(const std::string &s) {
+  unsigned char hash[WC_MD5_DIGEST_SIZE];
+  wc_Md5Hash(reinterpret_cast<const unsigned char *>(s.c_str()),
+             static_cast<word32>(s.size()), hash);
+  return hash_to_hex(hash);
+}
+
+inline std::string SHA_256(const std::string &s) {
+  unsigned char hash[WC_SHA256_DIGEST_SIZE];
+  wc_Sha256Hash(reinterpret_cast<const unsigned char *>(s.c_str()),
+                static_cast<word32>(s.size()), hash);
+  return hash_to_hex(hash);
+}
+
+inline std::string SHA_512(const std::string &s) {
+  unsigned char hash[WC_SHA512_DIGEST_SIZE];
+  wc_Sha512Hash(reinterpret_cast<const unsigned char *>(s.c_str()),
+                static_cast<word32>(s.size()), hash);
+  return hash_to_hex(hash);
+}
+#endif
+
+inline bool is_ip_address(const std::string &host) {
+  struct in_addr addr4;
+  struct in6_addr addr6;
+  return inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||
+         inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
+}
+
+template <typename T>
+inline bool process_server_socket_ssl(
+    const std::atomic<socket_t> &svr_sock, tls::session_t session,
+    socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec,
+    time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec,
+    time_t write_timeout_usec, T callback) {
+  return process_server_socket_core(
+      svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
+      [&](bool close_connection, bool &connection_closed) {
+        SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
+                             write_timeout_sec, write_timeout_usec);
+        return callback(strm, close_connection, connection_closed);
+      });
+}
+
+template <typename T>
+inline bool process_client_socket_ssl(
+    tls::session_t session, socket_t sock, time_t read_timeout_sec,
+    time_t read_timeout_usec, time_t write_timeout_sec,
+    time_t write_timeout_usec, time_t max_timeout_msec,
+    std::chrono::time_point<std::chrono::steady_clock> start_time, T callback) {
+  SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
+                       write_timeout_sec, write_timeout_usec, max_timeout_msec,
+                       start_time);
+  return callback(strm);
+}
+
+inline std::pair<std::string, std::string> make_digest_authentication_header(
+    const Request &req, const std::map<std::string, std::string> &auth,
+    size_t cnonce_count, const std::string &cnonce, const std::string &username,
+    const std::string &password, bool is_proxy = false) {
+  std::string nc;
+  {
+    std::stringstream ss;
+    ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count;
+    nc = ss.str();
+  }
+
+  std::string qop;
+  if (auth.find("qop") != auth.end()) {
+    qop = auth.at("qop");
+    if (qop.find("auth-int") != std::string::npos) {
+      qop = "auth-int";
+    } else if (qop.find("auth") != std::string::npos) {
+      qop = "auth";
+    } else {
+      qop.clear();
+    }
+  }
+
+  std::string algo = "MD5";
+  if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
+
+  std::string response;
+  {
+    auto H = algo == "SHA-256"   ? detail::SHA_256
+             : algo == "SHA-512" ? detail::SHA_512
+                                 : detail::MD5;
+
+    auto A1 = username + ":" + auth.at("realm") + ":" + password;
+
+    auto A2 = req.method + ":" + req.path;
+    if (qop == "auth-int") { A2 += ":" + H(req.body); }
+
+    if (qop.empty()) {
+      response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2));
+    } else {
+      response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce +
+                   ":" + qop + ":" + H(A2));
+    }
+  }
+
+  auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : "";
+
+  auto field = "Digest username=\"" + username + "\", realm=\"" +
+               auth.at("realm") + "\", nonce=\"" + auth.at("nonce") +
+               "\", uri=\"" + req.path + "\", algorithm=" + algo +
+               (qop.empty() ? ", response=\""
+                            : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" +
+                                  cnonce + "\", response=\"") +
+               response + "\"" +
+               (opaque.empty() ? "" : ", opaque=\"" + opaque + "\"");
+
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, field);
+}
+
+inline bool match_hostname(const std::string &pattern,
+                           const std::string &hostname) {
+  // Exact match (case-insensitive)
+  if (detail::case_ignore::equal(hostname, pattern)) { return true; }
+
+  // Split both pattern and hostname into components by '.'
+  std::vector<std::string> pattern_components;
+  if (!pattern.empty()) {
+    split(pattern.data(), pattern.data() + pattern.size(), '.',
+          [&](const char *b, const char *e) {
+            pattern_components.emplace_back(b, e);
+          });
+  }
+
+  std::vector<std::string> host_components;
+  if (!hostname.empty()) {
+    split(hostname.data(), hostname.data() + hostname.size(), '.',
+          [&](const char *b, const char *e) {
+            host_components.emplace_back(b, e);
+          });
+  }
+
+  // Component count must match
+  if (host_components.size() != pattern_components.size()) { return false; }
+
+  // Compare each component with wildcard support
+  // Supports: "*" (full wildcard), "prefix*" (partial wildcard)
+  // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
+  auto itr = pattern_components.begin();
+  for (const auto &h : host_components) {
+    auto &p = *itr;
+    if (!detail::case_ignore::equal(p, h) && p != "*") {
+      bool partial_match = false;
+      if (!p.empty() && p[p.size() - 1] == '*') {
+        const auto prefix_length = p.size() - 1;
+        if (prefix_length == 0) {
+          partial_match = true;
+        } else if (h.size() >= prefix_length) {
+          partial_match =
+              std::equal(p.begin(),
+                         p.begin() + static_cast<std::string::difference_type>(
+                                         prefix_length),
+                         h.begin(), [](const char ca, const char cb) {
+                           return detail::case_ignore::to_lower(ca) ==
+                                  detail::case_ignore::to_lower(cb);
+                         });
+        }
+      }
+      if (!partial_match) { return false; }
+    }
+    ++itr;
+  }
+
+  return true;
+}
+
+#ifdef _WIN32
+// Verify certificate using Windows CertGetCertificateChain API.
+// This provides real-time certificate validation with Windows Update
+// integration, independent of the TLS backend (OpenSSL or MbedTLS).
+inline bool
+verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
+                                  const std::string &hostname,
+                                  bool verify_hostname, uint64_t &out_error) {
+  if (der_cert.empty()) { return false; }
+
+  out_error = 0;
+
+  // Create Windows certificate context from DER data
+  auto cert_context = CertCreateCertificateContext(
+      X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),
+      static_cast<DWORD>(der_cert.size()));
+
+  if (!cert_context) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  auto cert_guard =
+      scope_exit([&] { CertFreeCertificateContext(cert_context); });
+
+  // Setup chain parameters
+  CERT_CHAIN_PARA chain_para = {};
+  chain_para.cbSize = sizeof(chain_para);
+
+  // Build certificate chain with revocation checking
+  PCCERT_CHAIN_CONTEXT chain_context = nullptr;
+  auto chain_result = CertGetCertificateChain(
+      nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,
+      CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
+          CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
+      nullptr, &chain_context);
+
+  if (!chain_result || !chain_context) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  auto chain_guard =
+      scope_exit([&] { CertFreeCertificateChain(chain_context); });
+
+  // Check if chain has errors
+  if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
+    out_error = chain_context->TrustStatus.dwErrorStatus;
+    return false;
+  }
+
+  // Verify SSL policy
+  SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
+  extra_policy_para.cbSize = sizeof(extra_policy_para);
+#ifdef AUTHTYPE_SERVER
+  extra_policy_para.dwAuthType = AUTHTYPE_SERVER;
+#endif
+
+  std::wstring whost;
+  if (verify_hostname) {
+    whost = u8string_to_wstring(hostname.c_str());
+    extra_policy_para.pwszServerName = const_cast<wchar_t *>(whost.c_str());
+  }
+
+  CERT_CHAIN_POLICY_PARA policy_para = {};
+  policy_para.cbSize = sizeof(policy_para);
+#ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS
+  policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
+#else
+  policy_para.dwFlags = 0;
+#endif
+  policy_para.pvExtraPolicyPara = &extra_policy_para;
+
+  CERT_CHAIN_POLICY_STATUS policy_status = {};
+  policy_status.cbSize = sizeof(policy_status);
+
+  if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context,
+                                        &policy_para, &policy_status)) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  if (policy_status.dwError != 0) {
+    out_error = policy_status.dwError;
+    return false;
+  }
+
+  return true;
+}
+#endif // _WIN32
+
+inline bool setup_client_tls_session(const std::string &host, tls::ctx_t &ctx,
+                                     tls::session_t &session, socket_t sock,
+                                     bool server_certificate_verification,
+                                     const std::string &ca_cert_file_path,
+                                     tls::ca_store_t ca_cert_store,
+                                     time_t timeout_sec, time_t timeout_usec) {
+  using namespace tls;
+
+  ctx = create_client_context();
+  if (!ctx) { return false; }
+
+  if (server_certificate_verification) {
+    if (!ca_cert_file_path.empty()) {
+      load_ca_file(ctx, ca_cert_file_path.c_str());
+    }
+    if (ca_cert_store) { set_ca_store(ctx, ca_cert_store); }
+    load_system_certs(ctx);
+  }
+
+  bool is_ip = is_ip_address(host);
+
+#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
+  if (is_ip && server_certificate_verification) {
+    set_verify_client(ctx, false);
+  } else {
+    set_verify_client(ctx, server_certificate_verification);
+  }
+#endif
+
+  session = create_session(ctx, sock);
+  if (!session) { return false; }
+
+  // RFC 6066: SNI must not be set for IP addresses
+  if (!is_ip) { set_sni(session, host.c_str()); }
+  if (server_certificate_verification) { set_hostname(session, host.c_str()); }
+
+  if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec, nullptr)) {
+    return false;
+  }
+
+  if (server_certificate_verification) {
+    if (get_verify_result(session) != 0) { return false; }
+  }
+
+  return true;
+}
+
+} // namespace detail
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 3: httplib namespace - Non-SSL public API implementations
+ */
+
+inline void default_socket_options(socket_t sock) {
+  set_socket_opt(sock, SOL_SOCKET,
+#ifdef SO_REUSEPORT
+                 SO_REUSEPORT,
+#else
+                 SO_REUSEADDR,
+#endif
+                 1);
+}
+
+inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) {
+  return detail::set_socket_opt_impl(sock, level, optname, &optval,
+                                     sizeof(optval));
+}
+
+inline std::string get_bearer_token_auth(const Request &req) {
+  if (req.has_header("Authorization")) {
+    constexpr auto bearer_header_prefix_len = detail::str_len("Bearer ");
+    return req.get_header_value("Authorization")
+        .substr(bearer_header_prefix_len);
+  }
+  return "";
+}
+
+inline const char *status_message(int status) {
+  switch (status) {
+  case StatusCode::Continue_100: return "Continue";
+  case StatusCode::SwitchingProtocol_101: return "Switching Protocol";
+  case StatusCode::Processing_102: return "Processing";
+  case StatusCode::EarlyHints_103: return "Early Hints";
+  case StatusCode::OK_200: return "OK";
+  case StatusCode::Created_201: return "Created";
+  case StatusCode::Accepted_202: return "Accepted";
+  case StatusCode::NonAuthoritativeInformation_203:
+    return "Non-Authoritative Information";
+  case StatusCode::NoContent_204: return "No Content";
+  case StatusCode::ResetContent_205: return "Reset Content";
+  case StatusCode::PartialContent_206: return "Partial Content";
+  case StatusCode::MultiStatus_207: return "Multi-Status";
+  case StatusCode::AlreadyReported_208: return "Already Reported";
+  case StatusCode::IMUsed_226: return "IM Used";
+  case StatusCode::MultipleChoices_300: return "Multiple Choices";
+  case StatusCode::MovedPermanently_301: return "Moved Permanently";
+  case StatusCode::Found_302: return "Found";
+  case StatusCode::SeeOther_303: return "See Other";
+  case StatusCode::NotModified_304: return "Not Modified";
+  case StatusCode::UseProxy_305: return "Use Proxy";
+  case StatusCode::unused_306: return "unused";
+  case StatusCode::TemporaryRedirect_307: return "Temporary Redirect";
+  case StatusCode::PermanentRedirect_308: return "Permanent Redirect";
+  case StatusCode::BadRequest_400: return "Bad Request";
+  case StatusCode::Unauthorized_401: return "Unauthorized";
+  case StatusCode::PaymentRequired_402: return "Payment Required";
+  case StatusCode::Forbidden_403: return "Forbidden";
+  case StatusCode::NotFound_404: return "Not Found";
+  case StatusCode::MethodNotAllowed_405: return "Method Not Allowed";
+  case StatusCode::NotAcceptable_406: return "Not Acceptable";
+  case StatusCode::ProxyAuthenticationRequired_407:
+    return "Proxy Authentication Required";
+  case StatusCode::RequestTimeout_408: return "Request Timeout";
+  case StatusCode::Conflict_409: return "Conflict";
+  case StatusCode::Gone_410: return "Gone";
+  case StatusCode::LengthRequired_411: return "Length Required";
+  case StatusCode::PreconditionFailed_412: return "Precondition Failed";
+  case StatusCode::PayloadTooLarge_413: return "Payload Too Large";
+  case StatusCode::UriTooLong_414: return "URI Too Long";
+  case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type";
+  case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable";
+  case StatusCode::ExpectationFailed_417: return "Expectation Failed";
+  case StatusCode::ImATeapot_418: return "I'm a teapot";
+  case StatusCode::MisdirectedRequest_421: return "Misdirected Request";
+  case StatusCode::UnprocessableContent_422: return "Unprocessable Content";
+  case StatusCode::Locked_423: return "Locked";
+  case StatusCode::FailedDependency_424: return "Failed Dependency";
+  case StatusCode::TooEarly_425: return "Too Early";
+  case StatusCode::UpgradeRequired_426: return "Upgrade Required";
+  case StatusCode::PreconditionRequired_428: return "Precondition Required";
+  case StatusCode::TooManyRequests_429: return "Too Many Requests";
+  case StatusCode::RequestHeaderFieldsTooLarge_431:
+    return "Request Header Fields Too Large";
+  case StatusCode::UnavailableForLegalReasons_451:
+    return "Unavailable For Legal Reasons";
+  case StatusCode::NotImplemented_501: return "Not Implemented";
+  case StatusCode::BadGateway_502: return "Bad Gateway";
+  case StatusCode::ServiceUnavailable_503: return "Service Unavailable";
+  case StatusCode::GatewayTimeout_504: return "Gateway Timeout";
+  case StatusCode::HttpVersionNotSupported_505:
+    return "HTTP Version Not Supported";
+  case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates";
+  case StatusCode::InsufficientStorage_507: return "Insufficient Storage";
+  case StatusCode::LoopDetected_508: return "Loop Detected";
+  case StatusCode::NotExtended_510: return "Not Extended";
+  case StatusCode::NetworkAuthenticationRequired_511:
+    return "Network Authentication Required";
+
+  default:
+  case StatusCode::InternalServerError_500: return "Internal Server Error";
+  }
+}
+
+inline std::string to_string(const Error error) {
+  switch (error) {
+  case Error::Success: return "Success (no error)";
+  case Error::Unknown: return "Unknown";
+  case Error::Connection: return "Could not establish connection";
+  case Error::BindIPAddress: return "Failed to bind IP address";
+  case Error::Read: return "Failed to read connection";
+  case Error::Write: return "Failed to write connection";
+  case Error::ExceedRedirectCount: return "Maximum redirect count exceeded";
+  case Error::Canceled: return "Connection handling canceled";
+  case Error::SSLConnection: return "SSL connection failed";
+  case Error::SSLLoadingCerts: return "SSL certificate loading failed";
+  case Error::SSLServerVerification: return "SSL server verification failed";
+  case Error::SSLServerHostnameVerification:
+    return "SSL server hostname verification failed";
+  case Error::UnsupportedMultipartBoundaryChars:
+    return "Unsupported HTTP multipart boundary characters";
+  case Error::Compression: return "Compression failed";
+  case Error::ConnectionTimeout: return "Connection timed out";
+  case Error::ProxyConnection: return "Proxy connection failed";
+  case Error::ConnectionClosed: return "Connection closed by server";
+  case Error::Timeout: return "Read timeout";
+  case Error::ResourceExhaustion: return "Resource exhaustion";
+  case Error::TooManyFormDataFiles: return "Too many form data files";
+  case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size";
+  case Error::ExceedUriMaxLength: return "Exceeded maximum URI length";
+  case Error::ExceedMaxSocketDescriptorCount:
+    return "Exceeded maximum socket descriptor count";
+  case Error::InvalidRequestLine: return "Invalid request line";
+  case Error::InvalidHTTPMethod: return "Invalid HTTP method";
+  case Error::InvalidHTTPVersion: return "Invalid HTTP version";
+  case Error::InvalidHeaders: return "Invalid headers";
+  case Error::MultipartParsing: return "Multipart parsing failed";
+  case Error::OpenFile: return "Failed to open file";
+  case Error::Listen: return "Failed to listen on socket";
+  case Error::GetSockName: return "Failed to get socket name";
+  case Error::UnsupportedAddressFamily: return "Unsupported address family";
+  case Error::HTTPParsing: return "HTTP parsing failed";
+  case Error::InvalidRangeHeader: return "Invalid Range header";
+  default: break;
+  }
+
+  return "Invalid";
+}
+
+inline std::ostream &operator<<(std::ostream &os, const Error &obj) {
+  os << to_string(obj);
+  os << " (" << static_cast<std::underlying_type<Error>::type>(obj) << ')';
+  return os;
+}
+
+inline std::string hosted_at(const std::string &hostname) {
+  std::vector<std::string> addrs;
+  hosted_at(hostname, addrs);
+  if (addrs.empty()) { return std::string(); }
+  return addrs[0];
+}
+
+inline void hosted_at(const std::string &hostname,
+                      std::vector<std::string> &addrs) {
+  struct addrinfo hints;
+  struct addrinfo *result;
+
+  memset(&hints, 0, sizeof(struct addrinfo));
+  hints.ai_family = AF_UNSPEC;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_protocol = 0;
+
+  if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints,
+                                       &result, 0)) {
+#if defined __linux__ && !defined __ANDROID__
+    res_init();
+#endif
+    return;
+  }
+  auto se = detail::scope_exit([&] { freeaddrinfo(result); });
+
+  for (auto rp = result; rp; rp = rp->ai_next) {
+    const auto &addr =
+        *reinterpret_cast<struct sockaddr_storage *>(rp->ai_addr);
+    std::string ip;
+    auto dummy = -1;
+    if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip,
+                                dummy)) {
+      addrs.emplace_back(std::move(ip));
+    }
+  }
+}
+
+inline std::string encode_uri_component(const std::string &value) {
+  std::ostringstream escaped;
+  escaped.fill('0');
+  escaped << std::hex;
+
+  for (auto c : value) {
+    if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
+        c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
+        c == ')') {
+      escaped << c;
+    } else {
+      escaped << std::uppercase;
+      escaped << '%' << std::setw(2)
+              << static_cast<int>(static_cast<unsigned char>(c));
+      escaped << std::nouppercase;
+    }
+  }
+
+  return escaped.str();
+}
+
+inline std::string encode_uri(const std::string &value) {
+  std::ostringstream escaped;
+  escaped.fill('0');
+  escaped << std::hex;
+
+  for (auto c : value) {
+    if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
+        c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
+        c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||
+        c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
+      escaped << c;
+    } else {
+      escaped << std::uppercase;
+      escaped << '%' << std::setw(2)
+              << static_cast<int>(static_cast<unsigned char>(c));
+      escaped << std::nouppercase;
+    }
+  }
+
+  return escaped.str();
+}
+
+inline std::string decode_uri_component(const std::string &value) {
+  std::string result;
+
+  for (size_t i = 0; i < value.size(); i++) {
+    if (value[i] == '%' && i + 2 < value.size()) {
+      auto val = 0;
+      if (detail::from_hex_to_i(value, i + 1, 2, val)) {
+        result += static_cast<char>(val);
+        i += 2;
+      } else {
+        result += value[i];
+      }
+    } else {
+      result += value[i];
+    }
+  }
+
+  return result;
+}
+
+inline std::string decode_uri(const std::string &value) {
+  std::string result;
+
+  for (size_t i = 0; i < value.size(); i++) {
+    if (value[i] == '%' && i + 2 < value.size()) {
+      auto val = 0;
+      if (detail::from_hex_to_i(value, i + 1, 2, val)) {
+        result += static_cast<char>(val);
+        i += 2;
+      } else {
+        result += value[i];
+      }
+    } else {
+      result += value[i];
+    }
+  }
+
+  return result;
+}
+
+inline std::string encode_path_component(const std::string &component) {
+  std::string result;
+  result.reserve(component.size() * 3);
+
+  for (size_t i = 0; i < component.size(); i++) {
+    auto c = static_cast<unsigned char>(component[i]);
+
+    // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
+    if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
+      result += static_cast<char>(c);
+    }
+    // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
+    // "," / ";" / "="
+    else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
+             c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
+             c == '=') {
+      result += static_cast<char>(c);
+    }
+    // Colon is allowed in path segments except first segment
+    else if (c == ':') {
+      result += static_cast<char>(c);
+    }
+    // @ is allowed in path
+    else if (c == '@') {
+      result += static_cast<char>(c);
+    } else {
+      result += '%';
+      char hex[3];
+      snprintf(hex, sizeof(hex), "%02X", c);
+      result.append(hex, 2);
+    }
+  }
+  return result;
+}
+
+inline std::string decode_path_component(const std::string &component) {
+  std::string result;
+  result.reserve(component.size());
+
+  for (size_t i = 0; i < component.size(); i++) {
+    if (component[i] == '%' && i + 1 < component.size()) {
+      if (component[i + 1] == 'u') {
+        // Unicode %uXXXX encoding
+        auto val = 0;
+        if (detail::from_hex_to_i(component, i + 2, 4, val)) {
+          // 4 digits Unicode codes: val is 0x0000-0xFFFF (from 4 hex digits),
+          // so to_utf8 writes at most 3 bytes. buff[4] is safe.
+          char buff[4];
+          size_t len = detail::to_utf8(val, buff);
+          if (len > 0) { result.append(buff, len); }
+          i += 5; // 'u0000'
+        } else {
+          result += component[i];
+        }
+      } else {
+        // Standard %XX encoding
+        auto val = 0;
+        if (detail::from_hex_to_i(component, i + 1, 2, val)) {
+          // 2 digits hex codes
+          result += static_cast<char>(val);
+          i += 2; // 'XX'
+        } else {
+          result += component[i];
+        }
+      }
+    } else {
+      result += component[i];
+    }
+  }
+  return result;
+}
+
+inline std::string encode_query_component(const std::string &component,
+                                          bool space_as_plus) {
+  std::string result;
+  result.reserve(component.size() * 3);
+
+  for (size_t i = 0; i < component.size(); i++) {
+    auto c = static_cast<unsigned char>(component[i]);
+
+    // Unreserved characters per RFC 3986
+    if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
+      result += static_cast<char>(c);
+    }
+    // Space handling
+    else if (c == ' ') {
+      if (space_as_plus) {
+        result += '+';
+      } else {
+        result += "%20";
+      }
+    }
+    // Plus sign handling
+    else if (c == '+') {
+      if (space_as_plus) {
+        result += "%2B";
+      } else {
+        result += static_cast<char>(c);
+      }
+    }
+    // Query-safe sub-delimiters (excluding & and = which are query delimiters)
+    else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' ||
+             c == '*' || c == ',' || c == ';') {
+      result += static_cast<char>(c);
+    }
+    // Colon and @ are allowed in query
+    else if (c == ':' || c == '@') {
+      result += static_cast<char>(c);
+    }
+    // Forward slash is allowed in query values
+    else if (c == '/') {
+      result += static_cast<char>(c);
+    }
+    // Question mark is allowed in query values (after first ?)
+    else if (c == '?') {
+      result += static_cast<char>(c);
+    } else {
+      result += '%';
+      char hex[3];
+      snprintf(hex, sizeof(hex), "%02X", c);
+      result.append(hex, 2);
+    }
+  }
+  return result;
+}
+
+inline std::string decode_query_component(const std::string &component,
+                                          bool plus_as_space) {
+  std::string result;
+  result.reserve(component.size());
+
+  for (size_t i = 0; i < component.size(); i++) {
+    if (component[i] == '%' && i + 2 < component.size()) {
+      std::string hex = component.substr(i + 1, 2);
+      char *end;
+      unsigned long value = std::strtoul(hex.c_str(), &end, 16);
+      if (end == hex.c_str() + 2) {
+        result += static_cast<char>(value);
+        i += 2;
+      } else {
+        result += component[i];
+      }
+    } else if (component[i] == '+' && plus_as_space) {
+      result += ' '; // + becomes space in form-urlencoded
+    } else {
+      result += component[i];
+    }
+  }
+  return result;
+}
+
+inline std::string sanitize_filename(const std::string &filename) {
+  // Extract basename: find the last path separator (/ or \)
+  auto pos = filename.find_last_of("/\\");
+  auto result =
+      (pos != std::string::npos) ? filename.substr(pos + 1) : filename;
+
+  // Strip null bytes
+  result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
+
+  // Trim whitespace
+  {
+    auto start = result.find_first_not_of(" \t");
+    auto end = result.find_last_not_of(" \t");
+    result = (start == std::string::npos)
+                 ? ""
+                 : result.substr(start, end - start + 1);
+  }
+
+  // Reject . and ..
+  if (result == "." || result == "..") { return ""; }
+
+  return result;
+}
+
+inline std::string append_query_params(const std::string &path,
+                                       const Params &params) {
+  std::string path_with_query = path;
+  thread_local const std::regex re("[^?]+\\?.*");
+  auto delm = std::regex_match(path, re) ? '&' : '?';
+  path_with_query += delm + detail::params_to_query_str(params);
+  return path_with_query;
+}
+
+// Header utilities
+inline std::pair<std::string, std::string>
+make_range_header(const Ranges &ranges) {
+  std::string field = "bytes=";
+  auto i = 0;
+  for (const auto &r : ranges) {
+    if (i != 0) { field += ", "; }
+    if (r.first != -1) { field += std::to_string(r.first); }
+    field += '-';
+    if (r.second != -1) { field += std::to_string(r.second); }
+    i++;
+  }
+  return std::make_pair("Range", std::move(field));
+}
+
+inline std::pair<std::string, std::string>
+make_basic_authentication_header(const std::string &username,
+                                 const std::string &password, bool is_proxy) {
+  auto field = "Basic " + detail::base64_encode(username + ":" + password);
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, std::move(field));
+}
+
+inline std::pair<std::string, std::string>
+make_bearer_token_authentication_header(const std::string &token,
+                                        bool is_proxy = false) {
+  auto field = "Bearer " + token;
+  auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
+  return std::make_pair(key, std::move(field));
+}
+
+// Request implementation
+inline size_t Request::get_header_value_u64(const std::string &key, size_t def,
+                                            size_t id) const {
+  return detail::get_header_value_u64(headers, key, def, id);
+}
+
+inline bool Request::has_header(const std::string &key) const {
+  return detail::has_header(headers, key);
+}
+
+inline std::string Request::get_header_value(const std::string &key,
+                                             const char *def, size_t id) const {
+  return detail::get_header_value(headers, key, def, id);
+}
+
+inline size_t Request::get_header_value_count(const std::string &key) const {
+  return detail::get_header_value_count(headers, key);
+}
+
+inline void Request::set_header(const std::string &key,
+                                const std::string &val) {
+  detail::set_header(headers, key, val);
+}
+
+inline bool Request::has_trailer(const std::string &key) const {
+  return trailers.find(key) != trailers.end();
+}
+
+inline std::string Request::get_trailer_value(const std::string &key,
+                                              size_t id) const {
+  return detail::get_multimap_value(trailers, key, id);
+}
+
+inline size_t Request::get_trailer_value_count(const std::string &key) const {
+  auto r = trailers.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+inline bool Request::has_param(const std::string &key) const {
+  return params.find(key) != params.end();
+}
+
+inline std::string Request::get_param_value(const std::string &key,
+                                            size_t id) const {
+  return detail::get_multimap_value(params, key, id);
+}
+
+inline std::vector<std::string>
+Request::get_param_values(const std::string &key) const {
+  auto rng = params.equal_range(key);
+  std::vector<std::string> values;
+  values.reserve(static_cast<size_t>(std::distance(rng.first, rng.second)));
+  for (auto it = rng.first; it != rng.second; ++it) {
+    values.push_back(it->second);
+  }
+  return values;
+}
+
+inline size_t Request::get_param_value_count(const std::string &key) const {
+  auto r = params.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+inline bool Request::is_multipart_form_data() const {
+  const auto &content_type = get_header_value("Content-Type");
+  return detail::extract_media_type(content_type) == "multipart/form-data";
+}
+
+// Multipart FormData implementation
+inline std::string MultipartFormData::get_field(const std::string &key,
+                                                size_t id) const {
+  auto rng = fields.equal_range(key);
+  auto it = rng.first;
+  std::advance(it, static_cast<ssize_t>(id));
+  if (it != rng.second) { return it->second.content; }
+  return std::string();
+}
+
+inline std::vector<std::string>
+MultipartFormData::get_fields(const std::string &key) const {
+  std::vector<std::string> values;
+  auto rng = fields.equal_range(key);
+  for (auto it = rng.first; it != rng.second; it++) {
+    values.push_back(it->second.content);
+  }
+  return values;
+}
+
+inline bool MultipartFormData::has_field(const std::string &key) const {
+  return fields.find(key) != fields.end();
+}
+
+inline size_t MultipartFormData::get_field_count(const std::string &key) const {
+  auto r = fields.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+inline FormData MultipartFormData::get_file(const std::string &key,
+                                            size_t id) const {
+  return detail::get_multimap_value(files, key, id);
+}
+
+inline std::vector<FormData>
+MultipartFormData::get_files(const std::string &key) const {
+  std::vector<FormData> values;
+  auto rng = files.equal_range(key);
+  for (auto it = rng.first; it != rng.second; it++) {
+    values.push_back(it->second);
+  }
+  return values;
+}
+
+inline bool MultipartFormData::has_file(const std::string &key) const {
+  return files.find(key) != files.end();
+}
+
+inline size_t MultipartFormData::get_file_count(const std::string &key) const {
+  auto r = files.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+// Response implementation
+inline size_t Response::get_header_value_u64(const std::string &key, size_t def,
+                                             size_t id) const {
+  return detail::get_header_value_u64(headers, key, def, id);
+}
+
+inline bool Response::has_header(const std::string &key) const {
+  return headers.find(key) != headers.end();
+}
+
+inline std::string Response::get_header_value(const std::string &key,
+                                              const char *def,
+                                              size_t id) const {
+  return detail::get_header_value(headers, key, def, id);
+}
+
+inline size_t Response::get_header_value_count(const std::string &key) const {
+  return detail::get_header_value_count(headers, key);
+}
+
+inline void Response::set_header(const std::string &key,
+                                 const std::string &val) {
+  detail::set_header(headers, key, val);
+}
+inline bool Response::has_trailer(const std::string &key) const {
+  return trailers.find(key) != trailers.end();
+}
+
+inline std::string Response::get_trailer_value(const std::string &key,
+                                               size_t id) const {
+  return detail::get_multimap_value(trailers, key, id);
+}
+
+inline size_t Response::get_trailer_value_count(const std::string &key) const {
+  auto r = trailers.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+inline void Response::set_redirect(const std::string &url, int stat) {
+  if (detail::fields::is_field_value(url)) {
+    set_header("Location", url);
+    if (300 <= stat && stat < 400) {
+      this->status = stat;
+    } else {
+      this->status = StatusCode::Found_302;
+    }
+  }
+}
+
+inline void Response::set_content(const char *s, size_t n,
+                                  const std::string &content_type) {
+  body.assign(s, n);
+
+  auto rng = headers.equal_range("Content-Type");
+  headers.erase(rng.first, rng.second);
+  set_header("Content-Type", content_type);
+}
+
+inline void Response::set_content(const std::string &s,
+                                  const std::string &content_type) {
+  set_content(s.data(), s.size(), content_type);
+}
+
+inline void Response::set_content(std::string &&s,
+                                  const std::string &content_type) {
+  body = std::move(s);
+
+  auto rng = headers.equal_range("Content-Type");
+  headers.erase(rng.first, rng.second);
+  set_header("Content-Type", content_type);
+}
+
+inline void Response::set_content_provider(
+    size_t in_length, const std::string &content_type, ContentProvider provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  set_header("Content-Type", content_type);
+  content_length_ = in_length;
+  if (in_length > 0) { content_provider_ = std::move(provider); }
+  content_provider_resource_releaser_ = std::move(resource_releaser);
+  is_chunked_content_provider_ = false;
+}
+
+inline void Response::set_content_provider(
+    const std::string &content_type, ContentProviderWithoutLength provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  set_header("Content-Type", content_type);
+  content_length_ = 0;
+  content_provider_ = detail::ContentProviderAdapter(std::move(provider));
+  content_provider_resource_releaser_ = std::move(resource_releaser);
+  is_chunked_content_provider_ = false;
+}
+
+inline void Response::set_chunked_content_provider(
+    const std::string &content_type, ContentProviderWithoutLength provider,
+    ContentProviderResourceReleaser resource_releaser) {
+  set_header("Content-Type", content_type);
+  content_length_ = 0;
+  content_provider_ = detail::ContentProviderAdapter(std::move(provider));
+  content_provider_resource_releaser_ = std::move(resource_releaser);
+  is_chunked_content_provider_ = true;
+}
+
+inline void Response::set_file_content(const std::string &path,
+                                       const std::string &content_type) {
+  file_content_path_ = path;
+  file_content_content_type_ = content_type;
+}
+
+inline void Response::set_file_content(const std::string &path) {
+  file_content_path_ = path;
+}
+
+// Result implementation
+inline size_t Result::get_request_header_value_u64(const std::string &key,
+                                                   size_t def,
+                                                   size_t id) const {
+  return detail::get_header_value_u64(request_headers_, key, def, id);
+}
+
+inline bool Result::has_request_header(const std::string &key) const {
+  return request_headers_.find(key) != request_headers_.end();
+}
+
+inline std::string Result::get_request_header_value(const std::string &key,
+                                                    const char *def,
+                                                    size_t id) const {
+  return detail::get_header_value(request_headers_, key, def, id);
+}
+
+inline size_t
+Result::get_request_header_value_count(const std::string &key) const {
+  auto r = request_headers_.equal_range(key);
+  return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
+// Stream implementation
+inline ssize_t Stream::write(const char *ptr) {
+  return write(ptr, strlen(ptr));
+}
+
+inline ssize_t Stream::write(const std::string &s) {
+  return write(s.data(), s.size());
+}
+
+// BodyReader implementation
+inline ssize_t detail::BodyReader::read(char *buf, size_t len) {
+  if (!stream) {
+    last_error = Error::Connection;
+    return -1;
+  }
+  if (eof) { return 0; }
+
+  if (!chunked) {
+    // Content-Length based reading
+    if (has_content_length && bytes_read >= content_length) {
+      eof = true;
+      return 0;
+    }
+
+    auto to_read = len;
+    if (has_content_length) {
+      auto remaining = content_length - bytes_read;
+      to_read = (std::min)(len, remaining);
+    }
+    auto n = stream->read(buf, to_read);
+
+    if (n < 0) {
+      last_error = stream->get_error();
+      if (last_error == Error::Success) { last_error = Error::Read; }
+      eof = true;
+      return n;
+    }
+    if (n == 0) {
+      // Unexpected EOF before content_length
+      last_error = stream->get_error();
+      if (last_error == Error::Success) { last_error = Error::Read; }
+      eof = true;
+      return 0;
+    }
+
+    bytes_read += static_cast<size_t>(n);
+    if (has_content_length && bytes_read >= content_length) { eof = true; }
+    if (payload_max_length > 0 && bytes_read > payload_max_length) {
+      last_error = Error::ExceedMaxPayloadSize;
+      eof = true;
+      return -1;
+    }
+    return n;
+  }
+
+  // Chunked transfer encoding: delegate to shared decoder instance.
+  if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); }
+
+  size_t chunk_offset = 0;
+  size_t chunk_total = 0;
+  auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total);
+  if (n < 0) {
+    last_error = stream->get_error();
+    if (last_error == Error::Success) { last_error = Error::Read; }
+    eof = true;
+    return n;
+  }
+
+  if (n == 0) {
+    // Final chunk observed. Leave trailer parsing to the caller (StreamHandle).
+    eof = true;
+    return 0;
+  }
+
+  bytes_read += static_cast<size_t>(n);
+  if (payload_max_length > 0 && bytes_read > payload_max_length) {
+    last_error = Error::ExceedMaxPayloadSize;
+    eof = true;
+    return -1;
+  }
+  return n;
+}
+
+// ThreadPool implementation
+inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
+    : base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0),
+      shutdown_(false) {
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+  if (max_n != 0 && max_n < n) {
+    std::string msg = "max_threads must be >= base_threads";
+    throw std::invalid_argument(msg);
+  }
+#endif
+  max_thread_count_ = max_n == 0 ? n : max_n;
+  threads_.reserve(base_thread_count_);
+  for (size_t i = 0; i < base_thread_count_; i++) {
+    threads_.emplace_back(std::thread([this]() { worker(false); }));
+  }
+}
+
+inline bool ThreadPool::enqueue(std::function<void()> fn) {
+  {
+    std::unique_lock<std::mutex> lock(mutex_);
+    if (shutdown_) { return false; }
+    if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) {
+      return false;
+    }
+    jobs_.push_back(std::move(fn));
+
+    // Spawn a dynamic thread if no idle threads and under max
+    if (idle_thread_count_ == 0 &&
+        threads_.size() + dynamic_threads_.size() < max_thread_count_) {
+      cleanup_finished_threads();
+      dynamic_threads_.emplace_back(std::thread([this]() { worker(true); }));
+    }
+  }
+
+  cond_.notify_one();
+  return true;
+}
+
+inline void ThreadPool::shutdown() {
+  {
+    std::unique_lock<std::mutex> lock(mutex_);
+    shutdown_ = true;
+  }
+
+  cond_.notify_all();
+
+  for (auto &t : threads_) {
+    if (t.joinable()) { t.join(); }
+  }
+
+  // Move dynamic_threads_ to a local list under the lock to avoid racing
+  // with worker threads that call move_to_finished() concurrently.
+  std::list<std::thread> remaining_dynamic;
+  {
+    std::unique_lock<std::mutex> lock(mutex_);
+    remaining_dynamic = std::move(dynamic_threads_);
+  }
+  for (auto &t : remaining_dynamic) {
+    if (t.joinable()) { t.join(); }
+  }
+
+  std::unique_lock<std::mutex> lock(mutex_);
+  cleanup_finished_threads();
+}
+
+inline void ThreadPool::move_to_finished(std::thread::id id) {
+  // Must be called with mutex_ held
+  for (auto it = dynamic_threads_.begin(); it != dynamic_threads_.end(); ++it) {
+    if (it->get_id() == id) {
+      finished_threads_.push_back(std::move(*it));
+      dynamic_threads_.erase(it);
+      return;
+    }
+  }
+}
+
+inline void ThreadPool::cleanup_finished_threads() {
+  // Must be called with mutex_ held
+  for (auto &t : finished_threads_) {
+    if (t.joinable()) { t.join(); }
+  }
+  finished_threads_.clear();
+}
+
+inline void ThreadPool::worker(bool is_dynamic) {
+  for (;;) {
+    std::function<void()> fn;
+    {
+      std::unique_lock<std::mutex> lock(mutex_);
+      idle_thread_count_++;
+
+      if (is_dynamic) {
+        auto has_work = cond_.wait_for(
+            lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT),
+            [&] { return !jobs_.empty() || shutdown_; });
+        if (!has_work) {
+          // Timed out with no work - exit this dynamic thread
+          idle_thread_count_--;
+          move_to_finished(std::this_thread::get_id());
+          break;
+        }
+      } else {
+        cond_.wait(lock, [&] { return !jobs_.empty() || shutdown_; });
+      }
+
+      idle_thread_count_--;
+
+      if (shutdown_ && jobs_.empty()) { break; }
+
+      fn = std::move(jobs_.front());
+      jobs_.pop_front();
+    }
+
+    assert(true == static_cast<bool>(fn));
+    fn();
+  }
+
+#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) &&   \
+    !defined(LIBRESSL_VERSION_NUMBER)
+  OPENSSL_thread_stop();
+#endif
+}
+
+/*
+ * Group 1 (continued): detail namespace - Stream implementations
+ */
+
+namespace detail {
+
+inline void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec,
+                                time_t timeout_sec, time_t timeout_usec,
+                                time_t &actual_timeout_sec,
+                                time_t &actual_timeout_usec) {
+  auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000);
+
+  auto actual_timeout_msec =
+      (std::min)(max_timeout_msec - duration_msec, timeout_msec);
+
+  if (actual_timeout_msec < 0) { actual_timeout_msec = 0; }
+
+  actual_timeout_sec = actual_timeout_msec / 1000;
+  actual_timeout_usec = (actual_timeout_msec % 1000) * 1000;
+}
+
+// Socket stream implementation
+inline SocketStream::SocketStream(
+    socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+    time_t write_timeout_sec, time_t write_timeout_usec,
+    time_t max_timeout_msec,
+    std::chrono::time_point<std::chrono::steady_clock> start_time)
+    : sock_(sock), read_timeout_sec_(read_timeout_sec),
+      read_timeout_usec_(read_timeout_usec),
+      write_timeout_sec_(write_timeout_sec),
+      write_timeout_usec_(write_timeout_usec),
+      max_timeout_msec_(max_timeout_msec), start_time_(start_time),
+      read_buff_(read_buff_size_, 0) {}
+
+inline SocketStream::~SocketStream() = default;
+
+inline bool SocketStream::is_readable() const {
+  return read_buff_off_ < read_buff_content_size_;
+}
+
+inline bool SocketStream::wait_readable() const {
+  if (max_timeout_msec_ <= 0) {
+    return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+  }
+
+  time_t read_timeout_sec;
+  time_t read_timeout_usec;
+  calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_,
+                      read_timeout_usec_, read_timeout_sec, read_timeout_usec);
+
+  return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;
+}
+
+inline bool SocketStream::wait_writable() const {
+  return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
+}
+
+inline bool SocketStream::is_peer_alive() const {
+  return detail::is_socket_alive(sock_);
+}
+
+inline ssize_t SocketStream::read(char *ptr, size_t size) {
+#ifdef _WIN32
+  size =
+      (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
+#else
+  size = (std::min)(size,
+                    static_cast<size_t>((std::numeric_limits<ssize_t>::max)()));
+#endif
+
+  if (read_buff_off_ < read_buff_content_size_) {
+    auto remaining_size = read_buff_content_size_ - read_buff_off_;
+    if (size <= remaining_size) {
+      memcpy(ptr, read_buff_.data() + read_buff_off_, size);
+      read_buff_off_ += size;
+      return static_cast<ssize_t>(size);
+    } else {
+      memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size);
+      read_buff_off_ += remaining_size;
+      return static_cast<ssize_t>(remaining_size);
+    }
+  }
+
+  if (!wait_readable()) {
+    error_ = Error::Timeout;
+    return -1;
+  }
+
+  read_buff_off_ = 0;
+  read_buff_content_size_ = 0;
+
+  if (size < read_buff_size_) {
+    auto n = read_socket(sock_, read_buff_.data(), read_buff_size_,
+                         CPPHTTPLIB_RECV_FLAGS);
+    if (n <= 0) {
+      if (n == 0) {
+        error_ = Error::ConnectionClosed;
+      } else {
+        error_ = Error::Read;
+      }
+      return n;
+    } else if (n <= static_cast<ssize_t>(size)) {
+      memcpy(ptr, read_buff_.data(), static_cast<size_t>(n));
+      return n;
+    } else {
+      memcpy(ptr, read_buff_.data(), size);
+      read_buff_off_ = size;
+      read_buff_content_size_ = static_cast<size_t>(n);
+      return static_cast<ssize_t>(size);
+    }
+  } else {
+    auto n = read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS);
+    if (n <= 0) {
+      if (n == 0) {
+        error_ = Error::ConnectionClosed;
+      } else {
+        error_ = Error::Read;
+      }
+    }
+    return n;
+  }
+}
+
+inline ssize_t SocketStream::write(const char *ptr, size_t size) {
+  if (!wait_writable()) { return -1; }
+
+#if defined(_WIN32) && !defined(_WIN64)
+  size =
+      (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
+#endif
+
+  return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS);
+}
+
+inline void SocketStream::get_remote_ip_and_port(std::string &ip,
+                                                 int &port) const {
+  return detail::get_remote_ip_and_port(sock_, ip, port);
+}
+
+inline void SocketStream::get_local_ip_and_port(std::string &ip,
+                                                int &port) const {
+  return detail::get_local_ip_and_port(sock_, ip, port);
+}
+
+inline socket_t SocketStream::socket() const { return sock_; }
+
+inline time_t SocketStream::duration() const {
+  return std::chrono::duration_cast<std::chrono::milliseconds>(
+             std::chrono::steady_clock::now() - start_time_)
+      .count();
+}
+
+inline void SocketStream::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+}
+
+// Buffer stream implementation
+inline bool BufferStream::is_readable() const { return true; }
+
+inline bool BufferStream::wait_readable() const { return true; }
+
+inline bool BufferStream::wait_writable() const { return true; }
+
+inline ssize_t BufferStream::read(char *ptr, size_t size) {
+#if defined(_MSC_VER) && _MSC_VER < 1910
+  auto len_read = buffer._Copy_s(ptr, size, size, position);
+#else
+  auto len_read = buffer.copy(ptr, size, position);
+#endif
+  position += static_cast<size_t>(len_read);
+  return static_cast<ssize_t>(len_read);
+}
+
+inline ssize_t BufferStream::write(const char *ptr, size_t size) {
+  buffer.append(ptr, size);
+  return static_cast<ssize_t>(size);
+}
+
+inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/,
+                                                 int & /*port*/) const {}
+
+inline void BufferStream::get_local_ip_and_port(std::string & /*ip*/,
+                                                int & /*port*/) const {}
+
+inline socket_t BufferStream::socket() const { return 0; }
+
+inline time_t BufferStream::duration() const { return 0; }
+
+inline const std::string &BufferStream::get_buffer() const { return buffer; }
+
+inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern)
+    : MatcherBase(pattern) {
+  constexpr const char marker[] = "/:";
+
+  // One past the last ending position of a path param substring
+  std::size_t last_param_end = 0;
+
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+  // Needed to ensure that parameter names are unique during matcher
+  // construction
+  // If exceptions are disabled, only last duplicate path
+  // parameter will be set
+  std::unordered_set<std::string> param_name_set;
+#endif
+
+  while (true) {
+    const auto marker_pos = pattern.find(
+        marker, last_param_end == 0 ? last_param_end : last_param_end - 1);
+    if (marker_pos == std::string::npos) { break; }
+
+    static_fragments_.push_back(
+        pattern.substr(last_param_end, marker_pos - last_param_end + 1));
+
+    const auto param_name_start = marker_pos + str_len(marker);
+
+    auto sep_pos = pattern.find(separator, param_name_start);
+    if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }
+
+    auto param_name =
+        pattern.substr(param_name_start, sep_pos - param_name_start);
+
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+    if (param_name_set.find(param_name) != param_name_set.cend()) {
+      std::string msg = "Encountered path parameter '" + param_name +
+                        "' multiple times in route pattern '" + pattern + "'.";
+      throw std::invalid_argument(msg);
+    }
+#endif
+
+    param_names_.push_back(std::move(param_name));
+
+    last_param_end = sep_pos + 1;
+  }
+
+  if (last_param_end < pattern.length()) {
+    static_fragments_.push_back(pattern.substr(last_param_end));
+  }
+}
+
+inline bool PathParamsMatcher::match(Request &request) const {
+  request.matches = std::smatch();
+  request.path_params.clear();
+  request.path_params.reserve(param_names_.size());
+
+  // One past the position at which the path matched the pattern last time
+  std::size_t starting_pos = 0;
+  for (size_t i = 0; i < static_fragments_.size(); ++i) {
+    const auto &fragment = static_fragments_[i];
+
+    if (starting_pos + fragment.length() > request.path.length()) {
+      return false;
+    }
+
+    // Avoid unnecessary allocation by using strncmp instead of substr +
+    // comparison
+    if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(),
+                     fragment.length()) != 0) {
+      return false;
+    }
+
+    starting_pos += fragment.length();
+
+    // Should only happen when we have a static fragment after a param
+    // Example: '/users/:id/subscriptions'
+    // The 'subscriptions' fragment here does not have a corresponding param
+    if (i >= param_names_.size()) { continue; }
+
+    auto sep_pos = request.path.find(separator, starting_pos);
+    if (sep_pos == std::string::npos) { sep_pos = request.path.length(); }
+
+    const auto &param_name = param_names_[i];
+
+    request.path_params.emplace(
+        param_name, request.path.substr(starting_pos, sep_pos - starting_pos));
+
+    // Mark everything up to '/' as matched
+    starting_pos = sep_pos + 1;
+  }
+  // Returns false if the path is longer than the pattern
+  return starting_pos >= request.path.length();
+}
+
+inline bool RegexMatcher::match(Request &request) const {
+  request.path_params.clear();
+  return std::regex_match(request.path, request.matches, regex_);
+}
+
+// Enclose IPv6 address in brackets if needed
+inline std::string prepare_host_string(const std::string &host) {
+  // Enclose IPv6 address in brackets (but not if already enclosed)
+  if (host.find(':') == std::string::npos ||
+      (!host.empty() && host[0] == '[')) {
+    // IPv4, hostname, or already bracketed IPv6
+    return host;
+  } else {
+    // IPv6 address without brackets
+    return "[" + host + "]";
+  }
+}
+
+inline std::string make_host_and_port_string(const std::string &host, int port,
+                                             bool is_ssl) {
+  auto result = prepare_host_string(host);
+
+  // Append port if not default
+  if ((!is_ssl && port == 80) || (is_ssl && port == 443)) {
+    ; // do nothing
+  } else {
+    result += ":" + std::to_string(port);
+  }
+
+  return result;
+}
+
+// Create "host:port" string always including port number (for CONNECT method)
+inline std::string
+make_host_and_port_string_always_port(const std::string &host, int port) {
+  return prepare_host_string(host) + ":" + std::to_string(port);
+}
+
+template <typename T>
+inline bool check_and_write_headers(Stream &strm, Headers &headers,
+                                    T header_writer, Error &error) {
+  for (const auto &h : headers) {
+    if (!detail::fields::is_field_name(h.first) ||
+        !detail::fields::is_field_value(h.second)) {
+      error = Error::InvalidHeaders;
+      return false;
+    }
+  }
+  if (header_writer(strm, headers) <= 0) {
+    error = Error::Write;
+    return false;
+  }
+  return true;
+}
+
+} // namespace detail
+
+/*
+ * Group 2 (continued): detail namespace - SSLSocketStream implementation
+ */
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+namespace detail {
+
+// SSL socket stream implementation
+inline SSLSocketStream::SSLSocketStream(
+    socket_t sock, tls::session_t session, time_t read_timeout_sec,
+    time_t read_timeout_usec, time_t write_timeout_sec,
+    time_t write_timeout_usec, time_t max_timeout_msec,
+    std::chrono::time_point<std::chrono::steady_clock> start_time)
+    : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec),
+      read_timeout_usec_(read_timeout_usec),
+      write_timeout_sec_(write_timeout_sec),
+      write_timeout_usec_(write_timeout_usec),
+      max_timeout_msec_(max_timeout_msec), start_time_(start_time) {
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  // Clear AUTO_RETRY for proper non-blocking I/O timeout handling
+  // Note: create_session() also clears this, but SSLClient currently
+  // uses ssl_new() which does not. Until full TLS API migration is complete,
+  // we need to ensure AUTO_RETRY is cleared here regardless of how the
+  // SSL session was created.
+  SSL_clear_mode(static_cast<SSL *>(session), SSL_MODE_AUTO_RETRY);
+#endif
+}
+
+inline SSLSocketStream::~SSLSocketStream() = default;
+
+inline bool SSLSocketStream::is_readable() const {
+  return tls::pending(session_) > 0;
+}
+
+inline bool SSLSocketStream::wait_readable() const {
+  if (max_timeout_msec_ <= 0) {
+    return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+  }
+
+  time_t read_timeout_sec;
+  time_t read_timeout_usec;
+  calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_,
+                      read_timeout_usec_, read_timeout_sec, read_timeout_usec);
+
+  return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;
+}
+
+inline bool SSLSocketStream::wait_writable() const {
+  return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
+         !tls::is_peer_closed(session_, sock_);
+}
+
+inline bool SSLSocketStream::is_peer_alive() const {
+  return !tls::is_peer_closed(session_, sock_);
+}
+
+inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
+  if (tls::pending(session_) > 0) {
+    tls::TlsError err;
+    auto ret = tls::read(session_, ptr, size, err);
+    if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) {
+      error_ = Error::ConnectionClosed;
+    }
+    return ret;
+  } else if (wait_readable()) {
+    tls::TlsError err;
+    auto ret = tls::read(session_, ptr, size, err);
+    if (ret < 0) {
+      auto n = 1000;
+#ifdef _WIN32
+      while (--n >= 0 && (err.code == tls::ErrorCode::WantRead ||
+                          (err.code == tls::ErrorCode::SyscallError &&
+                           WSAGetLastError() == WSAETIMEDOUT))) {
+#else
+      while (--n >= 0 && err.code == tls::ErrorCode::WantRead) {
+#endif
+        if (tls::pending(session_) > 0) {
+          return tls::read(session_, ptr, size, err);
+        } else if (wait_readable()) {
+          std::this_thread::sleep_for(std::chrono::microseconds{10});
+          ret = tls::read(session_, ptr, size, err);
+          if (ret >= 0) { return ret; }
+        } else {
+          break;
+        }
+      }
+      assert(ret < 0);
+    } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) {
+      error_ = Error::ConnectionClosed;
+    }
+    return ret;
+  } else {
+    error_ = Error::Timeout;
+    return -1;
+  }
+}
+
+inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
+  if (wait_writable()) {
+    auto handle_size =
+        std::min<size_t>(size, (std::numeric_limits<int>::max)());
+
+    tls::TlsError err;
+    auto ret = tls::write(session_, ptr, handle_size, err);
+    if (ret < 0) {
+      auto n = 1000;
+#ifdef _WIN32
+      while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite ||
+                          (err.code == tls::ErrorCode::SyscallError &&
+                           WSAGetLastError() == WSAETIMEDOUT))) {
+#else
+      while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) {
+#endif
+        if (wait_writable()) {
+          std::this_thread::sleep_for(std::chrono::microseconds{10});
+          ret = tls::write(session_, ptr, handle_size, err);
+          if (ret >= 0) { return ret; }
+        } else {
+          break;
+        }
+      }
+      assert(ret < 0);
+    }
+    return ret;
+  }
+  return -1;
+}
+
+inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip,
+                                                    int &port) const {
+  detail::get_remote_ip_and_port(sock_, ip, port);
+}
+
+inline void SSLSocketStream::get_local_ip_and_port(std::string &ip,
+                                                   int &port) const {
+  detail::get_local_ip_and_port(sock_, ip, port);
+}
+
+inline socket_t SSLSocketStream::socket() const { return sock_; }
+
+inline time_t SSLSocketStream::duration() const {
+  return std::chrono::duration_cast<std::chrono::milliseconds>(
+             std::chrono::steady_clock::now() - start_time_)
+      .count();
+}
+
+inline void SSLSocketStream::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+}
+
+} // namespace detail
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 4: Server implementation
+ */
+
+// HTTP server implementation
+inline Server::Server()
+    : new_task_queue([] {
+        return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT,
+                              CPPHTTPLIB_THREAD_POOL_MAX_COUNT);
+      }) {
+#ifndef _WIN32
+  signal(SIGPIPE, SIG_IGN);
+#endif
+}
+
+inline Server::~Server() = default;
+
+inline std::unique_ptr<detail::MatcherBase>
+Server::make_matcher(const std::string &pattern) {
+  if (pattern.find("/:") != std::string::npos) {
+    return detail::make_unique<detail::PathParamsMatcher>(pattern);
+  } else {
+    return detail::make_unique<detail::RegexMatcher>(pattern);
+  }
+}
+
+inline Server &Server::Get(const std::string &pattern, Handler handler) {
+  return add_handler(get_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::Post(const std::string &pattern, Handler handler) {
+  return add_handler(post_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::Post(const std::string &pattern,
+                            HandlerWithContentReader handler) {
+  return add_handler(post_handlers_for_content_reader_, pattern,
+                     std::move(handler));
+}
+
+inline Server &Server::Put(const std::string &pattern, Handler handler) {
+  return add_handler(put_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::Put(const std::string &pattern,
+                           HandlerWithContentReader handler) {
+  return add_handler(put_handlers_for_content_reader_, pattern,
+                     std::move(handler));
+}
+
+inline Server &Server::Patch(const std::string &pattern, Handler handler) {
+  return add_handler(patch_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::Patch(const std::string &pattern,
+                             HandlerWithContentReader handler) {
+  return add_handler(patch_handlers_for_content_reader_, pattern,
+                     std::move(handler));
+}
+
+inline Server &Server::Delete(const std::string &pattern, Handler handler) {
+  return add_handler(delete_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::Delete(const std::string &pattern,
+                              HandlerWithContentReader handler) {
+  return add_handler(delete_handlers_for_content_reader_, pattern,
+                     std::move(handler));
+}
+
+inline Server &Server::Options(const std::string &pattern, Handler handler) {
+  return add_handler(options_handlers_, pattern, std::move(handler));
+}
+
+inline Server &Server::WebSocket(const std::string &pattern,
+                                 WebSocketHandler handler) {
+  websocket_handlers_.push_back(
+      {make_matcher(pattern), std::move(handler), nullptr});
+  return *this;
+}
+
+inline Server &Server::WebSocket(const std::string &pattern,
+                                 WebSocketHandler handler,
+                                 SubProtocolSelector sub_protocol_selector) {
+  websocket_handlers_.push_back({make_matcher(pattern), std::move(handler),
+                                 std::move(sub_protocol_selector)});
+  return *this;
+}
+
+inline bool Server::set_base_dir(const std::string &dir,
+                                 const std::string &mount_point) {
+  return set_mount_point(mount_point, dir);
+}
+
+inline bool Server::set_mount_point(const std::string &mount_point,
+                                    const std::string &dir, Headers headers) {
+  detail::FileStat stat(dir);
+  if (stat.is_dir()) {
+    std::string mnt = !mount_point.empty() ? mount_point : "/";
+    if (!mnt.empty() && mnt[0] == '/') {
+      std::string resolved_base;
+      if (detail::canonicalize_path(dir.c_str(), resolved_base)) {
+#if defined(_WIN32)
+        if (resolved_base.back() != '\\' && resolved_base.back() != '/') {
+          resolved_base += '\\';
+        }
+#else
+        if (resolved_base.back() != '/') { resolved_base += '/'; }
+#endif
+      }
+      base_dirs_.push_back(
+          {std::move(mnt), dir, std::move(resolved_base), std::move(headers)});
+      return true;
+    }
+  }
+  return false;
+}
+
+inline bool Server::remove_mount_point(const std::string &mount_point) {
+  for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) {
+    if (it->mount_point == mount_point) {
+      base_dirs_.erase(it);
+      return true;
+    }
+  }
+  return false;
+}
+
+inline Server &
+Server::set_file_extension_and_mimetype_mapping(const std::string &ext,
+                                                const std::string &mime) {
+  file_extension_and_mimetype_map_[ext] = mime;
+  return *this;
+}
+
+inline Server &Server::set_default_file_mimetype(const std::string &mime) {
+  default_file_mimetype_ = mime;
+  return *this;
+}
+
+inline Server &Server::set_file_request_handler(Handler handler) {
+  file_request_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_error_handler_core(HandlerWithResponse handler,
+                                              std::true_type) {
+  error_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_error_handler_core(Handler handler,
+                                              std::false_type) {
+  error_handler_ = [handler](const Request &req, Response &res) {
+    handler(req, res);
+    return HandlerResponse::Handled;
+  };
+  return *this;
+}
+
+inline Server &Server::set_exception_handler(ExceptionHandler handler) {
+  exception_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) {
+  pre_routing_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_post_routing_handler(Handler handler) {
+  post_routing_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
+  pre_request_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_logger(Logger logger) {
+  logger_ = std::move(logger);
+  return *this;
+}
+
+inline Server &Server::set_error_logger(ErrorLogger error_logger) {
+  error_logger_ = std::move(error_logger);
+  return *this;
+}
+
+inline Server &Server::set_pre_compression_logger(Logger logger) {
+  pre_compression_logger_ = std::move(logger);
+  return *this;
+}
+
+inline Server &
+Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
+  expect_100_continue_handler_ = std::move(handler);
+  return *this;
+}
+
+inline Server &Server::set_address_family(int family) {
+  address_family_ = family;
+  return *this;
+}
+
+inline Server &Server::set_tcp_nodelay(bool on) {
+  tcp_nodelay_ = on;
+  return *this;
+}
+
+inline Server &Server::set_ipv6_v6only(bool on) {
+  ipv6_v6only_ = on;
+  return *this;
+}
+
+inline Server &Server::set_socket_options(SocketOptions socket_options) {
+  socket_options_ = std::move(socket_options);
+  return *this;
+}
+
+inline Server &Server::set_default_headers(Headers headers) {
+  default_headers_ = std::move(headers);
+  return *this;
+}
+
+inline Server &Server::set_header_writer(
+    std::function<ssize_t(Stream &, Headers &)> const &writer) {
+  header_writer_ = writer;
+  return *this;
+}
+
+inline Server &
+Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
+  trusted_proxies_ = proxies;
+  return *this;
+}
+
+inline Server &Server::set_keep_alive_max_count(size_t count) {
+  keep_alive_max_count_ = count;
+  return *this;
+}
+
+inline Server &Server::set_keep_alive_timeout(time_t sec) {
+  keep_alive_timeout_sec_ = sec;
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &Server::set_keep_alive_timeout(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) {
+    set_keep_alive_timeout(sec);
+  });
+  return *this;
+}
+
+inline Server &Server::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+  return *this;
+}
+
+inline Server &Server::set_write_timeout(time_t sec, time_t usec) {
+  write_timeout_sec_ = sec;
+  write_timeout_usec_ = usec;
+  return *this;
+}
+
+inline Server &Server::set_idle_interval(time_t sec, time_t usec) {
+  idle_interval_sec_ = sec;
+  idle_interval_usec_ = usec;
+  return *this;
+}
+
+inline Server &Server::set_payload_max_length(size_t length) {
+  payload_max_length_ = length;
+  return *this;
+}
+
+inline Server &Server::set_websocket_max_missed_pongs(int count) {
+  websocket_max_missed_pongs_ = count;
+  return *this;
+}
+
+inline Server &Server::set_websocket_ping_interval(time_t sec) {
+  websocket_ping_interval_sec_ = sec;
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &Server::set_websocket_ping_interval(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) {
+    set_websocket_ping_interval(sec);
+  });
+  return *this;
+}
+
+inline bool Server::bind_to_port(const std::string &host, int port,
+                                 int socket_flags) {
+  auto ret = bind_internal(host, port, socket_flags);
+  if (ret == -1) { is_decommissioned = true; }
+  return ret >= 0;
+}
+inline int Server::bind_to_any_port(const std::string &host, int socket_flags) {
+  auto ret = bind_internal(host, 0, socket_flags);
+  if (ret == -1) { is_decommissioned = true; }
+  return ret;
+}
+
+inline bool Server::listen_after_bind() { return listen_internal(); }
+
+inline bool Server::listen(const std::string &host, int port,
+                           int socket_flags) {
+  return bind_to_port(host, port, socket_flags) && listen_internal();
+}
+
+inline bool Server::is_running() const { return is_running_; }
+
+inline void Server::wait_until_ready() const {
+  while (!is_running_ && !is_decommissioned) {
+    std::this_thread::sleep_for(std::chrono::milliseconds{1});
+  }
+}
+
+inline void Server::stop() {
+  if (is_running_) {
+    assert(svr_sock_ != INVALID_SOCKET);
+    std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
+    detail::shutdown_socket(sock);
+    detail::close_socket(sock);
+  }
+  is_decommissioned = false;
+}
+
+inline void Server::decommission() { is_decommissioned = true; }
+
+inline bool Server::parse_request_line(const char *s, Request &req) const {
+  auto len = strlen(s);
+  if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; }
+  len -= 2;
+
+  {
+    size_t count = 0;
+
+    detail::split(s, s + len, ' ', [&](const char *b, const char *e) {
+      switch (count) {
+      case 0: req.method = std::string(b, e); break;
+      case 1: req.target = std::string(b, e); break;
+      case 2: req.version = std::string(b, e); break;
+      default: break;
+      }
+      count++;
+    });
+
+    if (count != 3) { return false; }
+  }
+
+  thread_local const std::set<std::string> methods{
+      "GET",     "HEAD",    "POST",  "PUT",   "DELETE",
+      "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"};
+
+  if (methods.find(req.method) == methods.end()) {
+    output_error_log(Error::InvalidHTTPMethod, &req);
+    return false;
+  }
+
+  if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") {
+    output_error_log(Error::InvalidHTTPVersion, &req);
+    return false;
+  }
+
+  {
+    // Skip URL fragment
+    for (size_t i = 0; i < req.target.size(); i++) {
+      if (req.target[i] == '#') {
+        req.target.erase(i);
+        break;
+      }
+    }
+
+    detail::divide(req.target, '?',
+                   [&](const char *lhs_data, std::size_t lhs_size,
+                       const char *rhs_data, std::size_t rhs_size) {
+                     req.path =
+                         decode_path_component(std::string(lhs_data, lhs_size));
+                     detail::parse_query_text(rhs_data, rhs_size, req.params);
+                   });
+  }
+
+  return true;
+}
+
+inline bool Server::write_response(Stream &strm, bool close_connection,
+                                   Request &req, Response &res) {
+  // NOTE: `req.ranges` should be empty, otherwise it will be applied
+  // incorrectly to the error content.
+  req.ranges.clear();
+  return write_response_core(strm, close_connection, req, res, false);
+}
+
+inline bool Server::write_response_with_content(Stream &strm,
+                                                bool close_connection,
+                                                const Request &req,
+                                                Response &res) {
+  return write_response_core(strm, close_connection, req, res, true);
+}
+
+inline bool Server::write_response_core(Stream &strm, bool close_connection,
+                                        const Request &req, Response &res,
+                                        bool need_apply_ranges) {
+  assert(res.status != -1);
+
+  if (400 <= res.status && error_handler_ &&
+      error_handler_(req, res) == HandlerResponse::Handled) {
+    need_apply_ranges = true;
+  }
+
+  std::string content_type;
+  std::string boundary;
+  if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }
+
+  // Prepare additional headers
+  if (close_connection || req.get_header_value("Connection") == "close" ||
+      400 <= res.status) { // Don't leave connections open after errors
+    res.set_header("Connection", "close");
+  } else {
+    std::string s = "timeout=";
+    s += std::to_string(keep_alive_timeout_sec_);
+    s += ", max=";
+    s += std::to_string(keep_alive_max_count_);
+    res.set_header("Keep-Alive", s);
+  }
+
+  if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&
+      !res.has_header("Content-Type")) {
+    res.set_header("Content-Type", "text/plain");
+  }
+
+  if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
+      !res.has_header("Content-Length")) {
+    res.set_header("Content-Length", "0");
+  }
+
+  if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
+    res.set_header("Accept-Ranges", "bytes");
+  }
+
+  if (post_routing_handler_) { post_routing_handler_(req, res); }
+
+  // Response line and headers
+  detail::BufferStream bstrm;
+  if (!detail::write_response_line(bstrm, res.status)) { return false; }
+  if (header_writer_(bstrm, res.headers) <= 0) { return false; }
+
+  // Combine small body with headers to reduce write syscalls
+  if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
+    bstrm.write(res.body.data(), res.body.size());
+  }
+
+  // Log before writing to avoid race condition with client-side code that
+  // accesses logger-captured data immediately after receiving the response.
+  output_log(req, res);
+
+  // Flush buffer
+  auto &data = bstrm.get_buffer();
+  if (!detail::write_data(strm, data.data(), data.size())) { return false; }
+
+  // Streaming body
+  auto ret = true;
+  if (req.method != "HEAD" && res.content_provider_) {
+    if (write_content_with_provider(strm, req, res, boundary, content_type)) {
+      res.content_provider_success_ = true;
+    } else {
+      ret = false;
+    }
+  }
+
+  return ret;
+}
+
+inline bool
+Server::write_content_with_provider(Stream &strm, const Request &req,
+                                    Response &res, const std::string &boundary,
+                                    const std::string &content_type) {
+  auto is_shutting_down = [this]() {
+    return this->svr_sock_ == INVALID_SOCKET;
+  };
+
+  if (res.content_length_ > 0) {
+    if (req.ranges.empty()) {
+      return detail::write_content(strm, res.content_provider_, 0,
+                                   res.content_length_, is_shutting_down);
+    } else if (req.ranges.size() == 1) {
+      auto offset_and_length = detail::get_range_offset_and_length(
+          req.ranges[0], res.content_length_);
+
+      return detail::write_content(strm, res.content_provider_,
+                                   offset_and_length.first,
+                                   offset_and_length.second, is_shutting_down);
+    } else {
+      return detail::write_multipart_ranges_data(
+          strm, req, res, boundary, content_type, res.content_length_,
+          is_shutting_down);
+    }
+  } else {
+    if (res.is_chunked_content_provider_) {
+      auto type = detail::encoding_type(req, res);
+
+      auto compressor = detail::make_compressor(type);
+      if (!compressor) {
+        compressor = detail::make_unique<detail::nocompressor>();
+      }
+
+      return detail::write_content_chunked(strm, res.content_provider_,
+                                           is_shutting_down, *compressor);
+    } else {
+      return detail::write_content_without_length(strm, res.content_provider_,
+                                                  is_shutting_down);
+    }
+  }
+}
+
+inline bool Server::read_content(Stream &strm, Request &req, Response &res) {
+  FormFields::iterator cur_field;
+  FormFiles::iterator cur_file;
+  auto is_text_field = false;
+  size_t count = 0;
+  if (read_content_core(
+          strm, req, res,
+          // Regular
+          [&](const char *buf, size_t n) {
+            // Prevent arithmetic overflow when checking sizes.
+            // Avoid computing (req.body.size() + n) directly because
+            // adding two unsigned `size_t` values can wrap around and
+            // produce a small result instead of indicating overflow.
+            // Instead, check using subtraction: ensure `n` does not
+            // exceed the remaining capacity `max_size() - size()`.
+            if (req.body.size() >= req.body.max_size() ||
+                n > req.body.max_size() - req.body.size()) {
+              return false;
+            }
+
+            // Limit decompressed body size to payload_max_length_ to protect
+            // against "zip bomb" attacks where a small compressed payload
+            // decompresses to a massive size.
+            if (payload_max_length_ > 0 &&
+                (req.body.size() >= payload_max_length_ ||
+                 n > payload_max_length_ - req.body.size())) {
+              return false;
+            }
+
+            req.body.append(buf, n);
+            return true;
+          },
+          // Multipart FormData
+          [&](const FormData &file) {
+            if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) {
+              output_error_log(Error::TooManyFormDataFiles, &req);
+              return false;
+            }
+
+            if (file.filename.empty()) {
+              cur_field = req.form.fields.emplace(
+                  file.name, FormField{file.name, file.content, file.headers});
+              is_text_field = true;
+            } else {
+              cur_file = req.form.files.emplace(file.name, file);
+              is_text_field = false;
+            }
+            return true;
+          },
+          [&](const char *buf, size_t n) {
+            if (is_text_field) {
+              auto &content = cur_field->second.content;
+              if (content.size() + n > content.max_size()) { return false; }
+              content.append(buf, n);
+            } else {
+              auto &content = cur_file->second.content;
+              if (content.size() + n > content.max_size()) { return false; }
+              content.append(buf, n);
+            }
+            return true;
+          })) {
+    const auto &content_type = req.get_header_value("Content-Type");
+    if (detail::extract_media_type(content_type) ==
+        "application/x-www-form-urlencoded") {
+      if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) {
+        res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414?
+        output_error_log(Error::ExceedMaxPayloadSize, &req);
+        return false;
+      }
+      detail::parse_query_text(req.body, req.params);
+    }
+    return true;
+  }
+  return false;
+}
+
+inline bool Server::read_content_with_content_receiver(
+    Stream &strm, Request &req, Response &res, ContentReceiver receiver,
+    FormDataHeader multipart_header, ContentReceiver multipart_receiver) {
+  return read_content_core(strm, req, res, std::move(receiver),
+                           std::move(multipart_header),
+                           std::move(multipart_receiver));
+}
+
+inline bool Server::read_content_core(
+    Stream &strm, Request &req, Response &res, ContentReceiver receiver,
+    FormDataHeader multipart_header, ContentReceiver multipart_receiver) const {
+  detail::FormDataParser multipart_form_data_parser;
+  ContentReceiverWithProgress out;
+
+  if (req.is_multipart_form_data()) {
+    const auto &content_type = req.get_header_value("Content-Type");
+    std::string boundary;
+    if (!detail::parse_multipart_boundary(content_type, boundary)) {
+      res.status = StatusCode::BadRequest_400;
+      output_error_log(Error::MultipartParsing, &req);
+      return false;
+    }
+
+    multipart_form_data_parser.set_boundary(std::move(boundary));
+    out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) {
+      return multipart_form_data_parser.parse(buf, n, multipart_header,
+                                              multipart_receiver);
+    };
+  } else {
+    out = [receiver](const char *buf, size_t n, size_t /*off*/,
+                     size_t /*len*/) { return receiver(buf, n); };
+  }
+
+  // RFC 7230 Section 3.3.3: If this is a request message and none of the above
+  // are true (no Transfer-Encoding and no Content-Length), then the message
+  // body length is zero (no message body is present).
+  //
+  // For non-SSL builds, detect clients that send a body without a
+  // Content-Length header (raw HTTP over TCP). Check both the stream's
+  // internal read buffer (data already read from the socket during header
+  // parsing) and the socket itself for pending data. If data is found and
+  // exceeds the configured payload limit, reject with 413.
+  // For SSL builds we cannot reliably peek the decrypted application bytes,
+  // so keep the original behaviour.
+#if !defined(CPPHTTPLIB_SSL_ENABLED)
+  if (!req.has_header("Content-Length") &&
+      !detail::is_chunked_transfer_encoding(req.headers)) {
+    // Only check if payload_max_length is set to a finite value
+    if (payload_max_length_ > 0 &&
+        payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
+      // Check if there is data already buffered in the stream (read during
+      // header parsing) or pending on the socket. Use a non-blocking socket
+      // check to avoid deadlock when the client sends no body.
+      bool has_data = strm.is_readable();
+      if (!has_data) {
+        socket_t s = strm.socket();
+        if (s != INVALID_SOCKET) {
+          has_data = detail::select_read(s, 0, 0) > 0;
+        }
+      }
+      if (has_data) {
+        auto result =
+            detail::read_content_without_length(strm, payload_max_length_, out);
+        if (result == detail::ReadContentResult::PayloadTooLarge) {
+          res.status = StatusCode::PayloadTooLarge_413;
+          return false;
+        } else if (result != detail::ReadContentResult::Success) {
+          return false;
+        }
+        return true;
+      }
+    }
+    return true;
+  }
+#else
+  if (!req.has_header("Content-Length") &&
+      !detail::is_chunked_transfer_encoding(req.headers)) {
+    return true;
+  }
+#endif
+
+  if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
+                            out, true)) {
+    return false;
+  }
+
+  req.body_consumed_ = true;
+
+  if (req.is_multipart_form_data()) {
+    if (!multipart_form_data_parser.is_valid()) {
+      res.status = StatusCode::BadRequest_400;
+      output_error_log(Error::MultipartParsing, &req);
+      return false;
+    }
+  }
+
+  return true;
+}
+
+inline bool Server::handle_file_request(Request &req, Response &res) {
+  for (const auto &entry : base_dirs_) {
+    // Prefix match
+    if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
+      std::string sub_path = "/" + req.path.substr(entry.mount_point.size());
+      if (detail::is_valid_path(sub_path)) {
+        auto path = entry.base_dir + sub_path;
+        if (path.back() == '/') { path += "index.html"; }
+
+        // Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
+        // but symlinks/junctions can still escape the base directory.
+        if (!entry.resolved_base_dir.empty()) {
+          std::string resolved_path;
+          if (detail::canonicalize_path(path.c_str(), resolved_path) &&
+              !detail::is_path_within_base(resolved_path,
+                                           entry.resolved_base_dir)) {
+            res.status = StatusCode::Forbidden_403;
+            return true;
+          }
+        }
+
+        detail::FileStat stat(path);
+
+        if (stat.is_dir()) {
+          res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
+          return true;
+        }
+
+        if (stat.is_file()) {
+          for (const auto &kv : entry.headers) {
+            res.set_header(kv.first, kv.second);
+          }
+
+          auto etag = detail::compute_etag(stat);
+          if (!etag.empty()) { res.set_header("ETag", etag); }
+
+          auto mtime = stat.mtime();
+
+          auto last_modified = detail::file_mtime_to_http_date(mtime);
+          if (!last_modified.empty()) {
+            res.set_header("Last-Modified", last_modified);
+          }
+
+          if (check_if_not_modified(req, res, etag, mtime)) { return true; }
+
+          check_if_range(req, etag, mtime);
+
+          auto mm = std::make_shared<detail::mmap>(path.c_str());
+          if (!mm->is_open()) {
+            output_error_log(Error::OpenFile, &req);
+            return false;
+          }
+
+          res.set_content_provider(
+              mm->size(),
+              detail::find_content_type(path, file_extension_and_mimetype_map_,
+                                        default_file_mimetype_),
+              [mm](size_t offset, size_t length, DataSink &sink) -> bool {
+                sink.write(mm->data() + offset, length);
+                return true;
+              });
+
+          if (req.method != "HEAD" && file_request_handler_) {
+            file_request_handler_(req, res);
+          }
+
+          return true;
+        } else {
+          output_error_log(Error::OpenFile, &req);
+        }
+      }
+    }
+  }
+  return false;
+}
+
+inline bool Server::check_if_not_modified(const Request &req, Response &res,
+                                          const std::string &etag,
+                                          time_t mtime) const {
+  // Handle conditional GET:
+  // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2)
+  // 2. If-Modified-Since is checked only when If-None-Match is absent
+  if (req.has_header("If-None-Match")) {
+    if (!etag.empty()) {
+      auto val = req.get_header_value("If-None-Match");
+
+      // NOTE: We use exact string matching here. This works correctly
+      // because our server always generates weak ETags (W/"..."), and
+      // clients typically send back the same ETag they received.
+      // RFC 9110 Section 8.8.3.2 allows weak comparison for
+      // If-None-Match, where W/"x" and "x" would match, but this
+      // simplified implementation requires exact matches.
+      auto ret = detail::split_find(val.data(), val.data() + val.size(), ',',
+                                    [&](const char *b, const char *e) {
+                                      auto seg_len = static_cast<size_t>(e - b);
+                                      return (seg_len == 1 && *b == '*') ||
+                                             (seg_len == etag.size() &&
+                                              std::equal(b, e, etag.begin()));
+                                    });
+
+      if (ret) {
+        res.status = StatusCode::NotModified_304;
+        return true;
+      }
+    }
+  } else if (req.has_header("If-Modified-Since")) {
+    auto val = req.get_header_value("If-Modified-Since");
+    auto t = detail::parse_http_date(val);
+
+    if (t != static_cast<time_t>(-1) && mtime <= t) {
+      res.status = StatusCode::NotModified_304;
+      return true;
+    }
+  }
+  return false;
+}
+
+inline bool Server::check_if_range(Request &req, const std::string &etag,
+                                   time_t mtime) const {
+  // Handle If-Range for partial content requests (RFC 9110
+  // Section 13.1.5). If-Range is only evaluated when Range header is
+  // present. If the validator matches, serve partial content; otherwise
+  // serve full content.
+  if (!req.ranges.empty() && req.has_header("If-Range")) {
+    auto val = req.get_header_value("If-Range");
+
+    auto is_valid_range = [&]() {
+      if (detail::is_strong_etag(val)) {
+        // RFC 9110 Section 13.1.5: If-Range requires strong ETag
+        // comparison.
+        return (!etag.empty() && val == etag);
+      } else if (detail::is_weak_etag(val)) {
+        // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5)
+        return false;
+      } else {
+        // HTTP-date comparison
+        auto t = detail::parse_http_date(val);
+        return (t != static_cast<time_t>(-1) && mtime <= t);
+      }
+    };
+
+    if (!is_valid_range()) {
+      // Validator doesn't match: ignore Range and serve full content
+      req.ranges.clear();
+      return false;
+    }
+  }
+
+  return true;
+}
+
+inline socket_t
+Server::create_server_socket(const std::string &host, int port,
+                             int socket_flags,
+                             SocketOptions socket_options) const {
+  return detail::create_socket(
+      host, std::string(), port, address_family_, socket_flags, tcp_nodelay_,
+      ipv6_v6only_, std::move(socket_options),
+      [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool {
+        if (::bind(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen))) {
+          output_error_log(Error::BindIPAddress, nullptr);
+          return false;
+        }
+        if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) {
+          output_error_log(Error::Listen, nullptr);
+          return false;
+        }
+        return true;
+      });
+}
+
+inline int Server::bind_internal(const std::string &host, int port,
+                                 int socket_flags) {
+  if (is_decommissioned) { return -1; }
+
+  if (!is_valid()) { return -1; }
+
+  svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_);
+  if (svr_sock_ == INVALID_SOCKET) { return -1; }
+
+  if (port == 0) {
+    struct sockaddr_storage addr;
+    socklen_t addr_len = sizeof(addr);
+    if (getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr),
+                    &addr_len) == -1) {
+      output_error_log(Error::GetSockName, nullptr);
+      return -1;
+    }
+    if (addr.ss_family == AF_INET) {
+      return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
+    } else if (addr.ss_family == AF_INET6) {
+      return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);
+    } else {
+      output_error_log(Error::UnsupportedAddressFamily, nullptr);
+      return -1;
+    }
+  } else {
+    return port;
+  }
+}
+
+inline bool Server::listen_internal() {
+  if (is_decommissioned) { return false; }
+
+  auto ret = true;
+  is_running_ = true;
+  auto se = detail::scope_exit([&]() { is_running_ = false; });
+
+  {
+    std::unique_ptr<TaskQueue> task_queue(new_task_queue());
+
+    while (svr_sock_ != INVALID_SOCKET) {
+#ifndef _WIN32
+      if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) {
+#endif
+        auto val = detail::select_read(svr_sock_, idle_interval_sec_,
+                                       idle_interval_usec_);
+        if (val == 0) { // Timeout
+          task_queue->on_idle();
+          continue;
+        }
+#ifndef _WIN32
+      }
+#endif
+
+#if defined _WIN32
+      // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT,
+      // OVERLAPPED
+      socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0);
+#elif defined SOCK_CLOEXEC
+      socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC);
+#else
+      socket_t sock = accept(svr_sock_, nullptr, nullptr);
+#endif
+
+      if (sock == INVALID_SOCKET) {
+        if (errno == EMFILE) {
+          // The per-process limit of open file descriptors has been reached.
+          // Try to accept new connections after a short sleep.
+          std::this_thread::sleep_for(std::chrono::microseconds{1});
+          continue;
+        } else if (errno == EINTR || errno == EAGAIN) {
+          continue;
+        }
+        if (svr_sock_ != INVALID_SOCKET) {
+          detail::close_socket(svr_sock_);
+          ret = false;
+          output_error_log(Error::Connection, nullptr);
+        } else {
+          ; // The server socket was closed by user.
+        }
+        break;
+      }
+
+      detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO,
+                                  read_timeout_sec_, read_timeout_usec_);
+      detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,
+                                  write_timeout_sec_, write_timeout_usec_);
+
+      if (tcp_nodelay_) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); }
+
+      if (!task_queue->enqueue(
+              [this, sock]() { process_and_close_socket(sock); })) {
+        output_error_log(Error::ResourceExhaustion, nullptr);
+        detail::shutdown_socket(sock);
+        detail::close_socket(sock);
+      }
+    }
+
+    task_queue->shutdown();
+  }
+
+  is_decommissioned = !ret;
+  return ret;
+}
+
+inline bool Server::routing(Request &req, Response &res, Stream &strm) {
+  if (pre_routing_handler_ &&
+      pre_routing_handler_(req, res) == HandlerResponse::Handled) {
+    return true;
+  }
+
+  // File handler
+  if ((req.method == "GET" || req.method == "HEAD") &&
+      handle_file_request(req, res)) {
+    return true;
+  }
+
+  if (detail::expect_content(req)) {
+    // Content reader handler
+    {
+      // Track whether the ContentReader was aborted due to the decompressed
+      // payload exceeding `payload_max_length_`.
+      // The user handler runs after the lambda returns, so we must restore the
+      // 413 status if the handler overwrites it.
+      bool content_reader_payload_too_large = false;
+
+      ContentReader reader(
+          [&](ContentReceiver receiver) {
+            auto result = read_content_with_content_receiver(
+                strm, req, res, std::move(receiver), nullptr, nullptr);
+            if (!result) {
+              output_error_log(Error::Read, &req);
+              if (res.status == StatusCode::PayloadTooLarge_413) {
+                content_reader_payload_too_large = true;
+              }
+            }
+            return result;
+          },
+          [&](FormDataHeader header, ContentReceiver receiver) {
+            auto result = read_content_with_content_receiver(
+                strm, req, res, nullptr, std::move(header),
+                std::move(receiver));
+            if (!result) {
+              output_error_log(Error::Read, &req);
+              if (res.status == StatusCode::PayloadTooLarge_413) {
+                content_reader_payload_too_large = true;
+              }
+            }
+            return result;
+          });
+
+      bool dispatched = false;
+      if (req.method == "POST") {
+        dispatched = dispatch_request_for_content_reader(
+            req, res, std::move(reader), post_handlers_for_content_reader_);
+      } else if (req.method == "PUT") {
+        dispatched = dispatch_request_for_content_reader(
+            req, res, std::move(reader), put_handlers_for_content_reader_);
+      } else if (req.method == "PATCH") {
+        dispatched = dispatch_request_for_content_reader(
+            req, res, std::move(reader), patch_handlers_for_content_reader_);
+      } else if (req.method == "DELETE") {
+        dispatched = dispatch_request_for_content_reader(
+            req, res, std::move(reader), delete_handlers_for_content_reader_);
+      }
+
+      if (dispatched) {
+        if (content_reader_payload_too_large) {
+          // Enforce the limit: override any status the handler may have set
+          // and return false so the error path sends a plain 413 response.
+          res.status = StatusCode::PayloadTooLarge_413;
+          res.body.clear();
+          res.content_length_ = 0;
+          res.content_provider_ = nullptr;
+          return false;
+        }
+        return true;
+      }
+    }
+
+    // Read content into `req.body`
+    if (!read_content(strm, req, res)) {
+      output_error_log(Error::Read, &req);
+      return false;
+    }
+  }
+
+  // Regular handler
+  if (req.method == "GET" || req.method == "HEAD") {
+    return dispatch_request(req, res, get_handlers_);
+  } else if (req.method == "POST") {
+    return dispatch_request(req, res, post_handlers_);
+  } else if (req.method == "PUT") {
+    return dispatch_request(req, res, put_handlers_);
+  } else if (req.method == "DELETE") {
+    return dispatch_request(req, res, delete_handlers_);
+  } else if (req.method == "OPTIONS") {
+    return dispatch_request(req, res, options_handlers_);
+  } else if (req.method == "PATCH") {
+    return dispatch_request(req, res, patch_handlers_);
+  }
+
+  res.status = StatusCode::BadRequest_400;
+  return false;
+}
+
+inline bool Server::dispatch_request(Request &req, Response &res,
+                                     const Handlers &handlers) const {
+  for (const auto &x : handlers) {
+    const auto &matcher = x.first;
+    const auto &handler = x.second;
+
+    if (matcher->match(req)) {
+      req.matched_route = matcher->pattern();
+      if (!pre_request_handler_ ||
+          pre_request_handler_(req, res) != HandlerResponse::Handled) {
+        handler(req, res);
+      }
+      return true;
+    }
+  }
+  return false;
+}
+
+inline void Server::apply_ranges(const Request &req, Response &res,
+                                 std::string &content_type,
+                                 std::string &boundary) const {
+  if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) {
+    auto it = res.headers.find("Content-Type");
+    if (it != res.headers.end()) {
+      content_type = it->second;
+      res.headers.erase(it);
+    }
+
+    boundary = detail::make_multipart_data_boundary();
+
+    res.set_header("Content-Type",
+                   "multipart/byteranges; boundary=" + boundary);
+  }
+
+  auto type = detail::encoding_type(req, res);
+
+  if (res.body.empty()) {
+    if (res.content_length_ > 0) {
+      size_t length = 0;
+      if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) {
+        length = res.content_length_;
+      } else if (req.ranges.size() == 1) {
+        auto offset_and_length = detail::get_range_offset_and_length(
+            req.ranges[0], res.content_length_);
+
+        length = offset_and_length.second;
+
+        auto content_range = detail::make_content_range_header_field(
+            offset_and_length, res.content_length_);
+        res.set_header("Content-Range", content_range);
+      } else {
+        length = detail::get_multipart_ranges_data_length(
+            req, boundary, content_type, res.content_length_);
+      }
+      res.set_header("Content-Length", std::to_string(length));
+    } else {
+      if (res.content_provider_) {
+        if (res.is_chunked_content_provider_) {
+          res.set_header("Transfer-Encoding", "chunked");
+          if (type != detail::EncodingType::None) {
+            res.set_header("Content-Encoding", detail::encoding_name(type));
+            res.set_header("Vary", "Accept-Encoding");
+          }
+        }
+      }
+    }
+  } else {
+    if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) {
+      ;
+    } else if (req.ranges.size() == 1) {
+      auto offset_and_length =
+          detail::get_range_offset_and_length(req.ranges[0], res.body.size());
+      auto offset = offset_and_length.first;
+      auto length = offset_and_length.second;
+
+      auto content_range = detail::make_content_range_header_field(
+          offset_and_length, res.body.size());
+      res.set_header("Content-Range", content_range);
+
+      assert(offset + length <= res.body.size());
+      res.body = res.body.substr(offset, length);
+    } else {
+      std::string data;
+      detail::make_multipart_ranges_data(req, res, boundary, content_type,
+                                         res.body.size(), data);
+      res.body.swap(data);
+    }
+
+    if (type != detail::EncodingType::None) {
+      output_pre_compression_log(req, res);
+
+      if (auto compressor = detail::make_compressor(type)) {
+        std::string compressed;
+        if (compressor->compress(res.body.data(), res.body.size(), true,
+                                 [&](const char *data, size_t data_len) {
+                                   compressed.append(data, data_len);
+                                   return true;
+                                 })) {
+          res.body.swap(compressed);
+          res.set_header("Content-Encoding", detail::encoding_name(type));
+          res.set_header("Vary", "Accept-Encoding");
+        }
+      }
+    }
+
+    auto length = std::to_string(res.body.size());
+    res.set_header("Content-Length", length);
+  }
+}
+
+inline bool Server::dispatch_request_for_content_reader(
+    Request &req, Response &res, ContentReader content_reader,
+    const HandlersForContentReader &handlers) const {
+  for (const auto &x : handlers) {
+    const auto &matcher = x.first;
+    const auto &handler = x.second;
+
+    if (matcher->match(req)) {
+      req.matched_route = matcher->pattern();
+      if (!pre_request_handler_ ||
+          pre_request_handler_(req, res) != HandlerResponse::Handled) {
+        handler(req, res, content_reader);
+      }
+      return true;
+    }
+  }
+  return false;
+}
+
+inline std::string
+get_client_ip(const std::string &x_forwarded_for,
+              const std::vector<std::string> &trusted_proxies) {
+  // X-Forwarded-For is a comma-separated list per RFC 7239
+  std::vector<std::string> ip_list;
+  detail::split(x_forwarded_for.data(),
+                x_forwarded_for.data() + x_forwarded_for.size(), ',',
+                [&](const char *b, const char *e) {
+                  auto r = detail::trim(b, e, 0, static_cast<size_t>(e - b));
+                  ip_list.emplace_back(std::string(b + r.first, b + r.second));
+                });
+
+  for (size_t i = 0; i < ip_list.size(); ++i) {
+    auto ip = ip_list[i];
+
+    auto is_trusted_proxy =
+        std::any_of(trusted_proxies.begin(), trusted_proxies.end(),
+                    [&](const std::string &proxy) { return ip == proxy; });
+
+    if (is_trusted_proxy) {
+      if (i == 0) {
+        // If the trusted proxy is the first IP, there's no preceding client IP
+        return ip;
+      } else {
+        // Return the IP immediately before the trusted proxy
+        return ip_list[i - 1];
+      }
+    }
+  }
+
+  // If no trusted proxy is found, return the first IP in the list
+  return ip_list.front();
+}
+
+inline bool
+Server::process_request(Stream &strm, const std::string &remote_addr,
+                        int remote_port, const std::string &local_addr,
+                        int local_port, bool close_connection,
+                        bool &connection_closed,
+                        const std::function<void(Request &)> &setup_request,
+                        bool *websocket_upgraded) {
+  std::array<char, 2048> buf{};
+
+  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
+
+  // Connection has been closed on client
+  if (!line_reader.getline()) { return false; }
+
+  Request req;
+  req.start_time_ = std::chrono::steady_clock::now();
+  req.remote_addr = remote_addr;
+  req.remote_port = remote_port;
+  req.local_addr = local_addr;
+  req.local_port = local_port;
+
+  Response res;
+  res.version = "HTTP/1.1";
+  res.headers = default_headers_;
+
+  // Request line and headers
+  if (!parse_request_line(line_reader.ptr(), req)) {
+    res.status = StatusCode::BadRequest_400;
+    output_error_log(Error::InvalidRequestLine, &req);
+    return write_response(strm, close_connection, req, res);
+  }
+
+  // Request headers
+  if (!detail::read_headers(strm, req.headers)) {
+    res.status = StatusCode::BadRequest_400;
+    output_error_log(Error::InvalidHeaders, &req);
+    return write_response(strm, close_connection, req, res);
+  }
+
+  // RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
+  // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
+  // tolerated for compatibility with existing clients.
+  if (req.get_header_value_u64("Content-Length") > 0 &&
+      req.has_header("Transfer-Encoding")) {
+    connection_closed = true;
+    res.status = StatusCode::BadRequest_400;
+    return write_response(strm, close_connection, req, res);
+  }
+
+  // Check if the request URI doesn't exceed the limit
+  if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
+    connection_closed = true;
+    res.status = StatusCode::UriTooLong_414;
+    output_error_log(Error::ExceedUriMaxLength, &req);
+    return write_response(strm, close_connection, req, res);
+  }
+
+  if (req.get_header_value("Connection") == "close") {
+    connection_closed = true;
+  }
+
+  if (req.version == "HTTP/1.0" &&
+      req.get_header_value("Connection") != "Keep-Alive") {
+    connection_closed = true;
+  }
+
+  if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
+    auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
+    req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
+  } else {
+    req.remote_addr = remote_addr;
+  }
+  req.remote_port = remote_port;
+
+  req.local_addr = local_addr;
+  req.local_port = local_port;
+
+  if (req.has_header("Accept")) {
+    const auto &accept_header = req.get_header_value("Accept");
+    if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {
+      connection_closed = true;
+      res.status = StatusCode::BadRequest_400;
+      output_error_log(Error::HTTPParsing, &req);
+      return write_response(strm, close_connection, req, res);
+    }
+  }
+
+  if (req.has_header("Range")) {
+    const auto &range_header_value = req.get_header_value("Range");
+    if (!detail::parse_range_header(range_header_value, req.ranges)) {
+      connection_closed = true;
+      res.status = StatusCode::RangeNotSatisfiable_416;
+      output_error_log(Error::InvalidRangeHeader, &req);
+      return write_response(strm, close_connection, req, res);
+    }
+  }
+
+  if (setup_request) { setup_request(req); }
+
+  if (req.get_header_value("Expect") == "100-continue") {
+    int status = StatusCode::Continue_100;
+    if (expect_100_continue_handler_) {
+      status = expect_100_continue_handler_(req, res);
+    }
+    switch (status) {
+    case StatusCode::Continue_100:
+    case StatusCode::ExpectationFailed_417:
+      detail::write_response_line(strm, status);
+      strm.write("\r\n");
+      break;
+    default:
+      connection_closed = true;
+      return write_response(strm, true, req, res);
+    }
+  }
+
+  // Setup `is_connection_closed` method
+  auto sock = strm.socket();
+  req.is_connection_closed = [sock]() {
+    return !detail::is_socket_alive(sock);
+  };
+
+  // WebSocket upgrade
+  // Check pre_routing_handler_ before upgrading so that authentication
+  // and other middleware can reject the request with an HTTP response
+  // (e.g., 401) before the protocol switches.
+  if (detail::is_websocket_upgrade(req)) {
+    if (pre_routing_handler_ &&
+        pre_routing_handler_(req, res) == HandlerResponse::Handled) {
+      if (res.status == -1) { res.status = StatusCode::OK_200; }
+      return write_response(strm, close_connection, req, res);
+    }
+    // Find matching WebSocket handler
+    for (const auto &entry : websocket_handlers_) {
+      if (entry.matcher->match(req)) {
+        // Compute accept key
+        auto client_key = req.get_header_value("Sec-WebSocket-Key");
+        auto accept_key = detail::websocket_accept_key(client_key);
+
+        // Negotiate subprotocol
+        std::string selected_subprotocol;
+        if (entry.sub_protocol_selector) {
+          auto protocol_header = req.get_header_value("Sec-WebSocket-Protocol");
+          if (!protocol_header.empty()) {
+            std::vector<std::string> protocols;
+            std::istringstream iss(protocol_header);
+            std::string token;
+            while (std::getline(iss, token, ',')) {
+              // Trim whitespace
+              auto start = token.find_first_not_of(' ');
+              auto end = token.find_last_not_of(' ');
+              if (start != std::string::npos) {
+                protocols.push_back(token.substr(start, end - start + 1));
+              }
+            }
+            selected_subprotocol = entry.sub_protocol_selector(protocols);
+          }
+        }
+
+        // Send 101 Switching Protocols
+        std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n"
+                                         "Upgrade: websocket\r\n"
+                                         "Connection: Upgrade\r\n"
+                                         "Sec-WebSocket-Accept: " +
+                                         accept_key + "\r\n";
+        if (!selected_subprotocol.empty()) {
+          if (!detail::fields::is_field_value(selected_subprotocol)) {
+            return false;
+          }
+          handshake_response +=
+              "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n";
+        }
+        handshake_response += "\r\n";
+        if (strm.write(handshake_response.data(), handshake_response.size()) <
+            0) {
+          return false;
+        }
+
+        connection_closed = true;
+        if (websocket_upgraded) { *websocket_upgraded = true; }
+
+        {
+          // Use WebSocket-specific read timeout instead of HTTP timeout
+          strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
+          ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
+                           websocket_max_missed_pongs_);
+          entry.handler(req, ws);
+        }
+        return true;
+      }
+    }
+    // No matching handler - fall through to 404
+  }
+
+  // Routing
+  auto routed = false;
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+  routed = routing(req, res, strm);
+#else
+  try {
+    routed = routing(req, res, strm);
+  } catch (std::exception &) {
+    if (exception_handler_) {
+      auto ep = std::current_exception();
+      exception_handler_(req, res, ep);
+      routed = true;
+    } else {
+      res.status = StatusCode::InternalServerError_500;
+    }
+  } catch (...) {
+    if (exception_handler_) {
+      auto ep = std::current_exception();
+      exception_handler_(req, res, ep);
+      routed = true;
+    } else {
+      res.status = StatusCode::InternalServerError_500;
+    }
+  }
+#endif
+  auto ret = false;
+  if (routed) {
+    if (res.status == -1) {
+      res.status = req.ranges.empty() ? StatusCode::OK_200
+                                      : StatusCode::PartialContent_206;
+    }
+
+    // Serve file content by using a content provider
+    auto file_open_error = false;
+    if (!res.file_content_path_.empty()) {
+      const auto &path = res.file_content_path_;
+      auto mm = std::make_shared<detail::mmap>(path.c_str());
+      if (!mm->is_open()) {
+        res.body.clear();
+        res.content_length_ = 0;
+        res.content_provider_ = nullptr;
+        res.status = StatusCode::NotFound_404;
+        output_error_log(Error::OpenFile, &req);
+        file_open_error = true;
+      } else {
+        auto content_type = res.file_content_content_type_;
+        if (content_type.empty()) {
+          content_type = detail::find_content_type(
+              path, file_extension_and_mimetype_map_, default_file_mimetype_);
+        }
+
+        res.set_content_provider(
+            mm->size(), content_type,
+            [mm](size_t offset, size_t length, DataSink &sink) -> bool {
+              sink.write(mm->data() + offset, length);
+              return true;
+            });
+      }
+    }
+
+    if (file_open_error) {
+      ret = write_response(strm, close_connection, req, res);
+    } else if (detail::range_error(req, res)) {
+      res.body.clear();
+      res.content_length_ = 0;
+      res.content_provider_ = nullptr;
+      res.status = StatusCode::RangeNotSatisfiable_416;
+      ret = write_response(strm, close_connection, req, res);
+    } else {
+      ret = write_response_with_content(strm, close_connection, req, res);
+    }
+  } else {
+    if (res.status == -1) { res.status = StatusCode::NotFound_404; }
+    ret = write_response(strm, close_connection, req, res);
+  }
+
+  // Drain any unconsumed request body to prevent request smuggling on
+  // keep-alive connections.
+  if (!req.body_consumed_ && detail::expect_content(req)) {
+    int drain_status = 200; // required by read_content signature
+    if (!detail::read_content(
+            strm, req, payload_max_length_, drain_status, nullptr,
+            [](const char *, size_t, size_t, size_t) { return true; }, false)) {
+      // Body exceeds payload limit or read error — close the connection
+      // to prevent leftover bytes from being misinterpreted.
+      connection_closed = true;
+    }
+  }
+
+  return ret;
+}
+
+inline bool Server::is_valid() const { return true; }
+
+inline bool Server::process_and_close_socket(socket_t sock) {
+  std::string remote_addr;
+  int remote_port = 0;
+  detail::get_remote_ip_and_port(sock, remote_addr, remote_port);
+
+  std::string local_addr;
+  int local_port = 0;
+  detail::get_local_ip_and_port(sock, local_addr, local_port);
+
+  bool websocket_upgraded = false;
+  auto ret = detail::process_server_socket(
+      svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
+      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_,
+      [&](Stream &strm, bool close_connection, bool &connection_closed) {
+        return process_request(strm, remote_addr, remote_port, local_addr,
+                               local_port, close_connection, connection_closed,
+                               nullptr, &websocket_upgraded);
+      });
+
+  detail::shutdown_socket(sock);
+  detail::close_socket(sock);
+  return ret;
+}
+
+inline void Server::output_log(const Request &req, const Response &res) const {
+  if (logger_) {
+    std::lock_guard<std::mutex> guard(logger_mutex_);
+    logger_(req, res);
+  }
+}
+
+inline void Server::output_pre_compression_log(const Request &req,
+                                               const Response &res) const {
+  if (pre_compression_logger_) {
+    std::lock_guard<std::mutex> guard(logger_mutex_);
+    pre_compression_logger_(req, res);
+  }
+}
+
+inline void Server::output_error_log(const Error &err,
+                                     const Request *req) const {
+  if (error_logger_) {
+    std::lock_guard<std::mutex> guard(logger_mutex_);
+    error_logger_(err, req);
+  }
+}
+
+/*
+ * Group 5: ClientImpl and Client (Universal) implementation
+ */
+// HTTP client implementation
+inline ClientImpl::ClientImpl(const std::string &host)
+    : ClientImpl(host, 80, std::string(), std::string()) {}
+
+inline ClientImpl::ClientImpl(const std::string &host, int port)
+    : ClientImpl(host, port, std::string(), std::string()) {}
+
+inline ClientImpl::ClientImpl(const std::string &host, int port,
+                              const std::string &client_cert_path,
+                              const std::string &client_key_path)
+    : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port),
+      client_cert_path_(client_cert_path), client_key_path_(client_key_path) {}
+
+inline ClientImpl::~ClientImpl() {
+  // Wait until all the requests in flight are handled.
+  size_t retry_count = 10;
+  while (retry_count-- > 0) {
+    {
+      std::lock_guard<std::mutex> guard(socket_mutex_);
+      if (socket_requests_in_flight_ == 0) { break; }
+    }
+    std::this_thread::sleep_for(std::chrono::milliseconds{1});
+  }
+
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+  shutdown_socket(socket_);
+  close_socket(socket_);
+}
+
+inline bool ClientImpl::is_valid() const { return true; }
+
+inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
+  client_cert_path_ = rhs.client_cert_path_;
+  client_key_path_ = rhs.client_key_path_;
+  connection_timeout_sec_ = rhs.connection_timeout_sec_;
+  read_timeout_sec_ = rhs.read_timeout_sec_;
+  read_timeout_usec_ = rhs.read_timeout_usec_;
+  write_timeout_sec_ = rhs.write_timeout_sec_;
+  write_timeout_usec_ = rhs.write_timeout_usec_;
+  max_timeout_msec_ = rhs.max_timeout_msec_;
+  basic_auth_username_ = rhs.basic_auth_username_;
+  basic_auth_password_ = rhs.basic_auth_password_;
+  bearer_token_auth_token_ = rhs.bearer_token_auth_token_;
+  keep_alive_ = rhs.keep_alive_;
+  follow_location_ = rhs.follow_location_;
+  path_encode_ = rhs.path_encode_;
+  address_family_ = rhs.address_family_;
+  tcp_nodelay_ = rhs.tcp_nodelay_;
+  ipv6_v6only_ = rhs.ipv6_v6only_;
+  socket_options_ = rhs.socket_options_;
+  compress_ = rhs.compress_;
+  decompress_ = rhs.decompress_;
+  payload_max_length_ = rhs.payload_max_length_;
+  has_payload_max_length_ = rhs.has_payload_max_length_;
+  interface_ = rhs.interface_;
+  proxy_host_ = rhs.proxy_host_;
+  proxy_port_ = rhs.proxy_port_;
+  proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;
+  proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;
+  proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;
+  logger_ = rhs.logger_;
+  error_logger_ = rhs.error_logger_;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  digest_auth_username_ = rhs.digest_auth_username_;
+  digest_auth_password_ = rhs.digest_auth_password_;
+  proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_;
+  proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_;
+  ca_cert_file_path_ = rhs.ca_cert_file_path_;
+  ca_cert_dir_path_ = rhs.ca_cert_dir_path_;
+  server_certificate_verification_ = rhs.server_certificate_verification_;
+  server_hostname_verification_ = rhs.server_hostname_verification_;
+#endif
+}
+
+inline socket_t ClientImpl::create_client_socket(Error &error) const {
+  if (!proxy_host_.empty() && proxy_port_ != -1) {
+    return detail::create_client_socket(
+        proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_,
+        ipv6_v6only_, socket_options_, connection_timeout_sec_,
+        connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_,
+        write_timeout_sec_, write_timeout_usec_, interface_, error);
+  }
+
+  // Check is custom IP specified for host_
+  std::string ip;
+  auto it = addr_map_.find(host_);
+  if (it != addr_map_.end()) { ip = it->second; }
+
+  return detail::create_client_socket(
+      host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
+      socket_options_, connection_timeout_sec_, connection_timeout_usec_,
+      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_, interface_, error);
+}
+
+inline bool ClientImpl::create_and_connect_socket(Socket &socket,
+                                                  Error &error) {
+  auto sock = create_client_socket(error);
+  if (sock == INVALID_SOCKET) { return false; }
+  socket.sock = sock;
+  return true;
+}
+
+inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
+  return create_and_connect_socket(socket, error);
+}
+
+inline bool ClientImpl::setup_proxy_connection(
+    Socket & /*socket*/,
+    std::chrono::time_point<std::chrono::steady_clock> /*start_time*/,
+    Response & /*res*/, bool & /*success*/, Error & /*error*/) {
+  return true;
+}
+
+inline void ClientImpl::shutdown_ssl(Socket & /*socket*/,
+                                     bool /*shutdown_gracefully*/) {
+  // If there are any requests in flight from threads other than us, then it's
+  // a thread-unsafe race because individual ssl* objects are not thread-safe.
+  assert(socket_requests_in_flight_ == 0 ||
+         socket_requests_are_from_thread_ == std::this_thread::get_id());
+}
+
+inline void ClientImpl::shutdown_socket(Socket &socket) const {
+  if (socket.sock == INVALID_SOCKET) { return; }
+  detail::shutdown_socket(socket.sock);
+}
+
+inline void ClientImpl::close_socket(Socket &socket) {
+  // If there are requests in flight in another thread, usually closing
+  // the socket will be fine and they will simply receive an error when
+  // using the closed socket, but it is still a bug since rarely the OS
+  // may reassign the socket id to be used for a new socket, and then
+  // suddenly they will be operating on a live socket that is different
+  // than the one they intended!
+  assert(socket_requests_in_flight_ == 0 ||
+         socket_requests_are_from_thread_ == std::this_thread::get_id());
+
+  // It is also a bug if this happens while SSL is still active
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  assert(socket.ssl == nullptr);
+#endif
+
+  if (socket.sock == INVALID_SOCKET) { return; }
+  detail::close_socket(socket.sock);
+  socket.sock = INVALID_SOCKET;
+}
+
+inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
+                                           Response &res,
+                                           bool skip_100_continue) const {
+  std::array<char, 2048> buf{};
+
+  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
+
+  if (!line_reader.getline()) { return false; }
+
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+  thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
+#else
+  thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
+#endif
+
+  std::cmatch m;
+  if (!std::regex_match(line_reader.ptr(), m, re)) {
+    return req.method == "CONNECT";
+  }
+  res.version = std::string(m[1]);
+  res.status = std::stoi(std::string(m[2]));
+  res.reason = std::string(m[3]);
+
+  // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)
+  while (skip_100_continue && res.status == StatusCode::Continue_100) {
+    if (!line_reader.getline()) { return false; } // CRLF
+    if (!line_reader.getline()) { return false; } // next response line
+
+    if (!std::regex_match(line_reader.ptr(), m, re)) { return false; }
+    res.version = std::string(m[1]);
+    res.status = std::stoi(std::string(m[2]));
+    res.reason = std::string(m[3]);
+  }
+
+  return true;
+}
+
+inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
+  std::lock_guard<std::recursive_mutex> request_mutex_guard(request_mutex_);
+  auto ret = send_(req, res, error);
+  if (error == Error::SSLPeerCouldBeClosed_) {
+    assert(!ret);
+    ret = send_(req, res, error);
+    // If still failing with SSLPeerCouldBeClosed_, convert to Read error
+    if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; }
+  }
+  return ret;
+}
+
+inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
+  {
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+
+    // Set this to false immediately - if it ever gets set to true by the end
+    // of the request, we know another thread instructed us to close the
+    // socket.
+    socket_should_be_closed_when_request_is_done_ = false;
+
+    auto is_alive = false;
+    if (socket_.is_open()) {
+      is_alive = detail::is_socket_alive(socket_.sock);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+      if (is_alive && is_ssl()) {
+        if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+          is_alive = false;
+        }
+      }
+#endif
+
+      if (!is_alive) {
+        // Attempt to avoid sigpipe by shutting down non-gracefully if it
+        // seems like the other side has already closed the connection Also,
+        // there cannot be any requests in flight from other threads since we
+        // locked request_mutex_, so safe to close everything immediately
+        const bool shutdown_gracefully = false;
+        shutdown_ssl(socket_, shutdown_gracefully);
+        shutdown_socket(socket_);
+        close_socket(socket_);
+      }
+    }
+
+    if (!is_alive) {
+      if (!ensure_socket_connection(socket_, error)) {
+        output_error_log(error, &req);
+        return false;
+      }
+
+      {
+        auto success = true;
+        if (!setup_proxy_connection(socket_, req.start_time_, res, success,
+                                    error)) {
+          if (!success) { output_error_log(error, &req); }
+          return success;
+        }
+      }
+    }
+
+    // Mark the current socket as being in use so that it cannot be closed by
+    // anyone else while this request is ongoing, even though we will be
+    // releasing the mutex.
+    if (socket_requests_in_flight_ > 1) {
+      assert(socket_requests_are_from_thread_ == std::this_thread::get_id());
+    }
+    socket_requests_in_flight_ += 1;
+    socket_requests_are_from_thread_ = std::this_thread::get_id();
+  }
+
+  for (const auto &header : default_headers_) {
+    if (req.headers.find(header.first) == req.headers.end()) {
+      req.headers.insert(header);
+    }
+  }
+
+  auto ret = false;
+  auto close_connection = !keep_alive_;
+
+  auto se = detail::scope_exit([&]() {
+    // Briefly lock mutex in order to mark that a request is no longer ongoing
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+    socket_requests_in_flight_ -= 1;
+    if (socket_requests_in_flight_ <= 0) {
+      assert(socket_requests_in_flight_ == 0);
+      socket_requests_are_from_thread_ = std::thread::id();
+    }
+
+    if (socket_should_be_closed_when_request_is_done_ || close_connection ||
+        !ret) {
+      shutdown_ssl(socket_, true);
+      shutdown_socket(socket_);
+      close_socket(socket_);
+    }
+  });
+
+  ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {
+    return handle_request(strm, req, res, close_connection, error);
+  });
+
+  if (!ret) {
+    if (error == Error::Success) {
+      error = Error::Unknown;
+      output_error_log(error, &req);
+    }
+  }
+
+  return ret;
+}
+
+inline Result ClientImpl::send(const Request &req) {
+  auto req2 = req;
+  return send_(std::move(req2));
+}
+
+inline Result ClientImpl::send_(Request &&req) {
+  auto res = detail::make_unique<Response>();
+  auto error = Error::Success;
+  auto ret = send(req, *res, error);
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
+                last_ssl_error_, last_backend_error_};
+#else
+  return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
+#endif
+}
+
+inline void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
+                                                const std::string &ct) {
+  (void)for_stream;
+  for (const auto &header : default_headers_) {
+    if (!r.has_header(header.first)) { r.headers.insert(header); }
+  }
+
+  if (!r.has_header("Host")) {
+    if (address_family_ == AF_UNIX) {
+      r.headers.emplace("Host", "localhost");
+    } else {
+      r.headers.emplace(
+          "Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
+    }
+  }
+
+  if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); }
+
+  if (!r.content_receiver) {
+    if (!r.has_header("Accept-Encoding")) {
+      std::string accept_encoding;
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+      accept_encoding = "br";
+#endif
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+      if (!accept_encoding.empty()) { accept_encoding += ", "; }
+      accept_encoding += "gzip, deflate";
+#endif
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+      if (!accept_encoding.empty()) { accept_encoding += ", "; }
+      accept_encoding += "zstd";
+#endif
+      r.set_header("Accept-Encoding", accept_encoding);
+    }
+
+#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
+    if (!r.has_header("User-Agent")) {
+      auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
+      r.set_header("User-Agent", agent);
+    }
+#endif
+  }
+
+  if (!r.body.empty()) {
+    if (!ct.empty() && !r.has_header("Content-Type")) {
+      r.headers.emplace("Content-Type", ct);
+    }
+    if (!r.has_header("Content-Length")) {
+      r.headers.emplace("Content-Length", std::to_string(r.body.size()));
+    }
+  }
+}
+
+inline ClientImpl::StreamHandle
+ClientImpl::open_stream(const std::string &method, const std::string &path,
+                        const Params &params, const Headers &headers,
+                        const std::string &body,
+                        const std::string &content_type) {
+  StreamHandle handle;
+  handle.response = detail::make_unique<Response>();
+  handle.error = Error::Success;
+
+  auto query_path = params.empty() ? path : append_query_params(path, params);
+  handle.connection_ = detail::make_unique<ClientConnection>();
+
+  {
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+
+    auto is_alive = false;
+    if (socket_.is_open()) {
+      is_alive = detail::is_socket_alive(socket_.sock);
+#ifdef CPPHTTPLIB_SSL_ENABLED
+      if (is_alive && is_ssl()) {
+        if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+          is_alive = false;
+        }
+      }
+#endif
+      if (!is_alive) {
+        shutdown_ssl(socket_, false);
+        shutdown_socket(socket_);
+        close_socket(socket_);
+      }
+    }
+
+    if (!is_alive) {
+      if (!ensure_socket_connection(socket_, handle.error)) {
+        handle.response.reset();
+        return handle;
+      }
+
+      {
+        auto success = true;
+        auto start_time = std::chrono::steady_clock::now();
+        if (!setup_proxy_connection(socket_, start_time, *handle.response,
+                                    success, handle.error)) {
+          if (!success) { handle.response.reset(); }
+          return handle;
+        }
+      }
+    }
+
+    transfer_socket_ownership_to_handle(handle);
+  }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if (is_ssl() && handle.connection_->session) {
+    handle.socket_stream_ = detail::make_unique<detail::SSLSocketStream>(
+        handle.connection_->sock, handle.connection_->session,
+        read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+        write_timeout_usec_);
+  } else {
+    handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
+        handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
+        write_timeout_sec_, write_timeout_usec_);
+  }
+#else
+  handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
+      handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
+      write_timeout_sec_, write_timeout_usec_);
+#endif
+  handle.stream_ = handle.socket_stream_.get();
+
+  Request req;
+  req.method = method;
+  req.path = query_path;
+  req.headers = headers;
+  req.body = body;
+
+  prepare_default_headers(req, true, content_type);
+
+  auto &strm = *handle.stream_;
+  if (detail::write_request_line(strm, req.method, req.path) < 0) {
+    handle.error = Error::Write;
+    handle.response.reset();
+    return handle;
+  }
+
+  if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
+                                       handle.error)) {
+    handle.response.reset();
+    return handle;
+  }
+
+  if (!body.empty()) {
+    if (strm.write(body.data(), body.size()) < 0) {
+      handle.error = Error::Write;
+      handle.response.reset();
+      return handle;
+    }
+  }
+
+  if (!read_response_line(strm, req, *handle.response) ||
+      !detail::read_headers(strm, handle.response->headers)) {
+    handle.error = Error::Read;
+    handle.response.reset();
+    return handle;
+  }
+
+  handle.body_reader_.stream = handle.stream_;
+  handle.body_reader_.payload_max_length = payload_max_length_;
+
+  if (handle.response->has_header("Content-Length")) {
+    bool is_invalid = false;
+    auto content_length = detail::get_header_value_u64(
+        handle.response->headers, "Content-Length", 0, 0, is_invalid);
+    if (is_invalid) {
+      handle.error = Error::Read;
+      handle.response.reset();
+      return handle;
+    }
+    handle.body_reader_.has_content_length = true;
+    handle.body_reader_.content_length = content_length;
+  }
+
+  auto transfer_encoding =
+      handle.response->get_header_value("Transfer-Encoding");
+  handle.body_reader_.chunked = (transfer_encoding == "chunked");
+
+  auto content_encoding = handle.response->get_header_value("Content-Encoding");
+  if (!content_encoding.empty()) {
+    handle.decompressor_ = detail::create_decompressor(content_encoding);
+  }
+
+  return handle;
+}
+
+inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {
+  if (!is_valid() || !response) { return -1; }
+
+  if (decompressor_) { return read_with_decompression(buf, len); }
+  auto n = detail::read_body_content(stream_, body_reader_, buf, len);
+
+  if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) {
+    trailers_parsed_ = true;
+    if (body_reader_.chunked_decoder) {
+      if (!body_reader_.chunked_decoder->parse_trailers_into(
+              response->trailers, response->headers)) {
+        return n;
+      }
+    } else {
+      detail::ChunkedDecoder dec(*stream_);
+      if (!dec.parse_trailers_into(response->trailers, response->headers)) {
+        return n;
+      }
+    }
+  }
+
+  return n;
+}
+
+inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,
+                                                                 size_t len) {
+  if (decompress_offset_ < decompress_buffer_.size()) {
+    auto available = decompress_buffer_.size() - decompress_offset_;
+    auto to_copy = (std::min)(len, available);
+    std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy);
+    decompress_offset_ += to_copy;
+    decompressed_bytes_read_ += to_copy;
+    return static_cast<ssize_t>(to_copy);
+  }
+
+  decompress_buffer_.clear();
+  decompress_offset_ = 0;
+
+  constexpr size_t kDecompressionBufferSize = 8192;
+  char compressed_buf[kDecompressionBufferSize];
+
+  while (true) {
+    auto n = detail::read_body_content(stream_, body_reader_, compressed_buf,
+                                       sizeof(compressed_buf));
+
+    if (n <= 0) { return n; }
+
+    bool decompress_ok = decompressor_->decompress(
+        compressed_buf, static_cast<size_t>(n),
+        [this](const char *data, size_t data_len) {
+          decompress_buffer_.append(data, data_len);
+          auto limit = body_reader_.payload_max_length;
+          if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) {
+            return false;
+          }
+          return true;
+        });
+
+    if (!decompress_ok) {
+      body_reader_.last_error = Error::Read;
+      return -1;
+    }
+
+    if (!decompress_buffer_.empty()) { break; }
+  }
+
+  auto to_copy = (std::min)(len, decompress_buffer_.size());
+  std::memcpy(buf, decompress_buffer_.data(), to_copy);
+  decompress_offset_ = to_copy;
+  decompressed_bytes_read_ += to_copy;
+  return static_cast<ssize_t>(to_copy);
+}
+
+inline void ClientImpl::StreamHandle::parse_trailers_if_needed() {
+  if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) {
+    return;
+  }
+
+  trailers_parsed_ = true;
+
+  const auto bufsiz = 128;
+  char line_buf[bufsiz];
+  detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz);
+
+  if (!line_reader.getline()) { return; }
+
+  if (!detail::parse_trailers(line_reader, response->trailers,
+                              response->headers)) {
+    return;
+  }
+}
+
+namespace detail {
+
+inline ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {}
+
+inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
+                                            size_t &out_chunk_offset,
+                                            size_t &out_chunk_total) {
+  if (finished) { return 0; }
+
+  if (chunk_remaining == 0) {
+    stream_line_reader lr(strm, line_buf, sizeof(line_buf));
+    if (!lr.getline()) { return -1; }
+
+    char *endptr = nullptr;
+    unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);
+    if (endptr == lr.ptr()) { return -1; }
+    if (chunk_len == ULONG_MAX) { return -1; }
+
+    if (chunk_len == 0) {
+      chunk_remaining = 0;
+      finished = true;
+      out_chunk_offset = 0;
+      out_chunk_total = 0;
+      return 0;
+    }
+
+    chunk_remaining = static_cast<size_t>(chunk_len);
+    last_chunk_total = chunk_remaining;
+    last_chunk_offset = 0;
+  }
+
+  auto to_read = (std::min)(chunk_remaining, len);
+  auto n = strm.read(buf, to_read);
+  if (n <= 0) { return -1; }
+
+  auto offset_before = last_chunk_offset;
+  last_chunk_offset += static_cast<size_t>(n);
+  chunk_remaining -= static_cast<size_t>(n);
+
+  out_chunk_offset = offset_before;
+  out_chunk_total = last_chunk_total;
+
+  if (chunk_remaining == 0) {
+    stream_line_reader lr(strm, line_buf, sizeof(line_buf));
+    if (!lr.getline()) { return -1; }
+    if (std::strcmp(lr.ptr(), "\r\n") != 0) { return -1; }
+  }
+
+  return n;
+}
+
+inline bool ChunkedDecoder::parse_trailers_into(Headers &dest,
+                                                const Headers &src_headers) {
+  stream_line_reader lr(strm, line_buf, sizeof(line_buf));
+  if (!lr.getline()) { return false; }
+  return parse_trailers(lr, dest, src_headers);
+}
+
+} // namespace detail
+
+inline void
+ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) {
+  handle.connection_->sock = socket_.sock;
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  handle.connection_->session = socket_.ssl;
+  socket_.ssl = nullptr;
+#endif
+  socket_.sock = INVALID_SOCKET;
+}
+
+inline bool ClientImpl::handle_request(Stream &strm, Request &req,
+                                       Response &res, bool close_connection,
+                                       Error &error) {
+  if (req.path.empty()) {
+    error = Error::Connection;
+    output_error_log(error, &req);
+    return false;
+  }
+
+  auto req_save = req;
+
+  bool ret;
+
+  if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
+    auto req2 = req;
+    req2.path = "http://" +
+                detail::make_host_and_port_string(host_, port_, false) +
+                req.path;
+    ret = process_request(strm, req2, res, close_connection, error);
+    req = std::move(req2);
+    req.path = req_save.path;
+  } else {
+    ret = process_request(strm, req, res, close_connection, error);
+  }
+
+  if (!ret) { return false; }
+
+  if (res.get_header_value("Connection") == "close" ||
+      (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
+    // NOTE: this requires a not-entirely-obvious chain of calls to be correct
+    // for this to be safe.
+
+    // This is safe to call because handle_request is only called by send_
+    // which locks the request mutex during the process. It would be a bug
+    // to call it from a different thread since it's a thread-safety issue
+    // to do these things to the socket if another thread is using the socket.
+    std::lock_guard<std::mutex> guard(socket_mutex_);
+    shutdown_ssl(socket_, true);
+    shutdown_socket(socket_);
+    close_socket(socket_);
+  }
+
+  if (300 < res.status && res.status < 400 && follow_location_) {
+    req = std::move(req_save);
+    ret = redirect(req, res, error);
+  }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if ((res.status == StatusCode::Unauthorized_401 ||
+       res.status == StatusCode::ProxyAuthenticationRequired_407) &&
+      req.authorization_count_ < 5) {
+    auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
+    const auto &username =
+        is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
+    const auto &password =
+        is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;
+
+    if (!username.empty() && !password.empty()) {
+      std::map<std::string, std::string> auth;
+      if (detail::parse_www_authenticate(res, auth, is_proxy)) {
+        Request new_req = req;
+        new_req.authorization_count_ += 1;
+        new_req.headers.erase(is_proxy ? "Proxy-Authorization"
+                                       : "Authorization");
+        new_req.headers.insert(detail::make_digest_authentication_header(
+            req, auth, new_req.authorization_count_, detail::random_string(10),
+            username, password, is_proxy));
+
+        Response new_res;
+
+        ret = send(new_req, new_res, error);
+        if (ret) { res = std::move(new_res); }
+      }
+    }
+  }
+#endif
+
+  return ret;
+}
+
+inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
+  if (req.redirect_count_ == 0) {
+    error = Error::ExceedRedirectCount;
+    output_error_log(error, &req);
+    return false;
+  }
+
+  auto location = res.get_header_value("location");
+  if (location.empty()) { return false; }
+
+  detail::UrlComponents uc;
+  if (!detail::parse_url(location, uc)) { return false; }
+
+  // Only follow http/https redirects
+  if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
+    return false;
+  }
+
+  auto scheme = is_ssl() ? "https" : "http";
+
+  auto next_scheme = std::move(uc.scheme);
+  auto next_host = std::move(uc.host);
+  auto port_str = std::move(uc.port);
+  auto next_path = std::move(uc.path);
+  auto next_query = std::move(uc.query);
+
+  auto next_port = port_;
+  if (!port_str.empty()) {
+    if (!detail::parse_port(port_str, next_port)) { return false; }
+  } else if (!next_scheme.empty()) {
+    next_port = next_scheme == "https" ? 443 : 80;
+  }
+
+  if (next_scheme.empty()) { next_scheme = scheme; }
+  if (next_host.empty()) { next_host = host_; }
+  if (next_path.empty()) { next_path = "/"; }
+
+  auto path = decode_path_component(next_path) + next_query;
+
+  // Same host redirect - use current client
+  if (next_scheme == scheme && next_host == host_ && next_port == port_) {
+    return detail::redirect(*this, req, res, path, location, error);
+  }
+
+  // Cross-host/scheme redirect - create new client with robust setup
+  return create_redirect_client(next_scheme, next_host, next_port, req, res,
+                                path, location, error);
+}
+
+// New method for robust redirect client creation
+inline bool ClientImpl::create_redirect_client(
+    const std::string &scheme, const std::string &host, int port, Request &req,
+    Response &res, const std::string &path, const std::string &location,
+    Error &error) {
+  // Determine if we need SSL
+  auto need_ssl = (scheme == "https");
+
+  // Clean up request headers that are host/client specific
+  // Remove headers that should not be carried over to new host
+  auto headers_to_remove =
+      std::vector<std::string>{"Host", "Proxy-Authorization", "Authorization"};
+
+  for (const auto &header_name : headers_to_remove) {
+    auto it = req.headers.find(header_name);
+    while (it != req.headers.end()) {
+      it = req.headers.erase(it);
+      it = req.headers.find(header_name);
+    }
+  }
+
+  // Create appropriate client type and handle redirect
+  if (need_ssl) {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    // Create SSL client for HTTPS redirect
+    SSLClient redirect_client(host, port);
+
+    // Setup basic client configuration first
+    setup_redirect_client(redirect_client);
+
+    redirect_client.enable_server_certificate_verification(
+        server_certificate_verification_);
+    redirect_client.enable_server_hostname_verification(
+        server_hostname_verification_);
+
+    // Transfer CA certificate to redirect client
+    if (!ca_cert_pem_.empty()) {
+      redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(),
+                                         ca_cert_pem_.size());
+    }
+    if (!ca_cert_file_path_.empty()) {
+      redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_);
+    }
+
+    // Client certificates are set through constructor for SSLClient
+    // NOTE: SSLClient constructor already takes client_cert_path and
+    // client_key_path so we need to create it properly if client certs are
+    // needed
+
+    // Execute the redirect
+    return detail::redirect(redirect_client, req, res, path, location, error);
+#else
+    // SSL not supported - set appropriate error
+    error = Error::SSLConnection;
+    output_error_log(error, &req);
+    return false;
+#endif
+  } else {
+    // HTTP redirect
+    ClientImpl redirect_client(host, port);
+
+    // Setup client with robust configuration
+    setup_redirect_client(redirect_client);
+
+    // Execute the redirect
+    return detail::redirect(redirect_client, req, res, path, location, error);
+  }
+}
+
+// New method for robust client setup (based on basic_manual_redirect.cpp
+// logic)
+template <typename ClientType>
+inline void ClientImpl::setup_redirect_client(ClientType &client) {
+  // Copy basic settings first
+  client.set_connection_timeout(connection_timeout_sec_);
+  client.set_read_timeout(read_timeout_sec_, read_timeout_usec_);
+  client.set_write_timeout(write_timeout_sec_, write_timeout_usec_);
+  client.set_keep_alive(keep_alive_);
+  client.set_follow_location(
+      true); // Enable redirects to handle multi-step redirects
+  client.set_path_encode(path_encode_);
+  client.set_compress(compress_);
+  client.set_decompress(decompress_);
+
+  // NOTE: Authentication credentials (basic auth, bearer token, digest auth)
+  // are intentionally NOT copied to the redirect client. Per RFC 9110 Section
+  // 15.4, credentials must not be forwarded when redirecting to a different
+  // host. This function is only called for cross-host redirects; same-host
+  // redirects are handled directly in ClientImpl::redirect().
+
+  // Setup proxy configuration (CRITICAL ORDER - proxy must be set
+  // before proxy auth)
+  if (!proxy_host_.empty() && proxy_port_ != -1) {
+    // First set proxy host and port
+    client.set_proxy(proxy_host_, proxy_port_);
+
+    // Then set proxy authentication (order matters!)
+    if (!proxy_basic_auth_username_.empty()) {
+      client.set_proxy_basic_auth(proxy_basic_auth_username_,
+                                  proxy_basic_auth_password_);
+    }
+    if (!proxy_bearer_token_auth_token_.empty()) {
+      client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_);
+    }
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    if (!proxy_digest_auth_username_.empty()) {
+      client.set_proxy_digest_auth(proxy_digest_auth_username_,
+                                   proxy_digest_auth_password_);
+    }
+#endif
+  }
+
+  // Copy network and socket settings
+  client.set_address_family(address_family_);
+  client.set_tcp_nodelay(tcp_nodelay_);
+  client.set_ipv6_v6only(ipv6_v6only_);
+  if (socket_options_) { client.set_socket_options(socket_options_); }
+  if (!interface_.empty()) { client.set_interface(interface_); }
+
+  // Copy logging and headers
+  if (logger_) { client.set_logger(logger_); }
+  if (error_logger_) { client.set_error_logger(error_logger_); }
+
+  // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers
+  // Each new client should generate its own headers based on its target host
+}
+
+inline bool ClientImpl::write_content_with_provider(Stream &strm,
+                                                    const Request &req,
+                                                    Error &error) const {
+  auto is_shutting_down = []() { return false; };
+
+  if (req.is_chunked_content_provider_) {
+    auto compressor = compress_ ? detail::create_compressor().first
+                                : std::unique_ptr<detail::compressor>();
+    if (!compressor) {
+      compressor = detail::make_unique<detail::nocompressor>();
+    }
+
+    return detail::write_content_chunked(strm, req.content_provider_,
+                                         is_shutting_down, *compressor, error);
+  } else {
+    return detail::write_content_with_progress(
+        strm, req.content_provider_, 0, req.content_length_, is_shutting_down,
+        req.upload_progress, error);
+  }
+}
+
+inline bool ClientImpl::write_request(Stream &strm, Request &req,
+                                      bool close_connection, Error &error,
+                                      bool skip_body) {
+  // Prepare additional headers
+  if (close_connection) {
+    if (!req.has_header("Connection")) {
+      req.set_header("Connection", "close");
+    }
+  }
+
+  std::string ct_for_defaults;
+  if (!req.has_header("Content-Type") && !req.body.empty()) {
+    ct_for_defaults = "text/plain";
+  }
+  prepare_default_headers(req, false, ct_for_defaults);
+
+  if (req.body.empty()) {
+    if (req.content_provider_) {
+      if (!req.is_chunked_content_provider_) {
+        if (!req.has_header("Content-Length")) {
+          auto length = std::to_string(req.content_length_);
+          req.set_header("Content-Length", length);
+        }
+      }
+    } else {
+      if (req.method == "POST" || req.method == "PUT" ||
+          req.method == "PATCH") {
+        req.set_header("Content-Length", "0");
+      }
+    }
+  }
+
+  if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
+    if (!req.has_header("Authorization")) {
+      req.headers.insert(make_basic_authentication_header(
+          basic_auth_username_, basic_auth_password_, false));
+    }
+  }
+
+  if (!proxy_basic_auth_username_.empty() &&
+      !proxy_basic_auth_password_.empty()) {
+    if (!req.has_header("Proxy-Authorization")) {
+      req.headers.insert(make_basic_authentication_header(
+          proxy_basic_auth_username_, proxy_basic_auth_password_, true));
+    }
+  }
+
+  if (!bearer_token_auth_token_.empty()) {
+    if (!req.has_header("Authorization")) {
+      req.headers.insert(make_bearer_token_authentication_header(
+          bearer_token_auth_token_, false));
+    }
+  }
+
+  if (!proxy_bearer_token_auth_token_.empty()) {
+    if (!req.has_header("Proxy-Authorization")) {
+      req.headers.insert(make_bearer_token_authentication_header(
+          proxy_bearer_token_auth_token_, true));
+    }
+  }
+
+  // Request line and headers
+  {
+    detail::BufferStream bstrm;
+
+    // Extract path and query from req.path
+    std::string path_part, query_part;
+    auto query_pos = req.path.find('?');
+    if (query_pos != std::string::npos) {
+      path_part = req.path.substr(0, query_pos);
+      query_part = req.path.substr(query_pos + 1);
+    } else {
+      path_part = req.path;
+      query_part = "";
+    }
+
+    // Encode path part. If the original `req.path` already contained a
+    // query component, preserve its raw query string (including parameter
+    // order) instead of reparsing and reassembling it which may reorder
+    // parameters due to container ordering (e.g. `Params` uses
+    // `std::multimap`). When there is no query in `req.path`, fall back to
+    // building a query from `req.params` so existing callers that pass
+    // `Params` continue to work.
+    auto path_with_query =
+        path_encode_ ? detail::encode_path(path_part) : path_part;
+
+    if (!query_part.empty()) {
+      // Normalize the query string (decode then re-encode) while preserving
+      // the original parameter order.
+      auto normalized = detail::normalize_query_string(query_part);
+      if (!normalized.empty()) { path_with_query += '?' + normalized; }
+
+      // Still populate req.params for handlers/users who read them.
+      detail::parse_query_text(query_part, req.params);
+    } else {
+      // No query in path; parse any query_part (empty) and append params
+      // from `req.params` when present (preserves prior behavior for
+      // callers who provide Params separately).
+      detail::parse_query_text(query_part, req.params);
+      if (!req.params.empty()) {
+        path_with_query = append_query_params(path_with_query, req.params);
+      }
+    }
+
+    // Write request line and headers
+    detail::write_request_line(bstrm, req.method, path_with_query);
+    if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
+                                         error)) {
+      output_error_log(error, &req);
+      return false;
+    }
+
+    // Flush buffer
+    auto &data = bstrm.get_buffer();
+    if (!detail::write_data(strm, data.data(), data.size())) {
+      error = Error::Write;
+      output_error_log(error, &req);
+      return false;
+    }
+  }
+
+  // After sending request line and headers, wait briefly for an early server
+  // response (e.g. 4xx) and avoid sending a potentially large request body
+  // unnecessarily. This workaround is only enabled on Windows because Unix
+  // platforms surface write errors (EPIPE) earlier; on Windows kernel send
+  // buffering can accept large writes even when the peer already responded.
+  // Check the stream first (which covers SSL via `is_readable()`), then
+  // fall back to select on the socket. Only perform the wait for very large
+  // request bodies to avoid interfering with normal small requests and
+  // reduce side-effects. Poll briefly (up to 50ms as default) for an early
+  // response. Skip this check when using Expect: 100-continue, as the protocol
+  // handles early responses properly.
+#if defined(_WIN32)
+  if (!skip_body &&
+      req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD &&
+      req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
+    auto start = std::chrono::high_resolution_clock::now();
+
+    for (;;) {
+      // Prefer socket-level readiness to avoid SSL_pending() false-positives
+      // from SSL internals. If the underlying socket is readable, assume an
+      // early response may be present.
+      auto sock = strm.socket();
+      if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) {
+        return false;
+      }
+
+      // Fallback to stream-level check for non-socket streams or when the
+      // socket isn't reporting readable. Avoid using `is_readable()` for
+      // SSL, since `SSL_pending()` may report buffered records that do not
+      // indicate a complete application-level response yet.
+      if (!is_ssl() && strm.is_readable()) { return false; }
+
+      auto now = std::chrono::high_resolution_clock::now();
+      auto elapsed =
+          std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
+              .count();
+      if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {
+        break;
+      }
+
+      std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+  }
+#endif
+
+  // Body
+  if (skip_body) { return true; }
+
+  return write_request_body(strm, req, error);
+}
+
+inline bool ClientImpl::write_request_body(Stream &strm, Request &req,
+                                           Error &error) {
+  if (req.body.empty()) {
+    return write_content_with_provider(strm, req, error);
+  }
+
+  if (req.upload_progress) {
+    auto body_size = req.body.size();
+    size_t written = 0;
+    auto data = req.body.data();
+
+    while (written < body_size) {
+      size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written);
+      if (!detail::write_data(strm, data + written, to_write)) {
+        error = Error::Write;
+        output_error_log(error, &req);
+        return false;
+      }
+      written += to_write;
+
+      if (!req.upload_progress(written, body_size)) {
+        error = Error::Canceled;
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+  } else {
+    if (!detail::write_data(strm, req.body.data(), req.body.size())) {
+      error = Error::Write;
+      output_error_log(error, &req);
+      return false;
+    }
+  }
+
+  return true;
+}
+
+inline std::unique_ptr<Response>
+ClientImpl::send_with_content_provider_and_receiver(
+    Request &req, const char *body, size_t content_length,
+    ContentProvider content_provider,
+    ContentProviderWithoutLength content_provider_without_length,
+    const std::string &content_type, ContentReceiver content_receiver,
+    Error &error) {
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+
+  auto enc = compress_
+                 ? detail::create_compressor()
+                 : std::pair<std::unique_ptr<detail::compressor>, const char *>(
+                       nullptr, nullptr);
+
+  if (enc.second) { req.set_header("Content-Encoding", enc.second); }
+
+  if (enc.first && !content_provider_without_length) {
+    auto &compressor = enc.first;
+
+    if (content_provider) {
+      auto ok = true;
+      size_t offset = 0;
+      DataSink data_sink;
+
+      data_sink.write = [&](const char *data, size_t data_len) -> bool {
+        if (ok) {
+          auto last = offset + data_len == content_length;
+
+          auto ret = compressor->compress(
+              data, data_len, last,
+              [&](const char *compressed_data, size_t compressed_data_len) {
+                req.body.append(compressed_data, compressed_data_len);
+                return true;
+              });
+
+          if (ret) {
+            offset += data_len;
+          } else {
+            ok = false;
+          }
+        }
+        return ok;
+      };
+
+      while (ok && offset < content_length) {
+        if (!content_provider(offset, content_length - offset, data_sink)) {
+          error = Error::Canceled;
+          output_error_log(error, &req);
+          return nullptr;
+        }
+      }
+    } else {
+      if (!compressor->compress(body, content_length, true,
+                                [&](const char *data, size_t data_len) {
+                                  req.body.append(data, data_len);
+                                  return true;
+                                })) {
+        error = Error::Compression;
+        output_error_log(error, &req);
+        return nullptr;
+      }
+    }
+  } else {
+    if (content_provider) {
+      req.content_length_ = content_length;
+      req.content_provider_ = std::move(content_provider);
+      req.is_chunked_content_provider_ = false;
+    } else if (content_provider_without_length) {
+      req.content_length_ = 0;
+      req.content_provider_ = detail::ContentProviderAdapter(
+          std::move(content_provider_without_length));
+      req.is_chunked_content_provider_ = true;
+      req.set_header("Transfer-Encoding", "chunked");
+    } else {
+      req.body.assign(body, content_length);
+    }
+  }
+
+  if (content_receiver) {
+    req.content_receiver =
+        [content_receiver](const char *data, size_t data_length,
+                           size_t /*offset*/, size_t /*total_length*/) {
+          return content_receiver(data, data_length);
+        };
+  }
+
+  auto res = detail::make_unique<Response>();
+  return send(req, *res, error) ? std::move(res) : nullptr;
+}
+
+inline Result ClientImpl::send_with_content_provider_and_receiver(
+    const std::string &method, const std::string &path, const Headers &headers,
+    const char *body, size_t content_length, ContentProvider content_provider,
+    ContentProviderWithoutLength content_provider_without_length,
+    const std::string &content_type, ContentReceiver content_receiver,
+    UploadProgress progress) {
+  Request req;
+  req.method = method;
+  req.headers = headers;
+  req.path = path;
+  req.upload_progress = std::move(progress);
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  auto error = Error::Success;
+
+  auto res = send_with_content_provider_and_receiver(
+      req, body, content_length, std::move(content_provider),
+      std::move(content_provider_without_length), content_type,
+      std::move(content_receiver), error);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
+                last_backend_error_};
+#else
+  return Result{std::move(res), error, std::move(req.headers)};
+#endif
+}
+
+inline void ClientImpl::output_log(const Request &req,
+                                   const Response &res) const {
+  if (logger_) {
+    std::lock_guard<std::mutex> guard(logger_mutex_);
+    logger_(req, res);
+  }
+}
+
+inline void ClientImpl::output_error_log(const Error &err,
+                                         const Request *req) const {
+  if (error_logger_) {
+    std::lock_guard<std::mutex> guard(logger_mutex_);
+    error_logger_(err, req);
+  }
+}
+
+inline bool ClientImpl::process_request(Stream &strm, Request &req,
+                                        Response &res, bool close_connection,
+                                        Error &error) {
+  // Auto-add Expect: 100-continue for large bodies
+  if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) {
+    auto body_size = req.body.empty() ? req.content_length_ : req.body.size();
+    if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) {
+      req.set_header("Expect", "100-continue");
+    }
+  }
+
+  // Check for Expect: 100-continue
+  auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
+
+  // Send request (skip body if using Expect: 100-continue)
+  auto write_request_success =
+      write_request(strm, req, close_connection, error, expect_100_continue);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if (is_ssl() && !expect_100_continue) {
+    auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;
+    if (!is_proxy_enabled) {
+      if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+        error = Error::SSLPeerCouldBeClosed_;
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+  }
+#endif
+
+  // Handle Expect: 100-continue with timeout
+  if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {
+    time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;
+    time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;
+    auto ret = detail::select_read(strm.socket(), sec, usec);
+    if (ret <= 0) {
+      // Timeout or error: send body anyway (server didn't respond in time)
+      if (!write_request_body(strm, req, error)) { return false; }
+      expect_100_continue = false; // Switch to normal response handling
+    }
+  }
+
+  // Receive response and headers
+  // When using Expect: 100-continue, don't auto-skip `100 Continue` response
+  if (!read_response_line(strm, req, res, !expect_100_continue) ||
+      !detail::read_headers(strm, res.headers)) {
+    if (write_request_success) { error = Error::Read; }
+    output_error_log(error, &req);
+    return false;
+  }
+
+  if (!write_request_success) { return false; }
+
+  // Handle Expect: 100-continue response
+  if (expect_100_continue) {
+    if (res.status == StatusCode::Continue_100) {
+      // Server accepted, send the body
+      if (!write_request_body(strm, req, error)) { return false; }
+
+      // Read the actual response
+      res.headers.clear();
+      res.body.clear();
+      if (!read_response_line(strm, req, res) ||
+          !detail::read_headers(strm, res.headers)) {
+        error = Error::Read;
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+    // If not 100 Continue, server returned an error; proceed with that response
+  }
+
+  // Body
+  if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
+      req.method != "CONNECT") {
+    auto redirect = 300 < res.status && res.status < 400 &&
+                    res.status != StatusCode::NotModified_304 &&
+                    follow_location_;
+
+    if (req.response_handler && !redirect) {
+      if (!req.response_handler(res)) {
+        error = Error::Canceled;
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+
+    auto out =
+        req.content_receiver
+            ? static_cast<ContentReceiverWithProgress>(
+                  [&](const char *buf, size_t n, size_t off, size_t len) {
+                    if (redirect) { return true; }
+                    auto ret = req.content_receiver(buf, n, off, len);
+                    if (!ret) {
+                      error = Error::Canceled;
+                      output_error_log(error, &req);
+                    }
+                    return ret;
+                  })
+            : static_cast<ContentReceiverWithProgress>(
+                  [&](const char *buf, size_t n, size_t /*off*/,
+                      size_t /*len*/) {
+                    assert(res.body.size() + n <= res.body.max_size());
+                    if (payload_max_length_ > 0 &&
+                        (res.body.size() >= payload_max_length_ ||
+                         n > payload_max_length_ - res.body.size())) {
+                      return false;
+                    }
+                    res.body.append(buf, n);
+                    return true;
+                  });
+
+    auto progress = [&](size_t current, size_t total) {
+      if (!req.download_progress || redirect) { return true; }
+      auto ret = req.download_progress(current, total);
+      if (!ret) {
+        error = Error::Canceled;
+        output_error_log(error, &req);
+      }
+      return ret;
+    };
+
+    if (res.has_header("Content-Length")) {
+      if (!req.content_receiver) {
+        auto len = res.get_header_value_u64("Content-Length");
+        if (len > res.body.max_size()) {
+          error = Error::Read;
+          output_error_log(error, &req);
+          return false;
+        }
+        res.body.reserve(static_cast<size_t>(len));
+      }
+    }
+
+    if (res.status != StatusCode::NotModified_304) {
+      int dummy_status;
+      auto max_length = (!has_payload_max_length_ && req.content_receiver)
+                            ? (std::numeric_limits<size_t>::max)()
+                            : payload_max_length_;
+      if (!detail::read_content(strm, res, max_length, dummy_status,
+                                std::move(progress), std::move(out),
+                                decompress_)) {
+        if (error != Error::Canceled) { error = Error::Read; }
+        output_error_log(error, &req);
+        return false;
+      }
+    }
+  }
+
+  // Log
+  output_log(req, res);
+
+  return true;
+}
+
+inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
+    const std::string &boundary, const UploadFormDataItems &items,
+    const FormDataProviderItems &provider_items) const {
+  size_t cur_item = 0;
+  size_t cur_start = 0;
+  // cur_item and cur_start are copied to within the std::function and
+  // maintain state between successive calls
+  return [&, cur_item, cur_start](size_t offset,
+                                  DataSink &sink) mutable -> bool {
+    if (!offset && !items.empty()) {
+      sink.os << detail::serialize_multipart_formdata(items, boundary, false);
+      return true;
+    } else if (cur_item < provider_items.size()) {
+      if (!cur_start) {
+        const auto &begin = detail::serialize_multipart_formdata_item_begin(
+            provider_items[cur_item], boundary);
+        offset += begin.size();
+        cur_start = offset;
+        sink.os << begin;
+      }
+
+      DataSink cur_sink;
+      auto has_data = true;
+      cur_sink.write = sink.write;
+      cur_sink.done = [&]() { has_data = false; };
+
+      if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) {
+        return false;
+      }
+
+      if (!has_data) {
+        sink.os << detail::serialize_multipart_formdata_item_end();
+        cur_item++;
+        cur_start = 0;
+      }
+      return true;
+    } else {
+      sink.os << detail::serialize_multipart_formdata_finish(boundary);
+      sink.done();
+      return true;
+    }
+  };
+}
+
+inline bool ClientImpl::process_socket(
+    const Socket &socket,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    std::function<bool(Stream &strm)> callback) {
+  return detail::process_client_socket(
+      socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback));
+}
+
+inline bool ClientImpl::is_ssl() const { return false; }
+
+inline Result ClientImpl::Get(const std::string &path,
+                              DownloadProgress progress) {
+  return Get(path, Headers(), std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Params &params,
+                              const Headers &headers,
+                              DownloadProgress progress) {
+  if (params.empty()) { return Get(path, headers); }
+
+  std::string path_with_query = append_query_params(path, params);
+  return Get(path_with_query, headers, std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Headers &headers,
+                              DownloadProgress progress) {
+  Request req;
+  req.method = "GET";
+  req.path = path;
+  req.headers = headers;
+  req.download_progress = std::move(progress);
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Get(const std::string &path,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  return Get(path, Headers(), nullptr, std::move(content_receiver),
+             std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Headers &headers,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  return Get(path, headers, nullptr, std::move(content_receiver),
+             std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  return Get(path, Headers(), std::move(response_handler),
+             std::move(content_receiver), std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Headers &headers,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  Request req;
+  req.method = "GET";
+  req.path = path;
+  req.headers = headers;
+  req.response_handler = std::move(response_handler);
+  req.content_receiver =
+      [content_receiver](const char *data, size_t data_length,
+                         size_t /*offset*/, size_t /*total_length*/) {
+        return content_receiver(data, data_length);
+      };
+  req.download_progress = std::move(progress);
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Params &params,
+                              const Headers &headers,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  return Get(path, params, headers, nullptr, std::move(content_receiver),
+             std::move(progress));
+}
+
+inline Result ClientImpl::Get(const std::string &path, const Params &params,
+                              const Headers &headers,
+                              ResponseHandler response_handler,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  if (params.empty()) {
+    return Get(path, headers, std::move(response_handler),
+               std::move(content_receiver), std::move(progress));
+  }
+
+  std::string path_with_query = append_query_params(path, params);
+  return Get(path_with_query, headers, std::move(response_handler),
+             std::move(content_receiver), std::move(progress));
+}
+
+inline Result ClientImpl::Head(const std::string &path) {
+  return Head(path, Headers());
+}
+
+inline Result ClientImpl::Head(const std::string &path,
+                               const Headers &headers) {
+  Request req;
+  req.method = "HEAD";
+  req.headers = headers;
+  req.path = path;
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Post(const std::string &path) {
+  return Post(path, std::string(), std::string());
+}
+
+inline Result ClientImpl::Post(const std::string &path,
+                               const Headers &headers) {
+  return Post(path, headers, nullptr, 0, std::string());
+}
+
+inline Result ClientImpl::Post(const std::string &path, const char *body,
+                               size_t content_length,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return Post(path, Headers(), body, content_length, content_type, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const std::string &body,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return Post(path, Headers(), body, content_type, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Params &params) {
+  return Post(path, Headers(), params);
+}
+
+inline Result ClientImpl::Post(const std::string &path, size_t content_length,
+                               ContentProvider content_provider,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return Post(path, Headers(), content_length, std::move(content_provider),
+              content_type, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, size_t content_length,
+                               ContentProvider content_provider,
+                               const std::string &content_type,
+                               ContentReceiver content_receiver,
+                               UploadProgress progress) {
+  return Post(path, Headers(), content_length, std::move(content_provider),
+              content_type, std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path,
+                               ContentProviderWithoutLength content_provider,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return Post(path, Headers(), std::move(content_provider), content_type,
+              progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path,
+                               ContentProviderWithoutLength content_provider,
+                               const std::string &content_type,
+                               ContentReceiver content_receiver,
+                               UploadProgress progress) {
+  return Post(path, Headers(), std::move(content_provider), content_type,
+              std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const Params &params) {
+  auto query = detail::params_to_query_str(params);
+  return Post(path, headers, query, "application/x-www-form-urlencoded");
+}
+
+inline Result ClientImpl::Post(const std::string &path,
+                               const UploadFormDataItems &items,
+                               UploadProgress progress) {
+  return Post(path, Headers(), items, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const UploadFormDataItems &items,
+                               UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Post(path, headers, content_length,
+              detail::make_multipart_content_provider(items, boundary),
+              content_type, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const UploadFormDataItems &items,
+                               const std::string &boundary,
+                               UploadProgress progress) {
+  if (!detail::is_multipart_boundary_chars_valid(boundary)) {
+    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
+  }
+
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Post(path, headers, content_length,
+              detail::make_multipart_content_provider(items, boundary),
+              content_type, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const char *body, size_t content_length,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, body, content_length, nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const std::string &body,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, body.data(), body.size(), nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               size_t content_length,
+                               ContentProvider content_provider,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               size_t content_length,
+                               ContentProvider content_provider,
+                               const std::string &content_type,
+                               ContentReceiver content_receiver,
+                               DownloadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type,
+      std::move(content_receiver), std::move(progress));
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               ContentProviderWithoutLength content_provider,
+                               const std::string &content_type,
+                               UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               ContentProviderWithoutLength content_provider,
+                               const std::string &content_type,
+                               ContentReceiver content_receiver,
+                               DownloadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, std::move(content_receiver), std::move(progress));
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const UploadFormDataItems &items,
+                               const FormDataProviderItems &provider_items,
+                               UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  return send_with_content_provider_and_receiver(
+      "POST", path, headers, nullptr, 0, nullptr,
+      get_multipart_content_provider(boundary, items, provider_items),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Post(const std::string &path, const Headers &headers,
+                               const std::string &body,
+                               const std::string &content_type,
+                               ContentReceiver content_receiver,
+                               DownloadProgress progress) {
+  Request req;
+  req.method = "POST";
+  req.path = path;
+  req.headers = headers;
+  req.body = body;
+  req.content_receiver =
+      [content_receiver](const char *data, size_t data_length,
+                         size_t /*offset*/, size_t /*total_length*/) {
+        return content_receiver(data, data_length);
+      };
+  req.download_progress = std::move(progress);
+
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Put(const std::string &path) {
+  return Put(path, std::string(), std::string());
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers) {
+  return Put(path, headers, nullptr, 0, std::string());
+}
+
+inline Result ClientImpl::Put(const std::string &path, const char *body,
+                              size_t content_length,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return Put(path, Headers(), body, content_length, content_type, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const std::string &body,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return Put(path, Headers(), body, content_type, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Params &params) {
+  return Put(path, Headers(), params);
+}
+
+inline Result ClientImpl::Put(const std::string &path, size_t content_length,
+                              ContentProvider content_provider,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return Put(path, Headers(), content_length, std::move(content_provider),
+             content_type, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, size_t content_length,
+                              ContentProvider content_provider,
+                              const std::string &content_type,
+                              ContentReceiver content_receiver,
+                              UploadProgress progress) {
+  return Put(path, Headers(), content_length, std::move(content_provider),
+             content_type, std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path,
+                              ContentProviderWithoutLength content_provider,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return Put(path, Headers(), std::move(content_provider), content_type,
+             progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path,
+                              ContentProviderWithoutLength content_provider,
+                              const std::string &content_type,
+                              ContentReceiver content_receiver,
+                              UploadProgress progress) {
+  return Put(path, Headers(), std::move(content_provider), content_type,
+             std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const Params &params) {
+  auto query = detail::params_to_query_str(params);
+  return Put(path, headers, query, "application/x-www-form-urlencoded");
+}
+
+inline Result ClientImpl::Put(const std::string &path,
+                              const UploadFormDataItems &items,
+                              UploadProgress progress) {
+  return Put(path, Headers(), items, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const UploadFormDataItems &items,
+                              UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Put(path, headers, content_length,
+             detail::make_multipart_content_provider(items, boundary),
+             content_type, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const UploadFormDataItems &items,
+                              const std::string &boundary,
+                              UploadProgress progress) {
+  if (!detail::is_multipart_boundary_chars_valid(boundary)) {
+    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
+  }
+
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Put(path, headers, content_length,
+             detail::make_multipart_content_provider(items, boundary),
+             content_type, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const char *body, size_t content_length,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, body, content_length, nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const std::string &body,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, body.data(), body.size(), nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              size_t content_length,
+                              ContentProvider content_provider,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              size_t content_length,
+                              ContentProvider content_provider,
+                              const std::string &content_type,
+                              ContentReceiver content_receiver,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type,
+      std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              ContentProviderWithoutLength content_provider,
+                              const std::string &content_type,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              ContentProviderWithoutLength content_provider,
+                              const std::string &content_type,
+                              ContentReceiver content_receiver,
+                              UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const UploadFormDataItems &items,
+                              const FormDataProviderItems &provider_items,
+                              UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  return send_with_content_provider_and_receiver(
+      "PUT", path, headers, nullptr, 0, nullptr,
+      get_multipart_content_provider(boundary, items, provider_items),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Put(const std::string &path, const Headers &headers,
+                              const std::string &body,
+                              const std::string &content_type,
+                              ContentReceiver content_receiver,
+                              DownloadProgress progress) {
+  Request req;
+  req.method = "PUT";
+  req.path = path;
+  req.headers = headers;
+  req.body = body;
+  req.content_receiver =
+      [content_receiver](const char *data, size_t data_length,
+                         size_t /*offset*/, size_t /*total_length*/) {
+        return content_receiver(data, data_length);
+      };
+  req.download_progress = std::move(progress);
+
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Patch(const std::string &path) {
+  return Patch(path, std::string(), std::string());
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                UploadProgress progress) {
+  return Patch(path, headers, nullptr, 0, std::string(), progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const char *body,
+                                size_t content_length,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), body, content_length, content_type, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path,
+                                const std::string &body,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), body, content_type, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Params &params) {
+  return Patch(path, Headers(), params);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, size_t content_length,
+                                ContentProvider content_provider,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), content_length, std::move(content_provider),
+               content_type, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, size_t content_length,
+                                ContentProvider content_provider,
+                                const std::string &content_type,
+                                ContentReceiver content_receiver,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), content_length, std::move(content_provider),
+               content_type, std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path,
+                                ContentProviderWithoutLength content_provider,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), std::move(content_provider), content_type,
+               progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path,
+                                ContentProviderWithoutLength content_provider,
+                                const std::string &content_type,
+                                ContentReceiver content_receiver,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), std::move(content_provider), content_type,
+               std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const Params &params) {
+  auto query = detail::params_to_query_str(params);
+  return Patch(path, headers, query, "application/x-www-form-urlencoded");
+}
+
+inline Result ClientImpl::Patch(const std::string &path,
+                                const UploadFormDataItems &items,
+                                UploadProgress progress) {
+  return Patch(path, Headers(), items, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const UploadFormDataItems &items,
+                                UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Patch(path, headers, content_length,
+               detail::make_multipart_content_provider(items, boundary),
+               content_type, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const UploadFormDataItems &items,
+                                const std::string &boundary,
+                                UploadProgress progress) {
+  if (!detail::is_multipart_boundary_chars_valid(boundary)) {
+    return Result{nullptr, Error::UnsupportedMultipartBoundaryChars};
+  }
+
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  auto content_length = detail::get_multipart_content_length(items, boundary);
+  return Patch(path, headers, content_length,
+               detail::make_multipart_content_provider(items, boundary),
+               content_type, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const char *body, size_t content_length,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, body, content_length, nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const std::string &body,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, body.data(), body.size(), nullptr, nullptr,
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                size_t content_length,
+                                ContentProvider content_provider,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                size_t content_length,
+                                ContentProvider content_provider,
+                                const std::string &content_type,
+                                ContentReceiver content_receiver,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, nullptr, content_length,
+      std::move(content_provider), nullptr, content_type,
+      std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                ContentProviderWithoutLength content_provider,
+                                const std::string &content_type,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                ContentProviderWithoutLength content_provider,
+                                const std::string &content_type,
+                                ContentReceiver content_receiver,
+                                UploadProgress progress) {
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider),
+      content_type, std::move(content_receiver), progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const UploadFormDataItems &items,
+                                const FormDataProviderItems &provider_items,
+                                UploadProgress progress) {
+  const auto &boundary = detail::make_multipart_data_boundary();
+  const auto &content_type =
+      detail::serialize_multipart_formdata_get_content_type(boundary);
+  return send_with_content_provider_and_receiver(
+      "PATCH", path, headers, nullptr, 0, nullptr,
+      get_multipart_content_provider(boundary, items, provider_items),
+      content_type, nullptr, progress);
+}
+
+inline Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+                                const std::string &body,
+                                const std::string &content_type,
+                                ContentReceiver content_receiver,
+                                DownloadProgress progress) {
+  Request req;
+  req.method = "PATCH";
+  req.path = path;
+  req.headers = headers;
+  req.body = body;
+  req.content_receiver =
+      [content_receiver](const char *data, size_t data_length,
+                         size_t /*offset*/, size_t /*total_length*/) {
+        return content_receiver(data, data_length);
+      };
+  req.download_progress = std::move(progress);
+
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 DownloadProgress progress) {
+  return Delete(path, Headers(), std::string(), std::string(), progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 const Headers &headers,
+                                 DownloadProgress progress) {
+  return Delete(path, headers, std::string(), std::string(), progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path, const char *body,
+                                 size_t content_length,
+                                 const std::string &content_type,
+                                 DownloadProgress progress) {
+  return Delete(path, Headers(), body, content_length, content_type, progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 const std::string &body,
+                                 const std::string &content_type,
+                                 DownloadProgress progress) {
+  return Delete(path, Headers(), body.data(), body.size(), content_type,
+                progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 const Headers &headers,
+                                 const std::string &body,
+                                 const std::string &content_type,
+                                 DownloadProgress progress) {
+  return Delete(path, headers, body.data(), body.size(), content_type,
+                progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path, const Params &params,
+                                 DownloadProgress progress) {
+  return Delete(path, Headers(), params, progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 const Headers &headers, const Params &params,
+                                 DownloadProgress progress) {
+  auto query = detail::params_to_query_str(params);
+  return Delete(path, headers, query, "application/x-www-form-urlencoded",
+                progress);
+}
+
+inline Result ClientImpl::Delete(const std::string &path,
+                                 const Headers &headers, const char *body,
+                                 size_t content_length,
+                                 const std::string &content_type,
+                                 DownloadProgress progress) {
+  Request req;
+  req.method = "DELETE";
+  req.headers = headers;
+  req.path = path;
+  req.download_progress = std::move(progress);
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+  req.body.assign(body, content_length);
+
+  return send_(std::move(req));
+}
+
+inline Result ClientImpl::Options(const std::string &path) {
+  return Options(path, Headers());
+}
+
+inline Result ClientImpl::Options(const std::string &path,
+                                  const Headers &headers) {
+  Request req;
+  req.method = "OPTIONS";
+  req.headers = headers;
+  req.path = path;
+  if (max_timeout_msec_ > 0) {
+    req.start_time_ = std::chrono::steady_clock::now();
+  }
+
+  return send_(std::move(req));
+}
+
+inline void ClientImpl::stop() {
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+
+  // If there is anything ongoing right now, the ONLY thread-safe thing we can
+  // do is to shutdown_socket, so that threads using this socket suddenly
+  // discover they can't read/write any more and error out. Everything else
+  // (closing the socket, shutting ssl down) is unsafe because these actions
+  // are not thread-safe.
+  if (socket_requests_in_flight_ > 0) {
+    shutdown_socket(socket_);
+
+    // Aside from that, we set a flag for the socket to be closed when we're
+    // done.
+    socket_should_be_closed_when_request_is_done_ = true;
+    return;
+  }
+
+  // Otherwise, still holding the mutex, we can shut everything down ourselves
+  shutdown_ssl(socket_, true);
+  shutdown_socket(socket_);
+  close_socket(socket_);
+}
+
+inline std::string ClientImpl::host() const { return host_; }
+
+inline int ClientImpl::port() const { return port_; }
+
+inline size_t ClientImpl::is_socket_open() const {
+  std::lock_guard<std::mutex> guard(socket_mutex_);
+  return socket_.is_open();
+}
+
+inline socket_t ClientImpl::socket() const { return socket_.sock; }
+
+inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) {
+  connection_timeout_sec_ = sec;
+  connection_timeout_usec_ = usec;
+}
+
+inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+}
+
+inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) {
+  write_timeout_sec_ = sec;
+  write_timeout_usec_ = usec;
+}
+
+inline void ClientImpl::set_max_timeout(time_t msec) {
+  max_timeout_msec_ = msec;
+}
+
+inline void ClientImpl::set_basic_auth(const std::string &username,
+                                       const std::string &password) {
+  basic_auth_username_ = username;
+  basic_auth_password_ = password;
+}
+
+inline void ClientImpl::set_bearer_token_auth(const std::string &token) {
+  bearer_token_auth_token_ = token;
+}
+
+inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; }
+
+inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; }
+
+inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
+
+inline void
+ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
+  addr_map_ = std::move(addr_map);
+}
+
+inline void ClientImpl::set_default_headers(Headers headers) {
+  default_headers_ = std::move(headers);
+}
+
+inline void ClientImpl::set_header_writer(
+    std::function<ssize_t(Stream &, Headers &)> const &writer) {
+  header_writer_ = writer;
+}
+
+inline void ClientImpl::set_address_family(int family) {
+  address_family_ = family;
+}
+
+inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
+
+inline void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; }
+
+inline void ClientImpl::set_socket_options(SocketOptions socket_options) {
+  socket_options_ = std::move(socket_options);
+}
+
+inline void ClientImpl::set_compress(bool on) { compress_ = on; }
+
+inline void ClientImpl::set_decompress(bool on) { decompress_ = on; }
+
+inline void ClientImpl::set_payload_max_length(size_t length) {
+  payload_max_length_ = length;
+  has_payload_max_length_ = true;
+}
+
+inline void ClientImpl::set_interface(const std::string &intf) {
+  interface_ = intf;
+}
+
+inline void ClientImpl::set_proxy(const std::string &host, int port) {
+  proxy_host_ = host;
+  proxy_port_ = port;
+}
+
+inline void ClientImpl::set_proxy_basic_auth(const std::string &username,
+                                             const std::string &password) {
+  proxy_basic_auth_username_ = username;
+  proxy_basic_auth_password_ = password;
+}
+
+inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
+  proxy_bearer_token_auth_token_ = token;
+}
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+inline void ClientImpl::set_digest_auth(const std::string &username,
+                                        const std::string &password) {
+  digest_auth_username_ = username;
+  digest_auth_password_ = password;
+}
+
+inline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path,
+                                         const std::string &ca_cert_dir_path) {
+  ca_cert_file_path_ = ca_cert_file_path;
+  ca_cert_dir_path_ = ca_cert_dir_path;
+}
+
+inline void ClientImpl::set_proxy_digest_auth(const std::string &username,
+                                              const std::string &password) {
+  proxy_digest_auth_username_ = username;
+  proxy_digest_auth_password_ = password;
+}
+
+inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
+  server_certificate_verification_ = enabled;
+}
+
+inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
+  server_hostname_verification_ = enabled;
+}
+#endif
+
+inline void ClientImpl::set_logger(Logger logger) {
+  logger_ = std::move(logger);
+}
+
+inline void ClientImpl::set_error_logger(ErrorLogger error_logger) {
+  error_logger_ = std::move(error_logger);
+}
+
+/*
+ * SSL/TLS Common Implementation
+ */
+
+inline ClientConnection::~ClientConnection() {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if (session) {
+    tls::shutdown(session, true);
+    tls::free_session(session);
+    session = nullptr;
+  }
+#endif
+
+  if (sock != INVALID_SOCKET) {
+    detail::close_socket(sock);
+    sock = INVALID_SOCKET;
+  }
+}
+
+// Universal client implementation
+inline Client::Client(const std::string &scheme_host_port)
+    : Client(scheme_host_port, std::string(), std::string()) {}
+
+inline Client::Client(const std::string &scheme_host_port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path) {
+  detail::UrlComponents uc;
+  if (detail::parse_url(scheme_host_port, uc) && !uc.host.empty()) {
+    auto &scheme = uc.scheme;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    if (!scheme.empty() && (scheme != "http" && scheme != "https")) {
+#else
+    if (!scheme.empty() && scheme != "http") {
+#endif
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+      std::string msg = "'" + scheme + "' scheme is not supported.";
+      throw std::invalid_argument(msg);
+#endif
+      return;
+    }
+
+    auto is_ssl = scheme == "https";
+
+    auto host = std::move(uc.host);
+
+    auto port = is_ssl ? 443 : 80;
+    if (!uc.port.empty() && !detail::parse_port(uc.port, port)) { return; }
+
+    if (is_ssl) {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+      cli_ = detail::make_unique<SSLClient>(host, port, client_cert_path,
+                                            client_key_path);
+      is_ssl_ = is_ssl;
+#endif
+    } else {
+      cli_ = detail::make_unique<ClientImpl>(host, port, client_cert_path,
+                                             client_key_path);
+    }
+  } else {
+    // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress)
+    // if port param below changes.
+    cli_ = detail::make_unique<ClientImpl>(scheme_host_port, 80,
+                                           client_cert_path, client_key_path);
+  }
+}
+
+inline Client::Client(const std::string &host, int port)
+    : Client(host, port, std::string(), std::string()) {}
+
+inline Client::Client(const std::string &host, int port,
+                      const std::string &client_cert_path,
+                      const std::string &client_key_path)
+    : cli_(detail::make_unique<ClientImpl>(host, port, client_cert_path,
+                                           client_key_path)) {}
+
+inline Client::~Client() = default;
+
+inline bool Client::is_valid() const {
+  return cli_ != nullptr && cli_->is_valid();
+}
+
+inline Result Client::Get(const std::string &path, DownloadProgress progress) {
+  return cli_->Get(path, std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Headers &headers,
+                          DownloadProgress progress) {
+  return cli_->Get(path, headers, std::move(progress));
+}
+inline Result Client::Get(const std::string &path,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, std::move(content_receiver), std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Headers &headers,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, headers, std::move(content_receiver),
+                   std::move(progress));
+}
+inline Result Client::Get(const std::string &path,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, std::move(response_handler),
+                   std::move(content_receiver), std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Headers &headers,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, headers, std::move(response_handler),
+                   std::move(content_receiver), std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Params &params,
+                          const Headers &headers, DownloadProgress progress) {
+  return cli_->Get(path, params, headers, std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Params &params,
+                          const Headers &headers,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, params, headers, std::move(content_receiver),
+                   std::move(progress));
+}
+inline Result Client::Get(const std::string &path, const Params &params,
+                          const Headers &headers,
+                          ResponseHandler response_handler,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Get(path, params, headers, std::move(response_handler),
+                   std::move(content_receiver), std::move(progress));
+}
+
+inline Result Client::Head(const std::string &path) { return cli_->Head(path); }
+inline Result Client::Head(const std::string &path, const Headers &headers) {
+  return cli_->Head(path, headers);
+}
+
+inline Result Client::Post(const std::string &path) { return cli_->Post(path); }
+inline Result Client::Post(const std::string &path, const Headers &headers) {
+  return cli_->Post(path, headers);
+}
+inline Result Client::Post(const std::string &path, const char *body,
+                           size_t content_length,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, body, content_length, content_type, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const char *body, size_t content_length,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, body, content_length, content_type,
+                    progress);
+}
+inline Result Client::Post(const std::string &path, const std::string &body,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, body, content_type, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const std::string &body,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, body, content_type, progress);
+}
+inline Result Client::Post(const std::string &path, size_t content_length,
+                           ContentProvider content_provider,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, content_length, std::move(content_provider),
+                    content_type, progress);
+}
+inline Result Client::Post(const std::string &path, size_t content_length,
+                           ContentProvider content_provider,
+                           const std::string &content_type,
+                           ContentReceiver content_receiver,
+                           UploadProgress progress) {
+  return cli_->Post(path, content_length, std::move(content_provider),
+                    content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Post(const std::string &path,
+                           ContentProviderWithoutLength content_provider,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, std::move(content_provider), content_type, progress);
+}
+inline Result Client::Post(const std::string &path,
+                           ContentProviderWithoutLength content_provider,
+                           const std::string &content_type,
+                           ContentReceiver content_receiver,
+                           UploadProgress progress) {
+  return cli_->Post(path, std::move(content_provider), content_type,
+                    std::move(content_receiver), progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           size_t content_length,
+                           ContentProvider content_provider,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, content_length, std::move(content_provider),
+                    content_type, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           size_t content_length,
+                           ContentProvider content_provider,
+                           const std::string &content_type,
+                           ContentReceiver content_receiver,
+                           DownloadProgress progress) {
+  return cli_->Post(path, headers, content_length, std::move(content_provider),
+                    content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           ContentProviderWithoutLength content_provider,
+                           const std::string &content_type,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, std::move(content_provider), content_type,
+                    progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           ContentProviderWithoutLength content_provider,
+                           const std::string &content_type,
+                           ContentReceiver content_receiver,
+                           DownloadProgress progress) {
+  return cli_->Post(path, headers, std::move(content_provider), content_type,
+                    std::move(content_receiver), progress);
+}
+inline Result Client::Post(const std::string &path, const Params &params) {
+  return cli_->Post(path, params);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const Params &params) {
+  return cli_->Post(path, headers, params);
+}
+inline Result Client::Post(const std::string &path,
+                           const UploadFormDataItems &items,
+                           UploadProgress progress) {
+  return cli_->Post(path, items, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const UploadFormDataItems &items,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, items, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const UploadFormDataItems &items,
+                           const std::string &boundary,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, items, boundary, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const UploadFormDataItems &items,
+                           const FormDataProviderItems &provider_items,
+                           UploadProgress progress) {
+  return cli_->Post(path, headers, items, provider_items, progress);
+}
+inline Result Client::Post(const std::string &path, const Headers &headers,
+                           const std::string &body,
+                           const std::string &content_type,
+                           ContentReceiver content_receiver,
+                           DownloadProgress progress) {
+  return cli_->Post(path, headers, body, content_type,
+                    std::move(content_receiver), progress);
+}
+
+inline Result Client::Put(const std::string &path) { return cli_->Put(path); }
+inline Result Client::Put(const std::string &path, const Headers &headers) {
+  return cli_->Put(path, headers);
+}
+inline Result Client::Put(const std::string &path, const char *body,
+                          size_t content_length,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, body, content_length, content_type, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const char *body, size_t content_length,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, body, content_length, content_type, progress);
+}
+inline Result Client::Put(const std::string &path, const std::string &body,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, body, content_type, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const std::string &body,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, body, content_type, progress);
+}
+inline Result Client::Put(const std::string &path, size_t content_length,
+                          ContentProvider content_provider,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, content_length, std::move(content_provider),
+                   content_type, progress);
+}
+inline Result Client::Put(const std::string &path, size_t content_length,
+                          ContentProvider content_provider,
+                          const std::string &content_type,
+                          ContentReceiver content_receiver,
+                          UploadProgress progress) {
+  return cli_->Put(path, content_length, std::move(content_provider),
+                   content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Put(const std::string &path,
+                          ContentProviderWithoutLength content_provider,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, std::move(content_provider), content_type, progress);
+}
+inline Result Client::Put(const std::string &path,
+                          ContentProviderWithoutLength content_provider,
+                          const std::string &content_type,
+                          ContentReceiver content_receiver,
+                          UploadProgress progress) {
+  return cli_->Put(path, std::move(content_provider), content_type,
+                   std::move(content_receiver), progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          size_t content_length,
+                          ContentProvider content_provider,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, content_length, std::move(content_provider),
+                   content_type, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          size_t content_length,
+                          ContentProvider content_provider,
+                          const std::string &content_type,
+                          ContentReceiver content_receiver,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, content_length, std::move(content_provider),
+                   content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          ContentProviderWithoutLength content_provider,
+                          const std::string &content_type,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, std::move(content_provider), content_type,
+                   progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          ContentProviderWithoutLength content_provider,
+                          const std::string &content_type,
+                          ContentReceiver content_receiver,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, std::move(content_provider), content_type,
+                   std::move(content_receiver), progress);
+}
+inline Result Client::Put(const std::string &path, const Params &params) {
+  return cli_->Put(path, params);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const Params &params) {
+  return cli_->Put(path, headers, params);
+}
+inline Result Client::Put(const std::string &path,
+                          const UploadFormDataItems &items,
+                          UploadProgress progress) {
+  return cli_->Put(path, items, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const UploadFormDataItems &items,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, items, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const UploadFormDataItems &items,
+                          const std::string &boundary,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, items, boundary, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const UploadFormDataItems &items,
+                          const FormDataProviderItems &provider_items,
+                          UploadProgress progress) {
+  return cli_->Put(path, headers, items, provider_items, progress);
+}
+inline Result Client::Put(const std::string &path, const Headers &headers,
+                          const std::string &body,
+                          const std::string &content_type,
+                          ContentReceiver content_receiver,
+                          DownloadProgress progress) {
+  return cli_->Put(path, headers, body, content_type, content_receiver,
+                   progress);
+}
+
+inline Result Client::Patch(const std::string &path) {
+  return cli_->Patch(path);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers) {
+  return cli_->Patch(path, headers);
+}
+inline Result Client::Patch(const std::string &path, const char *body,
+                            size_t content_length,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, body, content_length, content_type, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const char *body, size_t content_length,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, body, content_length, content_type,
+                     progress);
+}
+inline Result Client::Patch(const std::string &path, const std::string &body,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, body, content_type, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const std::string &body,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, body, content_type, progress);
+}
+inline Result Client::Patch(const std::string &path, size_t content_length,
+                            ContentProvider content_provider,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, content_length, std::move(content_provider),
+                     content_type, progress);
+}
+inline Result Client::Patch(const std::string &path, size_t content_length,
+                            ContentProvider content_provider,
+                            const std::string &content_type,
+                            ContentReceiver content_receiver,
+                            UploadProgress progress) {
+  return cli_->Patch(path, content_length, std::move(content_provider),
+                     content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Patch(const std::string &path,
+                            ContentProviderWithoutLength content_provider,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, std::move(content_provider), content_type, progress);
+}
+inline Result Client::Patch(const std::string &path,
+                            ContentProviderWithoutLength content_provider,
+                            const std::string &content_type,
+                            ContentReceiver content_receiver,
+                            UploadProgress progress) {
+  return cli_->Patch(path, std::move(content_provider), content_type,
+                     std::move(content_receiver), progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            size_t content_length,
+                            ContentProvider content_provider,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, content_length, std::move(content_provider),
+                     content_type, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            size_t content_length,
+                            ContentProvider content_provider,
+                            const std::string &content_type,
+                            ContentReceiver content_receiver,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, content_length, std::move(content_provider),
+                     content_type, std::move(content_receiver), progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            ContentProviderWithoutLength content_provider,
+                            const std::string &content_type,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, std::move(content_provider), content_type,
+                     progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            ContentProviderWithoutLength content_provider,
+                            const std::string &content_type,
+                            ContentReceiver content_receiver,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, std::move(content_provider), content_type,
+                     std::move(content_receiver), progress);
+}
+inline Result Client::Patch(const std::string &path, const Params &params) {
+  return cli_->Patch(path, params);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const Params &params) {
+  return cli_->Patch(path, headers, params);
+}
+inline Result Client::Patch(const std::string &path,
+                            const UploadFormDataItems &items,
+                            UploadProgress progress) {
+  return cli_->Patch(path, items, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const UploadFormDataItems &items,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, items, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const UploadFormDataItems &items,
+                            const std::string &boundary,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, items, boundary, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const UploadFormDataItems &items,
+                            const FormDataProviderItems &provider_items,
+                            UploadProgress progress) {
+  return cli_->Patch(path, headers, items, provider_items, progress);
+}
+inline Result Client::Patch(const std::string &path, const Headers &headers,
+                            const std::string &body,
+                            const std::string &content_type,
+                            ContentReceiver content_receiver,
+                            DownloadProgress progress) {
+  return cli_->Patch(path, headers, body, content_type, content_receiver,
+                     progress);
+}
+
+inline Result Client::Delete(const std::string &path,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, progress);
+}
+inline Result Client::Delete(const std::string &path, const Headers &headers,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, headers, progress);
+}
+inline Result Client::Delete(const std::string &path, const char *body,
+                             size_t content_length,
+                             const std::string &content_type,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, body, content_length, content_type, progress);
+}
+inline Result Client::Delete(const std::string &path, const Headers &headers,
+                             const char *body, size_t content_length,
+                             const std::string &content_type,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, headers, body, content_length, content_type,
+                      progress);
+}
+inline Result Client::Delete(const std::string &path, const std::string &body,
+                             const std::string &content_type,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, body, content_type, progress);
+}
+inline Result Client::Delete(const std::string &path, const Headers &headers,
+                             const std::string &body,
+                             const std::string &content_type,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, headers, body, content_type, progress);
+}
+inline Result Client::Delete(const std::string &path, const Params &params,
+                             DownloadProgress progress) {
+  return cli_->Delete(path, params, progress);
+}
+inline Result Client::Delete(const std::string &path, const Headers &headers,
+                             const Params &params, DownloadProgress progress) {
+  return cli_->Delete(path, headers, params, progress);
+}
+
+inline Result Client::Options(const std::string &path) {
+  return cli_->Options(path);
+}
+inline Result Client::Options(const std::string &path, const Headers &headers) {
+  return cli_->Options(path, headers);
+}
+
+inline ClientImpl::StreamHandle
+Client::open_stream(const std::string &method, const std::string &path,
+                    const Params &params, const Headers &headers,
+                    const std::string &body, const std::string &content_type) {
+  return cli_->open_stream(method, path, params, headers, body, content_type);
+}
+
+inline bool Client::send(Request &req, Response &res, Error &error) {
+  return cli_->send(req, res, error);
+}
+
+inline Result Client::send(const Request &req) { return cli_->send(req); }
+
+inline void Client::stop() { cli_->stop(); }
+
+inline std::string Client::host() const { return cli_->host(); }
+
+inline int Client::port() const { return cli_->port(); }
+
+inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); }
+
+inline socket_t Client::socket() const { return cli_->socket(); }
+
+inline void
+Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
+  cli_->set_hostname_addr_map(std::move(addr_map));
+}
+
+inline void Client::set_default_headers(Headers headers) {
+  cli_->set_default_headers(std::move(headers));
+}
+
+inline void Client::set_header_writer(
+    std::function<ssize_t(Stream &, Headers &)> const &writer) {
+  cli_->set_header_writer(writer);
+}
+
+inline void Client::set_address_family(int family) {
+  cli_->set_address_family(family);
+}
+
+inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); }
+
+inline void Client::set_socket_options(SocketOptions socket_options) {
+  cli_->set_socket_options(std::move(socket_options));
+}
+
+inline void Client::set_connection_timeout(time_t sec, time_t usec) {
+  cli_->set_connection_timeout(sec, usec);
+}
+
+inline void Client::set_read_timeout(time_t sec, time_t usec) {
+  cli_->set_read_timeout(sec, usec);
+}
+
+inline void Client::set_write_timeout(time_t sec, time_t usec) {
+  cli_->set_write_timeout(sec, usec);
+}
+
+inline void Client::set_basic_auth(const std::string &username,
+                                   const std::string &password) {
+  cli_->set_basic_auth(username, password);
+}
+inline void Client::set_bearer_token_auth(const std::string &token) {
+  cli_->set_bearer_token_auth(token);
+}
+
+inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); }
+inline void Client::set_follow_location(bool on) {
+  cli_->set_follow_location(on);
+}
+
+inline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); }
+
+inline void Client::set_compress(bool on) { cli_->set_compress(on); }
+
+inline void Client::set_decompress(bool on) { cli_->set_decompress(on); }
+
+inline void Client::set_payload_max_length(size_t length) {
+  cli_->set_payload_max_length(length);
+}
+
+inline void Client::set_interface(const std::string &intf) {
+  cli_->set_interface(intf);
+}
+
+inline void Client::set_proxy(const std::string &host, int port) {
+  cli_->set_proxy(host, port);
+}
+inline void Client::set_proxy_basic_auth(const std::string &username,
+                                         const std::string &password) {
+  cli_->set_proxy_basic_auth(username, password);
+}
+inline void Client::set_proxy_bearer_token_auth(const std::string &token) {
+  cli_->set_proxy_bearer_token_auth(token);
+}
+
+inline void Client::set_logger(Logger logger) {
+  cli_->set_logger(std::move(logger));
+}
+
+inline void Client::set_error_logger(ErrorLogger error_logger) {
+  cli_->set_error_logger(std::move(error_logger));
+}
+
+/*
+ * Group 6: SSL Server and Client implementation
+ */
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+
+// SSL HTTP server implementation
+inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
+                            const char *client_ca_cert_file_path,
+                            const char *client_ca_cert_dir_path,
+                            const char *private_key_password) {
+  using namespace tls;
+
+  ctx_ = create_server_context();
+  if (!ctx_) { return; }
+
+  // Load server certificate and private key
+  if (!set_server_cert_file(ctx_, cert_path, private_key_path,
+                            private_key_password)) {
+    last_ssl_error_ = static_cast<int>(get_error());
+    free_context(ctx_);
+    ctx_ = nullptr;
+    return;
+  }
+
+  // Load client CA certificates for client authentication
+  if (client_ca_cert_file_path || client_ca_cert_dir_path) {
+    if (!set_client_ca_file(ctx_, client_ca_cert_file_path,
+                            client_ca_cert_dir_path)) {
+      last_ssl_error_ = static_cast<int>(get_error());
+      free_context(ctx_);
+      ctx_ = nullptr;
+      return;
+    }
+    // Enable client certificate verification
+    set_verify_client(ctx_, true);
+  }
+}
+
+inline SSLServer::SSLServer(const PemMemory &pem) {
+  using namespace tls;
+  ctx_ = create_server_context();
+  if (ctx_) {
+    if (!set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem,
+                             pem.private_key_password)) {
+      last_ssl_error_ = static_cast<int>(get_error());
+      free_context(ctx_);
+      ctx_ = nullptr;
+    } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) {
+      if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) {
+        last_ssl_error_ = static_cast<int>(get_error());
+        free_context(ctx_);
+        ctx_ = nullptr;
+      } else {
+        set_verify_client(ctx_, true);
+      }
+    }
+  }
+}
+
+inline SSLServer::SSLServer(const tls::ContextSetupCallback &setup_callback) {
+  using namespace tls;
+  ctx_ = create_server_context();
+  if (ctx_) {
+    if (!setup_callback(ctx_)) {
+      free_context(ctx_);
+      ctx_ = nullptr;
+    }
+  }
+}
+
+inline SSLServer::~SSLServer() {
+  if (ctx_) { tls::free_context(ctx_); }
+}
+
+inline bool SSLServer::is_valid() const { return ctx_ != nullptr; }
+
+inline bool SSLServer::process_and_close_socket(socket_t sock) {
+  using namespace tls;
+
+  // Create TLS session with mutex protection
+  session_t session = nullptr;
+  {
+    std::lock_guard<std::mutex> guard(ctx_mutex_);
+    session = create_session(static_cast<ctx_t>(ctx_), sock);
+  }
+
+  if (!session) {
+    last_ssl_error_ = static_cast<int>(get_error());
+    detail::shutdown_socket(sock);
+    detail::close_socket(sock);
+    return false;
+  }
+
+  // Use scope_exit to ensure cleanup on all paths (including exceptions)
+  bool handshake_done = false;
+  bool ret = false;
+  bool websocket_upgraded = false;
+  auto cleanup = detail::scope_exit([&] {
+    if (handshake_done) { shutdown(session, !websocket_upgraded && ret); }
+    free_session(session);
+    detail::shutdown_socket(sock);
+    detail::close_socket(sock);
+  });
+
+  // Perform TLS accept handshake with timeout
+  TlsError tls_err;
+  if (!accept_nonblocking(session, sock, read_timeout_sec_, read_timeout_usec_,
+                          &tls_err)) {
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+    // Map TlsError to legacy ssl_error for backward compatibility
+    if (tls_err.code == ErrorCode::WantRead) {
+      last_ssl_error_ = SSL_ERROR_WANT_READ;
+    } else if (tls_err.code == ErrorCode::WantWrite) {
+      last_ssl_error_ = SSL_ERROR_WANT_WRITE;
+    } else {
+      last_ssl_error_ = SSL_ERROR_SSL;
+    }
+#else
+    last_ssl_error_ = static_cast<int>(get_error());
+#endif
+    return false;
+  }
+
+  handshake_done = true;
+
+  std::string remote_addr;
+  int remote_port = 0;
+  detail::get_remote_ip_and_port(sock, remote_addr, remote_port);
+
+  std::string local_addr;
+  int local_port = 0;
+  detail::get_local_ip_and_port(sock, local_addr, local_port);
+
+  ret = detail::process_server_socket_ssl(
+      svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
+      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_,
+      [&](Stream &strm, bool close_connection, bool &connection_closed) {
+        return process_request(
+            strm, remote_addr, remote_port, local_addr, local_port,
+            close_connection, connection_closed,
+            [&](Request &req) { req.ssl = session; }, &websocket_upgraded);
+      });
+
+  return ret;
+}
+
+inline bool SSLServer::update_certs_pem(const char *cert_pem,
+                                        const char *key_pem,
+                                        const char *client_ca_pem,
+                                        const char *password) {
+  if (!ctx_) { return false; }
+  std::lock_guard<std::mutex> guard(ctx_mutex_);
+  if (!tls::update_server_cert(ctx_, cert_pem, key_pem, password)) {
+    return false;
+  }
+  if (client_ca_pem) {
+    return tls::update_server_client_ca(ctx_, client_ca_pem);
+  }
+  return true;
+}
+
+// SSL HTTP client implementation
+inline SSLClient::~SSLClient() {
+  if (ctx_) { tls::free_context(ctx_); }
+  // Make sure to shut down SSL since shutdown_ssl will resolve to the
+  // base function rather than the derived function once we get to the
+  // base class destructor, and won't free the SSL (causing a leak).
+  shutdown_ssl_impl(socket_, true);
+}
+
+inline bool SSLClient::is_valid() const { return ctx_ != nullptr; }
+
+inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) {
+  shutdown_ssl_impl(socket, shutdown_gracefully);
+}
+
+inline void SSLClient::shutdown_ssl_impl(Socket &socket,
+                                         bool shutdown_gracefully) {
+  if (socket.sock == INVALID_SOCKET) {
+    assert(socket.ssl == nullptr);
+    return;
+  }
+  if (socket.ssl) {
+    tls::shutdown(socket.ssl, shutdown_gracefully);
+    {
+      std::lock_guard<std::mutex> guard(ctx_mutex_);
+      tls::free_session(socket.ssl);
+    }
+    socket.ssl = nullptr;
+  }
+  assert(socket.ssl == nullptr);
+}
+
+inline bool SSLClient::process_socket(
+    const Socket &socket,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    std::function<bool(Stream &strm)> callback) {
+  assert(socket.ssl);
+  return detail::process_client_socket_ssl(
+      socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_,
+      write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time,
+      std::move(callback));
+}
+
+inline bool SSLClient::is_ssl() const { return true; }
+
+inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {
+  if (!is_valid()) {
+    error = Error::SSLConnection;
+    return false;
+  }
+  return ClientImpl::create_and_connect_socket(socket, error);
+}
+
+inline bool SSLClient::setup_proxy_connection(
+    Socket &socket,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    Response &res, bool &success, Error &error) {
+  if (proxy_host_.empty() || proxy_port_ == -1) { return true; }
+
+  if (!connect_with_proxy(socket, start_time, res, success, error)) {
+    return false;
+  }
+
+  if (!initialize_ssl(socket, error)) {
+    success = false;
+    return false;
+  }
+
+  return true;
+}
+
+// Assumes that socket_mutex_ is locked and that there are no requests in
+// flight
+inline bool SSLClient::connect_with_proxy(
+    Socket &socket,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    Response &res, bool &success, Error &error) {
+  success = true;
+  Response proxy_res;
+  if (!detail::process_client_socket(
+          socket.sock, read_timeout_sec_, read_timeout_usec_,
+          write_timeout_sec_, write_timeout_usec_, max_timeout_msec_,
+          start_time, [&](Stream &strm) {
+            Request req2;
+            req2.method = "CONNECT";
+            req2.path =
+                detail::make_host_and_port_string_always_port(host_, port_);
+            if (max_timeout_msec_ > 0) {
+              req2.start_time_ = std::chrono::steady_clock::now();
+            }
+            return process_request(strm, req2, proxy_res, false, error);
+          })) {
+    // Thread-safe to close everything because we are assuming there are no
+    // requests in flight
+    shutdown_ssl(socket, true);
+    shutdown_socket(socket);
+    close_socket(socket);
+    success = false;
+    return false;
+  }
+
+  if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) {
+    if (!proxy_digest_auth_username_.empty() &&
+        !proxy_digest_auth_password_.empty()) {
+      std::map<std::string, std::string> auth;
+      if (detail::parse_www_authenticate(proxy_res, auth, true)) {
+        // Close the current socket and create a new one for the authenticated
+        // request
+        shutdown_ssl(socket, true);
+        shutdown_socket(socket);
+        close_socket(socket);
+
+        // Create a new socket for the authenticated CONNECT request
+        if (!ensure_socket_connection(socket, error)) {
+          success = false;
+          output_error_log(error, nullptr);
+          return false;
+        }
+
+        proxy_res = Response();
+        if (!detail::process_client_socket(
+                socket.sock, read_timeout_sec_, read_timeout_usec_,
+                write_timeout_sec_, write_timeout_usec_, max_timeout_msec_,
+                start_time, [&](Stream &strm) {
+                  Request req3;
+                  req3.method = "CONNECT";
+                  req3.path = detail::make_host_and_port_string_always_port(
+                      host_, port_);
+                  req3.headers.insert(detail::make_digest_authentication_header(
+                      req3, auth, 1, detail::random_string(10),
+                      proxy_digest_auth_username_, proxy_digest_auth_password_,
+                      true));
+                  if (max_timeout_msec_ > 0) {
+                    req3.start_time_ = std::chrono::steady_clock::now();
+                  }
+                  return process_request(strm, req3, proxy_res, false, error);
+                })) {
+          // Thread-safe to close everything because we are assuming there are
+          // no requests in flight
+          shutdown_ssl(socket, true);
+          shutdown_socket(socket);
+          close_socket(socket);
+          success = false;
+          return false;
+        }
+      }
+    }
+  }
+
+  // If status code is not 200, proxy request is failed.
+  // Set error to ProxyConnection and return proxy response
+  // as the response of the request
+  if (proxy_res.status != StatusCode::OK_200) {
+    error = Error::ProxyConnection;
+    output_error_log(error, nullptr);
+    res = std::move(proxy_res);
+    // Thread-safe to close everything because we are assuming there are
+    // no requests in flight
+    shutdown_ssl(socket, true);
+    shutdown_socket(socket);
+    close_socket(socket);
+    return false;
+  }
+
+  return true;
+}
+
+inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {
+  if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; }
+
+  if (!proxy_host_.empty() && proxy_port_ != -1) { return true; }
+
+  if (!initialize_ssl(socket, error)) {
+    shutdown_socket(socket);
+    close_socket(socket);
+    return false;
+  }
+
+  return true;
+}
+
+// SSL HTTP client implementation
+inline SSLClient::SSLClient(const std::string &host)
+    : SSLClient(host, 443, std::string(), std::string()) {}
+
+inline SSLClient::SSLClient(const std::string &host, int port)
+    : SSLClient(host, port, std::string(), std::string()) {}
+
+inline void SSLClient::init_ctx() {
+  ctx_ = tls::create_client_context();
+  if (ctx_) { tls::set_min_version(ctx_, tls::Version::TLS1_2); }
+}
+
+inline void SSLClient::reset_ctx_on_error() {
+  last_backend_error_ = tls::get_error();
+  tls::free_context(ctx_);
+  ctx_ = nullptr;
+}
+
+inline SSLClient::SSLClient(const std::string &host, int port,
+                            const std::string &client_cert_path,
+                            const std::string &client_key_path,
+                            const std::string &private_key_password)
+    : ClientImpl(host, port, client_cert_path, client_key_path) {
+  init_ctx();
+  if (!ctx_) { return; }
+
+  if (!client_cert_path.empty() && !client_key_path.empty()) {
+    const char *password =
+        private_key_password.empty() ? nullptr : private_key_password.c_str();
+    if (!tls::set_client_cert_file(ctx_, client_cert_path.c_str(),
+                                   client_key_path.c_str(), password)) {
+      reset_ctx_on_error();
+    }
+  }
+}
+
+inline SSLClient::SSLClient(const std::string &host, int port,
+                            const PemMemory &pem)
+    : ClientImpl(host, port) {
+  init_ctx();
+  if (!ctx_) { return; }
+
+  if (pem.cert_pem && pem.key_pem) {
+    if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem,
+                                  pem.private_key_password)) {
+      reset_ctx_on_error();
+    }
+  }
+}
+
+inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
+  if (ca_cert_store && ctx_) {
+    // set_ca_store takes ownership of ca_cert_store
+    tls::set_ca_store(ctx_, ca_cert_store);
+  } else if (ca_cert_store) {
+    tls::free_ca_store(ca_cert_store);
+  }
+}
+
+inline void
+SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
+  if (!ctx_) { return; }
+  tls::set_verify_callback(ctx_, verifier);
+}
+
+inline void SSLClient::set_session_verifier(
+    std::function<SSLVerifierResponse(tls::session_t)> verifier) {
+  session_verifier_ = std::move(verifier);
+}
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
+  enable_windows_cert_verification_ = enabled;
+}
+#endif
+
+inline void SSLClient::load_ca_cert_store(const char *ca_cert,
+                                          std::size_t size) {
+  if (ctx_ && ca_cert && size > 0) {
+    ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer
+    tls::load_ca_pem(ctx_, ca_cert, size);
+  }
+}
+
+inline bool SSLClient::load_certs() {
+  auto ret = true;
+
+  std::call_once(initialize_cert_, [&]() {
+    std::lock_guard<std::mutex> guard(ctx_mutex_);
+
+    if (!ca_cert_file_path_.empty()) {
+      if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) {
+        last_backend_error_ = tls::get_error();
+        ret = false;
+      }
+    } else if (!ca_cert_dir_path_.empty()) {
+      if (!tls::load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) {
+        last_backend_error_ = tls::get_error();
+        ret = false;
+      }
+    } else if (ca_cert_pem_.empty()) {
+      if (!tls::load_system_certs(ctx_)) {
+        last_backend_error_ = tls::get_error();
+      }
+    }
+  });
+
+  return ret;
+}
+
+inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
+  using namespace tls;
+
+  // Load CA certificates if server verification is enabled
+  if (server_certificate_verification_) {
+    if (!load_certs()) {
+      error = Error::SSLLoadingCerts;
+      output_error_log(error, nullptr);
+      return false;
+    }
+  }
+
+  bool is_ip = detail::is_ip_address(host_);
+
+#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
+  // MbedTLS/wolfSSL need explicit verification mode (OpenSSL uses
+  // SSL_VERIFY_NONE by default and performs all verification post-handshake).
+  // For IP addresses with verification enabled, use OPTIONAL mode since
+  // these backends require hostname for strict verification.
+  if (is_ip && server_certificate_verification_) {
+    set_verify_client(ctx_, false);
+  } else {
+    set_verify_client(ctx_, server_certificate_verification_);
+  }
+#endif
+
+  // Create TLS session
+  session_t session = nullptr;
+  {
+    std::lock_guard<std::mutex> guard(ctx_mutex_);
+    session = create_session(ctx_, socket.sock);
+  }
+
+  if (!session) {
+    error = Error::SSLConnection;
+    last_backend_error_ = get_error();
+    return false;
+  }
+
+  // Use scope_exit to ensure session is freed on error paths
+  bool success = false;
+  auto session_guard = detail::scope_exit([&] {
+    if (!success) { free_session(session); }
+  });
+
+  // Set SNI extension (skip for IP addresses per RFC 6066).
+  // On MbedTLS, set_sni also enables hostname verification internally.
+  // On OpenSSL, set_sni only sets SNI; verification is done post-handshake.
+  if (!is_ip) {
+    if (!set_sni(session, host_.c_str())) {
+      error = Error::SSLConnection;
+      last_backend_error_ = get_error();
+      return false;
+    }
+  }
+
+  // Perform non-blocking TLS handshake with timeout
+  TlsError tls_err;
+  if (!connect_nonblocking(session, socket.sock, connection_timeout_sec_,
+                           connection_timeout_usec_, &tls_err)) {
+    last_ssl_error_ = static_cast<int>(tls_err.code);
+    last_backend_error_ = tls_err.backend_code;
+    if (tls_err.code == ErrorCode::CertVerifyFailed) {
+      error = Error::SSLServerVerification;
+    } else if (tls_err.code == ErrorCode::HostnameMismatch) {
+      error = Error::SSLServerHostnameVerification;
+    } else {
+      error = Error::SSLConnection;
+    }
+    output_error_log(error, nullptr);
+    return false;
+  }
+
+  // Post-handshake session verifier callback
+  auto verification_status = SSLVerifierResponse::NoDecisionMade;
+  if (session_verifier_) { verification_status = session_verifier_(session); }
+
+  if (verification_status == SSLVerifierResponse::CertificateRejected) {
+    last_backend_error_ = get_error();
+    error = Error::SSLServerVerification;
+    output_error_log(error, nullptr);
+    return false;
+  }
+
+  // Default server certificate verification
+  if (verification_status == SSLVerifierResponse::NoDecisionMade &&
+      server_certificate_verification_) {
+    verify_result_ = tls::get_verify_result(session);
+    if (verify_result_ != 0) {
+      last_backend_error_ = static_cast<uint64_t>(verify_result_);
+      error = Error::SSLServerVerification;
+      output_error_log(error, nullptr);
+      return false;
+    }
+
+    auto server_cert = get_peer_cert(session);
+    if (!server_cert) {
+      last_backend_error_ = get_error();
+      error = Error::SSLServerVerification;
+      output_error_log(error, nullptr);
+      return false;
+    }
+    auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
+
+    // Hostname verification (post-handshake for all cases).
+    // On OpenSSL, verification is always post-handshake (SSL_VERIFY_NONE).
+    // On MbedTLS, set_sni already enabled hostname verification during
+    // handshake for non-IP hosts, but this check is still needed for IP
+    // addresses where SNI is not set.
+    if (server_hostname_verification_) {
+      if (!verify_hostname(server_cert, host_.c_str())) {
+        last_backend_error_ = hostname_mismatch_code();
+        error = Error::SSLServerHostnameVerification;
+        output_error_log(error, nullptr);
+        return false;
+      }
+    }
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+    // Additional Windows Schannel verification.
+    // This provides real-time certificate validation with Windows Update
+    // integration, working with both OpenSSL and MbedTLS backends.
+    // Skip when a custom CA cert is specified, as the Windows certificate
+    // store would not know about user-provided CA certificates.
+    if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&
+        ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) {
+      std::vector<unsigned char> der;
+      if (get_cert_der(server_cert, der)) {
+        uint64_t wincrypt_error = 0;
+        if (!detail::verify_cert_with_windows_schannel(
+                der, host_, server_hostname_verification_, wincrypt_error)) {
+          last_backend_error_ = wincrypt_error;
+          error = Error::SSLServerVerification;
+          output_error_log(error, nullptr);
+          return false;
+        }
+      }
+    }
+#endif
+  }
+
+  success = true;
+  socket.ssl = session;
+  return true;
+}
+
+inline void Client::set_digest_auth(const std::string &username,
+                                    const std::string &password) {
+  cli_->set_digest_auth(username, password);
+}
+
+inline void Client::set_proxy_digest_auth(const std::string &username,
+                                          const std::string &password) {
+  cli_->set_proxy_digest_auth(username, password);
+}
+
+inline void Client::enable_server_certificate_verification(bool enabled) {
+  cli_->enable_server_certificate_verification(enabled);
+}
+
+inline void Client::enable_server_hostname_verification(bool enabled) {
+  cli_->enable_server_hostname_verification(enabled);
+}
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+inline void Client::enable_windows_certificate_verification(bool enabled) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(
+        enabled);
+  }
+}
+#endif
+
+inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path,
+                                     const std::string &ca_cert_dir_path) {
+  cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path);
+}
+
+inline void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).set_ca_cert_store(ca_cert_store);
+  } else if (ca_cert_store) {
+    tls::free_ca_store(ca_cert_store);
+  }
+}
+
+inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) {
+  set_ca_cert_store(tls::create_ca_store(ca_cert, size));
+}
+
+inline void
+Client::set_server_certificate_verifier(tls::VerifyCallback verifier) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).set_server_certificate_verifier(
+        std::move(verifier));
+  }
+}
+
+inline void Client::set_session_verifier(
+    std::function<SSLVerifierResponse(tls::session_t)> verifier) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).set_session_verifier(std::move(verifier));
+  }
+}
+
+inline tls::ctx_t Client::tls_context() const {
+  if (is_ssl_) { return static_cast<SSLClient &>(*cli_).tls_context(); }
+  return nullptr;
+}
+
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 7: TLS abstraction layer - Common API
+ */
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+
+namespace tls {
+
+// Helper for PeerCert construction
+inline PeerCert get_peer_cert_from_session(const_session_t session) {
+  return PeerCert(get_peer_cert(session));
+}
+
+namespace impl {
+
+inline VerifyCallback &get_verify_callback() {
+  static thread_local VerifyCallback callback;
+  return callback;
+}
+
+inline VerifyCallback &get_mbedtls_verify_callback() {
+  static thread_local VerifyCallback callback;
+  return callback;
+}
+
+// Check if a string is an IPv4 address
+inline bool is_ipv4_address(const std::string &str) {
+  int dots = 0;
+  for (char c : str) {
+    if (c == '.') {
+      dots++;
+    } else if (!isdigit(static_cast<unsigned char>(c))) {
+      return false;
+    }
+  }
+  return dots == 3;
+}
+
+// Parse IPv4 address string to bytes
+inline bool parse_ipv4(const std::string &str, unsigned char *out) {
+  const char *p = str.c_str();
+  for (int i = 0; i < 4; i++) {
+    if (i > 0) {
+      if (*p != '.') { return false; }
+      p++;
+    }
+    int val = 0;
+    int digits = 0;
+    while (*p >= '0' && *p <= '9') {
+      val = val * 10 + (*p - '0');
+      if (val > 255) { return false; }
+      p++;
+      digits++;
+    }
+    if (digits == 0) { return false; }
+    // Reject leading zeros (e.g., "01.002.03.04") to prevent ambiguity
+    if (digits > 1 && *(p - digits) == '0') { return false; }
+    out[i] = static_cast<unsigned char>(val);
+  }
+  return *p == '\0';
+}
+
+#ifdef _WIN32
+// Enumerate Windows system certificates and call callback with DER data
+template <typename Callback>
+inline bool enumerate_windows_system_certs(Callback cb) {
+  bool loaded = false;
+  static const wchar_t *store_names[] = {L"ROOT", L"CA"};
+  for (auto store_name : store_names) {
+    HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name);
+    if (hStore) {
+      PCCERT_CONTEXT pContext = nullptr;
+      while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
+             nullptr) {
+        if (cb(pContext->pbCertEncoded, pContext->cbCertEncoded)) {
+          loaded = true;
+        }
+      }
+      CertCloseStore(hStore, 0);
+    }
+  }
+  return loaded;
+}
+#endif
+
+#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+// Enumerate macOS Keychain certificates and call callback with DER data
+template <typename Callback>
+inline bool enumerate_macos_keychain_certs(Callback cb) {
+  bool loaded = false;
+  CFArrayRef certs = nullptr;
+  OSStatus status = SecTrustCopyAnchorCertificates(&certs);
+  if (status == errSecSuccess && certs) {
+    CFIndex count = CFArrayGetCount(certs);
+    for (CFIndex i = 0; i < count; i++) {
+      SecCertificateRef cert =
+          (SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
+      CFDataRef data = SecCertificateCopyData(cert);
+      if (data) {
+        if (cb(CFDataGetBytePtr(data),
+               static_cast<size_t>(CFDataGetLength(data)))) {
+          loaded = true;
+        }
+        CFRelease(data);
+      }
+    }
+    CFRelease(certs);
+  }
+  return loaded;
+}
+#endif
+
+#if !defined(_WIN32) && !(defined(__APPLE__) &&                                \
+                          defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN))
+// Common CA certificate file paths on Linux/Unix
+inline const char **system_ca_paths() {
+  static const char *paths[] = {
+      "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
+      "/etc/pki/tls/certs/ca-bundle.crt",   // RHEL/CentOS
+      "/etc/ssl/ca-bundle.pem",             // OpenSUSE
+      "/etc/pki/tls/cacert.pem",            // OpenELEC
+      "/etc/ssl/cert.pem",                  // Alpine, FreeBSD
+      nullptr};
+  return paths;
+}
+
+// Common CA certificate directory paths on Linux/Unix
+inline const char **system_ca_dirs() {
+  static const char *dirs[] = {"/etc/ssl/certs",             // Debian/Ubuntu
+                               "/etc/pki/tls/certs",         // RHEL/CentOS
+                               "/usr/share/ca-certificates", // Other
+                               nullptr};
+  return dirs;
+}
+#endif
+
+} // namespace impl
+
+inline bool set_client_ca_file(ctx_t ctx, const char *ca_file,
+                               const char *ca_dir) {
+  if (!ctx) { return false; }
+
+  bool success = true;
+  if (ca_file && *ca_file) {
+    if (!load_ca_file(ctx, ca_file)) { success = false; }
+  }
+  if (ca_dir && *ca_dir) {
+    if (!load_ca_dir(ctx, ca_dir)) { success = false; }
+  }
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+  // Set CA list for client certificate request (CertificateRequest message)
+  if (ca_file && *ca_file) {
+    auto list = SSL_load_client_CA_file(ca_file);
+    if (list) { SSL_CTX_set_client_CA_list(static_cast<SSL_CTX *>(ctx), list); }
+  }
+#endif
+
+  return success;
+}
+
+inline bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                                const char *password) {
+  return set_client_cert_pem(ctx, cert, key, password);
+}
+
+inline bool set_server_cert_file(ctx_t ctx, const char *cert_path,
+                                 const char *key_path, const char *password) {
+  return set_client_cert_file(ctx, cert_path, key_path, password);
+}
+
+// PeerCert implementation
+inline PeerCert::PeerCert() = default;
+
+inline PeerCert::PeerCert(cert_t cert) : cert_(cert) {}
+
+inline PeerCert::PeerCert(PeerCert &&other) noexcept : cert_(other.cert_) {
+  other.cert_ = nullptr;
+}
+
+inline PeerCert &PeerCert::operator=(PeerCert &&other) noexcept {
+  if (this != &other) {
+    if (cert_) { free_cert(cert_); }
+    cert_ = other.cert_;
+    other.cert_ = nullptr;
+  }
+  return *this;
+}
+
+inline PeerCert::~PeerCert() {
+  if (cert_) { free_cert(cert_); }
+}
+
+inline PeerCert::operator bool() const { return cert_ != nullptr; }
+
+inline std::string PeerCert::subject_cn() const {
+  return cert_ ? get_cert_subject_cn(cert_) : std::string();
+}
+
+inline std::string PeerCert::issuer_name() const {
+  return cert_ ? get_cert_issuer_name(cert_) : std::string();
+}
+
+inline bool PeerCert::check_hostname(const char *hostname) const {
+  return cert_ ? verify_hostname(cert_, hostname) : false;
+}
+
+inline std::vector<SanEntry> PeerCert::sans() const {
+  std::vector<SanEntry> result;
+  if (cert_) { get_cert_sans(cert_, result); }
+  return result;
+}
+
+inline bool PeerCert::validity(time_t &not_before, time_t &not_after) const {
+  return cert_ ? get_cert_validity(cert_, not_before, not_after) : false;
+}
+
+inline std::string PeerCert::serial() const {
+  return cert_ ? get_cert_serial(cert_) : std::string();
+}
+
+// VerifyContext method implementations
+inline std::string VerifyContext::subject_cn() const {
+  return cert ? get_cert_subject_cn(cert) : std::string();
+}
+
+inline std::string VerifyContext::issuer_name() const {
+  return cert ? get_cert_issuer_name(cert) : std::string();
+}
+
+inline bool VerifyContext::check_hostname(const char *hostname) const {
+  return cert ? verify_hostname(cert, hostname) : false;
+}
+
+inline std::vector<SanEntry> VerifyContext::sans() const {
+  std::vector<SanEntry> result;
+  if (cert) { get_cert_sans(cert, result); }
+  return result;
+}
+
+inline bool VerifyContext::validity(time_t &not_before,
+                                    time_t &not_after) const {
+  return cert ? get_cert_validity(cert, not_before, not_after) : false;
+}
+
+inline std::string VerifyContext::serial() const {
+  return cert ? get_cert_serial(cert) : std::string();
+}
+
+// TlsError static method implementation
+inline std::string TlsError::verify_error_to_string(long error_code) {
+  return verify_error_string(error_code);
+}
+
+} // namespace tls
+
+// Request::peer_cert() implementation
+inline tls::PeerCert Request::peer_cert() const {
+  return tls::get_peer_cert_from_session(ssl);
+}
+
+// Request::sni() implementation
+inline std::string Request::sni() const {
+  if (!ssl) { return std::string(); }
+  const char *s = tls::get_sni(ssl);
+  return s ? std::string(s) : std::string();
+}
+
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 8: TLS abstraction layer - OpenSSL backend
+ */
+
+/*
+ * OpenSSL Backend Implementation
+ */
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+namespace tls {
+
+namespace impl {
+
+// Helper to map OpenSSL SSL_get_error to ErrorCode
+inline ErrorCode map_ssl_error(int ssl_error, int &out_errno) {
+  switch (ssl_error) {
+  case SSL_ERROR_NONE: return ErrorCode::Success;
+  case SSL_ERROR_WANT_READ: return ErrorCode::WantRead;
+  case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;
+  case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
+  case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
+  case SSL_ERROR_SSL:
+  default: return ErrorCode::Fatal;
+  }
+}
+
+// Helper: Create client CA list from PEM string
+// Returns a new STACK_OF(X509_NAME)* or nullptr on failure
+// Caller takes ownership of returned list
+inline STACK_OF(X509_NAME) *
+    create_client_ca_list_from_pem(const char *ca_pem) {
+  if (!ca_pem) { return nullptr; }
+
+  auto ca_list = sk_X509_NAME_new_null();
+  if (!ca_list) { return nullptr; }
+
+  BIO *bio = BIO_new_mem_buf(ca_pem, -1);
+  if (!bio) {
+    sk_X509_NAME_pop_free(ca_list, X509_NAME_free);
+    return nullptr;
+  }
+
+  X509 *cert = nullptr;
+  while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) !=
+         nullptr) {
+    X509_NAME *name = X509_get_subject_name(cert);
+    if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(name)); }
+    X509_free(cert);
+  }
+  BIO_free(bio);
+
+  return ca_list;
+}
+
+// OpenSSL verify callback wrapper
+inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
+  auto &callback = get_verify_callback();
+  if (!callback) { return preverify_ok; }
+
+  // Get SSL object from X509_STORE_CTX
+  auto ssl = static_cast<SSL *>(
+      X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
+  if (!ssl) { return preverify_ok; }
+
+  // Get current certificate and depth
+  auto cert = X509_STORE_CTX_get_current_cert(ctx);
+  int depth = X509_STORE_CTX_get_error_depth(ctx);
+  int error = X509_STORE_CTX_get_error(ctx);
+
+  // Build context
+  VerifyContext verify_ctx;
+  verify_ctx.session = static_cast<session_t>(ssl);
+  verify_ctx.cert = static_cast<cert_t>(cert);
+  verify_ctx.depth = depth;
+  verify_ctx.preverify_ok = (preverify_ok != 0);
+  verify_ctx.error_code = error;
+  verify_ctx.error_string =
+      (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr;
+
+  return callback(verify_ctx) ? 1 : 0;
+}
+
+} // namespace impl
+
+inline ctx_t create_client_context() {
+  SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
+  if (ctx) {
+    // Disable auto-retry to properly handle non-blocking I/O
+    SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY);
+    // Set minimum TLS version
+    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
+  }
+  return static_cast<ctx_t>(ctx);
+}
+
+inline void free_context(ctx_t ctx) {
+  if (ctx) { SSL_CTX_free(static_cast<SSL_CTX *>(ctx)); }
+}
+
+inline bool set_min_version(ctx_t ctx, Version version) {
+  if (!ctx) return false;
+  return SSL_CTX_set_min_proto_version(static_cast<SSL_CTX *>(ctx),
+                                       static_cast<int>(version)) == 1;
+}
+
+inline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) {
+  if (!ctx || !pem || len == 0) return false;
+
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+  auto store = SSL_CTX_get_cert_store(ssl_ctx);
+  if (!store) return false;
+
+  auto bio = BIO_new_mem_buf(pem, static_cast<int>(len));
+  if (!bio) return false;
+
+  bool ok = true;
+  X509 *cert = nullptr;
+  while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) !=
+         nullptr) {
+    if (X509_STORE_add_cert(store, cert) != 1) {
+      // Ignore duplicate errors
+      auto err = ERR_peek_last_error();
+      if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) {
+        ok = false;
+      }
+    }
+    X509_free(cert);
+    if (!ok) break;
+  }
+  BIO_free(bio);
+
+  // Clear any "no more certificates" errors
+  ERR_clear_error();
+  return ok;
+}
+
+inline bool load_ca_file(ctx_t ctx, const char *file_path) {
+  if (!ctx || !file_path) return false;
+  return SSL_CTX_load_verify_locations(static_cast<SSL_CTX *>(ctx), file_path,
+                                       nullptr) == 1;
+}
+
+inline bool load_ca_dir(ctx_t ctx, const char *dir_path) {
+  if (!ctx || !dir_path) return false;
+  return SSL_CTX_load_verify_locations(static_cast<SSL_CTX *>(ctx), nullptr,
+                                       dir_path) == 1;
+}
+
+inline bool load_system_certs(ctx_t ctx) {
+  if (!ctx) return false;
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+#ifdef _WIN32
+  // Windows: Load from system certificate store (ROOT and CA)
+  auto store = SSL_CTX_get_cert_store(ssl_ctx);
+  if (!store) return false;
+
+  bool loaded_any = false;
+  static const wchar_t *store_names[] = {L"ROOT", L"CA"};
+  for (auto store_name : store_names) {
+    auto hStore = CertOpenSystemStoreW(NULL, store_name);
+    if (!hStore) continue;
+
+    PCCERT_CONTEXT pContext = nullptr;
+    while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
+           nullptr) {
+      const unsigned char *data = pContext->pbCertEncoded;
+      auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded);
+      if (x509) {
+        if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
+        X509_free(x509);
+      }
+    }
+    CertCloseStore(hStore, 0);
+  }
+  return loaded_any;
+
+#elif defined(__APPLE__)
+#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
+  // macOS: Load from Keychain
+  auto store = SSL_CTX_get_cert_store(ssl_ctx);
+  if (!store) return false;
+
+  CFArrayRef certs = nullptr;
+  if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) {
+    return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+  }
+
+  bool loaded_any = false;
+  auto count = CFArrayGetCount(certs);
+  for (CFIndex i = 0; i < count; i++) {
+    auto cert = reinterpret_cast<SecCertificateRef>(
+        const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
+    CFDataRef der = SecCertificateCopyData(cert);
+    if (der) {
+      const unsigned char *data = CFDataGetBytePtr(der);
+      auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
+      if (x509) {
+        if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
+        X509_free(x509);
+      }
+      CFRelease(der);
+    }
+  }
+  CFRelease(certs);
+  return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+#else
+  return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+#endif
+
+#else
+  // Other Unix: use default verify paths
+  return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+#endif
+}
+
+inline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                                const char *password) {
+  if (!ctx || !cert || !key) return false;
+
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  // Load certificate
+  auto cert_bio = BIO_new_mem_buf(cert, -1);
+  if (!cert_bio) return false;
+
+  auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
+  BIO_free(cert_bio);
+  if (!x509) return false;
+
+  auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1;
+  X509_free(x509);
+  if (!cert_ok) return false;
+
+  // Load private key
+  auto key_bio = BIO_new_mem_buf(key, -1);
+  if (!key_bio) return false;
+
+  auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr,
+                                      password ? const_cast<char *>(password)
+                                               : nullptr);
+  BIO_free(key_bio);
+  if (!pkey) return false;
+
+  auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1;
+  EVP_PKEY_free(pkey);
+
+  return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1;
+}
+
+inline bool set_client_cert_file(ctx_t ctx, const char *cert_path,
+                                 const char *key_path, const char *password) {
+  if (!ctx || !cert_path || !key_path) return false;
+
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  if (password && password[0] != '\0') {
+    SSL_CTX_set_default_passwd_cb_userdata(
+        ssl_ctx, reinterpret_cast<void *>(const_cast<char *>(password)));
+  }
+
+  return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 &&
+         SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1;
+}
+
+inline ctx_t create_server_context() {
+  SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
+  if (ctx) {
+    SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION |
+                                 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
+  }
+  return static_cast<ctx_t>(ctx);
+}
+
+inline void set_verify_client(ctx_t ctx, bool require) {
+  if (!ctx) return;
+  SSL_CTX_set_verify(static_cast<SSL_CTX *>(ctx),
+                     require
+                         ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
+                         : SSL_VERIFY_NONE,
+                     nullptr);
+}
+
+inline session_t create_session(ctx_t ctx, socket_t sock) {
+  if (!ctx || sock == INVALID_SOCKET) return nullptr;
+
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+  SSL *ssl = SSL_new(ssl_ctx);
+  if (!ssl) return nullptr;
+
+  // Disable auto-retry for proper non-blocking I/O handling
+  SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY);
+
+  auto bio = BIO_new_socket(static_cast<int>(sock), BIO_NOCLOSE);
+  if (!bio) {
+    SSL_free(ssl);
+    return nullptr;
+  }
+
+  SSL_set_bio(ssl, bio, bio);
+  return static_cast<session_t>(ssl);
+}
+
+inline void free_session(session_t session) {
+  if (session) { SSL_free(static_cast<SSL *>(session)); }
+}
+
+inline bool set_sni(session_t session, const char *hostname) {
+  if (!session || !hostname) return false;
+
+  auto ssl = static_cast<SSL *>(session);
+
+  // Set SNI (Server Name Indication) only - does not enable verification
+#if defined(OPENSSL_IS_BORINGSSL)
+  return SSL_set_tlsext_host_name(ssl, hostname) == 1;
+#else
+  // Direct call instead of macro to suppress -Wold-style-cast warning
+  return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name,
+                  static_cast<void *>(const_cast<char *>(hostname))) == 1;
+#endif
+}
+
+inline bool set_hostname(session_t session, const char *hostname) {
+  if (!session || !hostname) return false;
+
+  auto ssl = static_cast<SSL *>(session);
+
+  // Set SNI (Server Name Indication)
+  if (!set_sni(session, hostname)) { return false; }
+
+  // Enable hostname verification
+  auto param = SSL_get0_param(ssl);
+  if (!param) return false;
+
+  X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
+  if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; }
+
+  SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr);
+  return true;
+}
+
+inline TlsError connect(session_t session) {
+  if (!session) { return TlsError(); }
+
+  auto ssl = static_cast<SSL *>(session);
+  auto ret = SSL_connect(ssl);
+
+  TlsError err;
+  if (ret == 1) {
+    err.code = ErrorCode::Success;
+  } else {
+    auto ssl_err = SSL_get_error(ssl, ret);
+    err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
+    err.backend_code = ERR_get_error();
+  }
+  return err;
+}
+
+inline TlsError accept(session_t session) {
+  if (!session) { return TlsError(); }
+
+  auto ssl = static_cast<SSL *>(session);
+  auto ret = SSL_accept(ssl);
+
+  TlsError err;
+  if (ret == 1) {
+    err.code = ErrorCode::Success;
+  } else {
+    auto ssl_err = SSL_get_error(ssl, ret);
+    err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
+    err.backend_code = ERR_get_error();
+  }
+  return err;
+}
+
+inline bool connect_nonblocking(session_t session, socket_t sock,
+                                time_t timeout_sec, time_t timeout_usec,
+                                TlsError *err) {
+  if (!session) {
+    if (err) { err->code = ErrorCode::Fatal; }
+    return false;
+  }
+
+  auto ssl = static_cast<SSL *>(session);
+  auto bio = SSL_get_rbio(ssl);
+
+  // Set non-blocking mode for handshake
+  detail::set_nonblocking(sock, true);
+  if (bio) { BIO_set_nbio(bio, 1); }
+
+  auto cleanup = detail::scope_exit([&]() {
+    // Restore blocking mode after handshake
+    if (bio) { BIO_set_nbio(bio, 0); }
+    detail::set_nonblocking(sock, false);
+  });
+
+  auto res = 0;
+  while ((res = SSL_connect(ssl)) != 1) {
+    auto ssl_err = SSL_get_error(ssl, res);
+    switch (ssl_err) {
+    case SSL_ERROR_WANT_READ:
+      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+      break;
+    case SSL_ERROR_WANT_WRITE:
+      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+      break;
+    default: break;
+    }
+    if (err) {
+      err->code = impl::map_ssl_error(ssl_err, err->sys_errno);
+      err->backend_code = ERR_get_error();
+    }
+    return false;
+  }
+  if (err) { err->code = ErrorCode::Success; }
+  return true;
+}
+
+inline bool accept_nonblocking(session_t session, socket_t sock,
+                               time_t timeout_sec, time_t timeout_usec,
+                               TlsError *err) {
+  if (!session) {
+    if (err) { err->code = ErrorCode::Fatal; }
+    return false;
+  }
+
+  auto ssl = static_cast<SSL *>(session);
+  auto bio = SSL_get_rbio(ssl);
+
+  // Set non-blocking mode for handshake
+  detail::set_nonblocking(sock, true);
+  if (bio) { BIO_set_nbio(bio, 1); }
+
+  auto cleanup = detail::scope_exit([&]() {
+    // Restore blocking mode after handshake
+    if (bio) { BIO_set_nbio(bio, 0); }
+    detail::set_nonblocking(sock, false);
+  });
+
+  auto res = 0;
+  while ((res = SSL_accept(ssl)) != 1) {
+    auto ssl_err = SSL_get_error(ssl, res);
+    switch (ssl_err) {
+    case SSL_ERROR_WANT_READ:
+      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+      break;
+    case SSL_ERROR_WANT_WRITE:
+      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+      break;
+    default: break;
+    }
+    if (err) {
+      err->code = impl::map_ssl_error(ssl_err, err->sys_errno);
+      err->backend_code = ERR_get_error();
+    }
+    return false;
+  }
+  if (err) { err->code = ErrorCode::Success; }
+  return true;
+}
+
+inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto ssl = static_cast<SSL *>(session);
+  constexpr auto max_len =
+      static_cast<size_t>((std::numeric_limits<int>::max)());
+  if (len > max_len) { len = max_len; }
+  auto ret = SSL_read(ssl, buf, static_cast<int>(len));
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return ret;
+  }
+
+  auto ssl_err = SSL_get_error(ssl, ret);
+  err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
+  if (err.code == ErrorCode::PeerClosed) {
+    return 0;
+  } // Gracefully handle the peer closed state.
+  if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
+  return -1;
+}
+
+inline ssize_t write(session_t session, const void *buf, size_t len,
+                     TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto ssl = static_cast<SSL *>(session);
+  auto ret = SSL_write(ssl, buf, static_cast<int>(len));
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return ret;
+  }
+
+  auto ssl_err = SSL_get_error(ssl, ret);
+  err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
+  if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
+  return -1;
+}
+
+inline int pending(const_session_t session) {
+  if (!session) return 0;
+  return SSL_pending(static_cast<SSL *>(const_cast<void *>(session)));
+}
+
+inline void shutdown(session_t session, bool graceful) {
+  if (!session) return;
+
+  auto ssl = static_cast<SSL *>(session);
+  if (graceful) {
+    // First call sends close_notify
+    if (SSL_shutdown(ssl) == 0) {
+      // Second call waits for peer's close_notify
+      SSL_shutdown(ssl);
+    }
+  }
+}
+
+inline bool is_peer_closed(session_t session, socket_t sock) {
+  if (!session) return true;
+
+  // Temporarily set socket to non-blocking to avoid blocking on SSL_peek
+  detail::set_nonblocking(sock, true);
+  auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  auto ssl = static_cast<SSL *>(session);
+  char buf;
+  auto ret = SSL_peek(ssl, &buf, 1);
+  if (ret > 0) return false;
+
+  auto err = SSL_get_error(ssl, ret);
+  return err == SSL_ERROR_ZERO_RETURN;
+}
+
+inline cert_t get_peer_cert(const_session_t session) {
+  if (!session) return nullptr;
+  return static_cast<cert_t>(SSL_get1_peer_certificate(
+      static_cast<SSL *>(const_cast<void *>(session))));
+}
+
+inline void free_cert(cert_t cert) {
+  if (cert) { X509_free(static_cast<X509 *>(cert)); }
+}
+
+inline bool verify_hostname(cert_t cert, const char *hostname) {
+  if (!cert || !hostname) return false;
+
+  auto x509 = static_cast<X509 *>(cert);
+
+  // Use X509_check_ip_asc for IP addresses, X509_check_host for DNS names
+  if (detail::is_ip_address(hostname)) {
+    return X509_check_ip_asc(x509, hostname, 0) == 1;
+  }
+  return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1;
+}
+
+inline uint64_t hostname_mismatch_code() {
+  return static_cast<uint64_t>(X509_V_ERR_HOSTNAME_MISMATCH);
+}
+
+inline long get_verify_result(const_session_t session) {
+  if (!session) return X509_V_ERR_UNSPECIFIED;
+  return SSL_get_verify_result(static_cast<SSL *>(const_cast<void *>(session)));
+}
+
+inline std::string get_cert_subject_cn(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<X509 *>(cert);
+  auto subject_name = X509_get_subject_name(x509);
+  if (!subject_name) return "";
+
+  char buf[256];
+  auto len =
+      X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf));
+  if (len < 0) return "";
+  return std::string(buf, static_cast<size_t>(len));
+}
+
+inline std::string get_cert_issuer_name(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<X509 *>(cert);
+  auto issuer_name = X509_get_issuer_name(x509);
+  if (!issuer_name) return "";
+
+  char buf[256];
+  X509_NAME_oneline(issuer_name, buf, sizeof(buf));
+  return std::string(buf);
+}
+
+inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
+  sans.clear();
+  if (!cert) return false;
+  auto x509 = static_cast<X509 *>(cert);
+
+  auto names = static_cast<GENERAL_NAMES *>(
+      X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
+  if (!names) return true; // No SANs is valid
+
+  auto count = sk_GENERAL_NAME_num(names);
+  for (decltype(count) i = 0; i < count; i++) {
+    auto gen = sk_GENERAL_NAME_value(names, i);
+    if (!gen) continue;
+
+    SanEntry entry;
+    switch (gen->type) {
+    case GEN_DNS:
+      entry.type = SanType::DNS;
+      if (gen->d.dNSName) {
+        entry.value = std::string(
+            reinterpret_cast<const char *>(
+                ASN1_STRING_get0_data(gen->d.dNSName)),
+            static_cast<size_t>(ASN1_STRING_length(gen->d.dNSName)));
+      }
+      break;
+    case GEN_IPADD:
+      entry.type = SanType::IP;
+      if (gen->d.iPAddress) {
+        auto data = ASN1_STRING_get0_data(gen->d.iPAddress);
+        auto len = ASN1_STRING_length(gen->d.iPAddress);
+        if (len == 4) {
+          // IPv4
+          char buf[INET_ADDRSTRLEN];
+          inet_ntop(AF_INET, data, buf, sizeof(buf));
+          entry.value = buf;
+        } else if (len == 16) {
+          // IPv6
+          char buf[INET6_ADDRSTRLEN];
+          inet_ntop(AF_INET6, data, buf, sizeof(buf));
+          entry.value = buf;
+        }
+      }
+      break;
+    case GEN_EMAIL:
+      entry.type = SanType::EMAIL;
+      if (gen->d.rfc822Name) {
+        entry.value = std::string(
+            reinterpret_cast<const char *>(
+                ASN1_STRING_get0_data(gen->d.rfc822Name)),
+            static_cast<size_t>(ASN1_STRING_length(gen->d.rfc822Name)));
+      }
+      break;
+    case GEN_URI:
+      entry.type = SanType::URI;
+      if (gen->d.uniformResourceIdentifier) {
+        entry.value = std::string(
+            reinterpret_cast<const char *>(
+                ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)),
+            static_cast<size_t>(
+                ASN1_STRING_length(gen->d.uniformResourceIdentifier)));
+      }
+      break;
+    default: entry.type = SanType::OTHER; break;
+    }
+
+    if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
+  }
+
+  GENERAL_NAMES_free(names);
+  return true;
+}
+
+inline bool get_cert_validity(cert_t cert, time_t &not_before,
+                              time_t &not_after) {
+  if (!cert) return false;
+  auto x509 = static_cast<X509 *>(cert);
+
+  auto nb = X509_get0_notBefore(x509);
+  auto na = X509_get0_notAfter(x509);
+  if (!nb || !na) return false;
+
+  ASN1_TIME *epoch = ASN1_TIME_new();
+  if (!epoch) return false;
+  auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); });
+
+  if (!ASN1_TIME_set(epoch, 0)) return false;
+
+  int pday, psec;
+
+  if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false;
+  not_before = 86400 * (time_t)pday + psec;
+
+  if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false;
+  not_after = 86400 * (time_t)pday + psec;
+
+  return true;
+}
+
+inline std::string get_cert_serial(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<X509 *>(cert);
+
+  auto serial = X509_get_serialNumber(x509);
+  if (!serial) return "";
+
+  auto bn = ASN1_INTEGER_to_BN(serial, nullptr);
+  if (!bn) return "";
+
+  auto hex = BN_bn2hex(bn);
+  BN_free(bn);
+  if (!hex) return "";
+
+  std::string result(hex);
+  OPENSSL_free(hex);
+  return result;
+}
+
+inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+  if (!cert) return false;
+  auto x509 = static_cast<X509 *>(cert);
+  auto len = i2d_X509(x509, nullptr);
+  if (len < 0) return false;
+  der.resize(static_cast<size_t>(len));
+  auto p = der.data();
+  i2d_X509(x509, &p);
+  return true;
+}
+
+inline const char *get_sni(const_session_t session) {
+  if (!session) return nullptr;
+  auto ssl = static_cast<SSL *>(const_cast<void *>(session));
+  return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+}
+
+inline uint64_t peek_error() { return ERR_peek_last_error(); }
+
+inline uint64_t get_error() { return ERR_get_error(); }
+
+inline std::string error_string(uint64_t code) {
+  char buf[256];
+  ERR_error_string_n(static_cast<unsigned long>(code), buf, sizeof(buf));
+  return std::string(buf);
+}
+
+inline ca_store_t create_ca_store(const char *pem, size_t len) {
+  auto mem = BIO_new_mem_buf(pem, static_cast<int>(len));
+  if (!mem) { return nullptr; }
+  auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); });
+
+  auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr);
+  if (!inf) { return nullptr; }
+
+  auto store = X509_STORE_new();
+  if (store) {
+    for (auto i = 0; i < static_cast<int>(sk_X509_INFO_num(inf)); i++) {
+      auto itmp = sk_X509_INFO_value(inf, i);
+      if (!itmp) { continue; }
+      if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); }
+      if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); }
+    }
+  }
+
+  sk_X509_INFO_pop_free(inf, X509_INFO_free);
+  return static_cast<ca_store_t>(store);
+}
+
+inline void free_ca_store(ca_store_t store) {
+  if (store) { X509_STORE_free(static_cast<X509_STORE *>(store)); }
+}
+
+inline bool set_ca_store(ctx_t ctx, ca_store_t store) {
+  if (!ctx || !store) { return false; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+  auto x509_store = static_cast<X509_STORE *>(store);
+
+  // Check if same store is already set
+  if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; }
+
+  // SSL_CTX_set_cert_store takes ownership and frees the old store
+  SSL_CTX_set_cert_store(ssl_ctx, x509_store);
+  return true;
+}
+
+inline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {
+  certs.clear();
+  if (!ctx) { return 0; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  auto store = SSL_CTX_get_cert_store(ssl_ctx);
+  if (!store) { return 0; }
+
+  auto objs = X509_STORE_get0_objects(store);
+  if (!objs) { return 0; }
+
+  auto count = sk_X509_OBJECT_num(objs);
+  for (decltype(count) i = 0; i < count; i++) {
+    auto obj = sk_X509_OBJECT_value(objs, i);
+    if (!obj) { continue; }
+    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
+      auto x509 = X509_OBJECT_get0_X509(obj);
+      if (x509) {
+        // Increment reference count so caller can free it
+        X509_up_ref(x509);
+        certs.push_back(static_cast<cert_t>(x509));
+      }
+    }
+  }
+  return certs.size();
+}
+
+inline std::vector<std::string> get_ca_names(ctx_t ctx) {
+  std::vector<std::string> names;
+  if (!ctx) { return names; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  auto store = SSL_CTX_get_cert_store(ssl_ctx);
+  if (!store) { return names; }
+
+  auto objs = X509_STORE_get0_objects(store);
+  if (!objs) { return names; }
+
+  auto count = sk_X509_OBJECT_num(objs);
+  for (decltype(count) i = 0; i < count; i++) {
+    auto obj = sk_X509_OBJECT_value(objs, i);
+    if (!obj) { continue; }
+    if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
+      auto x509 = X509_OBJECT_get0_X509(obj);
+      if (x509) {
+        auto subject = X509_get_subject_name(x509);
+        if (subject) {
+          char buf[512];
+          X509_NAME_oneline(subject, buf, sizeof(buf));
+          names.push_back(buf);
+        }
+      }
+    }
+  }
+  return names;
+}
+
+inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
+                               const char *key_pem, const char *password) {
+  if (!ctx || !cert_pem || !key_pem) { return false; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  // Load certificate from PEM
+  auto cert_bio = BIO_new_mem_buf(cert_pem, -1);
+  if (!cert_bio) { return false; }
+  auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
+  BIO_free(cert_bio);
+  if (!cert) { return false; }
+
+  // Load private key from PEM
+  auto key_bio = BIO_new_mem_buf(key_pem, -1);
+  if (!key_bio) {
+    X509_free(cert);
+    return false;
+  }
+  auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr,
+                                     password ? const_cast<char *>(password)
+                                              : nullptr);
+  BIO_free(key_bio);
+  if (!key) {
+    X509_free(cert);
+    return false;
+  }
+
+  // Update certificate and key
+  auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 &&
+             SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1;
+
+  X509_free(cert);
+  EVP_PKEY_free(key);
+  return ret;
+}
+
+inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
+  if (!ctx || !ca_pem) { return false; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  // Create new X509_STORE from PEM
+  auto store = create_ca_store(ca_pem, strlen(ca_pem));
+  if (!store) { return false; }
+
+  // SSL_CTX_set_cert_store takes ownership
+  SSL_CTX_set_cert_store(ssl_ctx, static_cast<X509_STORE *>(store));
+
+  // Set client CA list for client certificate request
+  auto ca_list = impl::create_client_ca_list_from_pem(ca_pem);
+  if (ca_list) {
+    // SSL_CTX_set_client_CA_list takes ownership of ca_list
+    SSL_CTX_set_client_CA_list(ssl_ctx, ca_list);
+  }
+
+  return true;
+}
+
+inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {
+  if (!ctx) { return false; }
+  auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+  impl::get_verify_callback() = std::move(callback);
+
+  if (impl::get_verify_callback()) {
+    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback);
+  } else {
+    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr);
+  }
+  return true;
+}
+
+inline long get_verify_error(const_session_t session) {
+  if (!session) { return -1; }
+  auto ssl = static_cast<SSL *>(const_cast<void *>(session));
+  return SSL_get_verify_result(ssl);
+}
+
+inline std::string verify_error_string(long error_code) {
+  if (error_code == X509_V_OK) { return ""; }
+  const char *str = X509_verify_cert_error_string(static_cast<int>(error_code));
+  return str ? str : "unknown error";
+}
+
+} // namespace tls
+
+inline bool SSLClient::verify_host(X509 *server_cert) const {
+  /* Quote from RFC2818 section 3.1 "Server Identity"
+
+     If a subjectAltName extension of type dNSName is present, that MUST
+     be used as the identity. Otherwise, the (most specific) Common Name
+     field in the Subject field of the certificate MUST be used. Although
+     the use of the Common Name is existing practice, it is deprecated and
+     Certification Authorities are encouraged to use the dNSName instead.
+
+     Matching is performed using the matching rules specified by
+     [RFC2459].  If more than one identity of a given type is present in
+     the certificate (e.g., more than one dNSName name, a match in any one
+     of the set is considered acceptable.) Names may contain the wildcard
+     character * which is considered to match any single domain name
+     component or component fragment. E.g., *.a.com matches foo.a.com but
+     not bar.foo.a.com. f*.com matches foo.com but not bar.com.
+
+     In some cases, the URI is specified as an IP address rather than a
+     hostname. In this case, the iPAddress subjectAltName must be present
+     in the certificate and must exactly match the IP in the URI.
+
+  */
+  return verify_host_with_subject_alt_name(server_cert) ||
+         verify_host_with_common_name(server_cert);
+}
+
+inline bool
+SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
+  auto ret = false;
+
+  auto type = GEN_DNS;
+
+  struct in6_addr addr6 = {};
+  struct in_addr addr = {};
+  size_t addr_len = 0;
+
+#ifndef __MINGW32__
+  if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
+    type = GEN_IPADD;
+    addr_len = sizeof(struct in6_addr);
+  } else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
+    type = GEN_IPADD;
+    addr_len = sizeof(struct in_addr);
+  }
+#endif
+
+  auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
+      X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
+
+  if (alt_names) {
+    auto dsn_matched = false;
+    auto ip_matched = false;
+
+    auto count = sk_GENERAL_NAME_num(alt_names);
+
+    for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
+      auto val = sk_GENERAL_NAME_value(alt_names, i);
+      if (!val || val->type != type) { continue; }
+
+      auto name =
+          reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
+      if (name == nullptr) { continue; }
+
+      auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
+
+      switch (type) {
+      case GEN_DNS:
+        dsn_matched =
+            detail::match_hostname(std::string(name, name_len), host_);
+        break;
+
+      case GEN_IPADD:
+        if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
+          ip_matched = true;
+        }
+        break;
+      }
+    }
+
+    if (dsn_matched || ip_matched) { ret = true; }
+  }
+
+  GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(
+      reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));
+  return ret;
+}
+
+inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
+  const auto subject_name = X509_get_subject_name(server_cert);
+
+  if (subject_name != nullptr) {
+    char name[BUFSIZ];
+    auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName,
+                                              name, sizeof(name));
+
+    if (name_len != -1) {
+      return detail::match_hostname(
+          std::string(name, static_cast<size_t>(name_len)), host_);
+    }
+  }
+
+  return false;
+}
+
+#endif // CPPHTTPLIB_OPENSSL_SUPPORT
+
+/*
+ * Group 9: TLS abstraction layer - Mbed TLS backend
+ */
+
+/*
+ * Mbed TLS Backend Implementation
+ */
+
+#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
+namespace tls {
+
+namespace impl {
+
+// Mbed TLS session wrapper
+struct MbedTlsSession {
+  mbedtls_ssl_context ssl;
+  socket_t sock = INVALID_SOCKET;
+  std::string hostname;     // For client: set via set_sni
+  std::string sni_hostname; // For server: received from client via SNI callback
+
+  MbedTlsSession() { mbedtls_ssl_init(&ssl); }
+
+  ~MbedTlsSession() { mbedtls_ssl_free(&ssl); }
+
+  MbedTlsSession(const MbedTlsSession &) = delete;
+  MbedTlsSession &operator=(const MbedTlsSession &) = delete;
+};
+
+// Thread-local error code accessor for Mbed TLS (since it doesn't have an error
+// queue)
+inline int &mbedtls_last_error() {
+  static thread_local int err = 0;
+  return err;
+}
+
+// Helper to map Mbed TLS error to ErrorCode
+inline ErrorCode map_mbedtls_error(int ret, int &out_errno) {
+  if (ret == 0) { return ErrorCode::Success; }
+  if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; }
+  if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; }
+  if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
+    return ErrorCode::PeerClosed;
+  }
+  if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED ||
+      ret == MBEDTLS_ERR_NET_RECV_FAILED) {
+    out_errno = errno;
+    return ErrorCode::SyscallError;
+  }
+  if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
+    return ErrorCode::CertVerifyFailed;
+  }
+  return ErrorCode::Fatal;
+}
+
+// BIO-like send callback for Mbed TLS
+inline int mbedtls_net_send_cb(void *ctx, const unsigned char *buf,
+                               size_t len) {
+  auto sock = *static_cast<socket_t *>(ctx);
+#ifdef _WIN32
+  auto ret =
+      send(sock, reinterpret_cast<const char *>(buf), static_cast<int>(len), 0);
+  if (ret == SOCKET_ERROR) {
+    int err = WSAGetLastError();
+    if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; }
+    return MBEDTLS_ERR_NET_SEND_FAILED;
+  }
+#else
+  auto ret = send(sock, buf, len, 0);
+  if (ret < 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      return MBEDTLS_ERR_SSL_WANT_WRITE;
+    }
+    return MBEDTLS_ERR_NET_SEND_FAILED;
+  }
+#endif
+  return static_cast<int>(ret);
+}
+
+// BIO-like recv callback for Mbed TLS
+inline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) {
+  auto sock = *static_cast<socket_t *>(ctx);
+#ifdef _WIN32
+  auto ret =
+      recv(sock, reinterpret_cast<char *>(buf), static_cast<int>(len), 0);
+  if (ret == SOCKET_ERROR) {
+    int err = WSAGetLastError();
+    if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; }
+    return MBEDTLS_ERR_NET_RECV_FAILED;
+  }
+#else
+  auto ret = recv(sock, buf, len, 0);
+  if (ret < 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      return MBEDTLS_ERR_SSL_WANT_READ;
+    }
+    return MBEDTLS_ERR_NET_RECV_FAILED;
+  }
+#endif
+  if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; }
+  return static_cast<int>(ret);
+}
+
+// MbedTlsContext constructor/destructor implementations
+inline MbedTlsContext::MbedTlsContext() {
+  mbedtls_ssl_config_init(&conf);
+  mbedtls_entropy_init(&entropy);
+  mbedtls_ctr_drbg_init(&ctr_drbg);
+  mbedtls_x509_crt_init(&ca_chain);
+  mbedtls_x509_crt_init(&own_cert);
+  mbedtls_pk_init(&own_key);
+}
+
+inline MbedTlsContext::~MbedTlsContext() {
+  mbedtls_pk_free(&own_key);
+  mbedtls_x509_crt_free(&own_cert);
+  mbedtls_x509_crt_free(&ca_chain);
+  mbedtls_ctr_drbg_free(&ctr_drbg);
+  mbedtls_entropy_free(&entropy);
+  mbedtls_ssl_config_free(&conf);
+}
+
+// Thread-local storage for SNI captured during handshake
+// This is needed because the SNI callback doesn't have a way to pass
+// session-specific data before the session is fully set up
+inline std::string &mbedpending_sni() {
+  static thread_local std::string sni;
+  return sni;
+}
+
+// SNI callback for Mbed TLS server to capture client's SNI hostname
+inline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl,
+                                const unsigned char *name, size_t name_len) {
+  (void)p_ctx;
+  (void)ssl;
+
+  // Store SNI name in thread-local storage
+  // It will be retrieved and stored in the session after handshake
+  if (name && name_len > 0) {
+    mbedpending_sni().assign(reinterpret_cast<const char *>(name), name_len);
+  } else {
+    mbedpending_sni().clear();
+  }
+  return 0; // Accept any SNI
+}
+
+inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,
+                                   int cert_depth, uint32_t *flags);
+
+// MbedTLS verify callback wrapper
+inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,
+                                   int cert_depth, uint32_t *flags) {
+  auto &callback = get_verify_callback();
+  if (!callback) { return 0; } // Continue with default verification
+
+  // data points to the MbedTlsSession
+  auto *session = static_cast<MbedTlsSession *>(data);
+
+  // Build context
+  VerifyContext verify_ctx;
+  verify_ctx.session = static_cast<session_t>(session);
+  verify_ctx.cert = static_cast<cert_t>(crt);
+  verify_ctx.depth = cert_depth;
+  verify_ctx.preverify_ok = (*flags == 0);
+  verify_ctx.error_code = static_cast<long>(*flags);
+
+  // Convert Mbed TLS flags to error string
+  static thread_local char error_buf[256];
+  if (*flags != 0) {
+    mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), "", *flags);
+    verify_ctx.error_string = error_buf;
+  } else {
+    verify_ctx.error_string = nullptr;
+  }
+
+  bool accepted = callback(verify_ctx);
+
+  if (accepted) {
+    *flags = 0; // Clear all error flags
+    return 0;
+  }
+  return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED;
+}
+
+} // namespace impl
+
+inline ctx_t create_client_context() {
+  auto ctx = new (std::nothrow) impl::MbedTlsContext();
+  if (!ctx) { return nullptr; }
+
+  ctx->is_server = false;
+
+  // Seed the random number generator
+  const char *pers = "httplib_client";
+  int ret = mbedtls_ctr_drbg_seed(
+      &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy,
+      reinterpret_cast<const unsigned char *>(pers), strlen(pers));
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    delete ctx;
+    return nullptr;
+  }
+
+  // Set up SSL config for client
+  ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT,
+                                    MBEDTLS_SSL_TRANSPORT_STREAM,
+                                    MBEDTLS_SSL_PRESET_DEFAULT);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    delete ctx;
+    return nullptr;
+  }
+
+  // Set random number generator
+  mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg);
+
+  // Default: verify peer certificate
+  mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
+
+  // Set minimum TLS version to 1.2
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2);
+#else
+  mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3,
+                               MBEDTLS_SSL_MINOR_VERSION_3);
+#endif
+
+  return static_cast<ctx_t>(ctx);
+}
+
+inline ctx_t create_server_context() {
+  auto ctx = new (std::nothrow) impl::MbedTlsContext();
+  if (!ctx) { return nullptr; }
+
+  ctx->is_server = true;
+
+  // Seed the random number generator
+  const char *pers = "httplib_server";
+  int ret = mbedtls_ctr_drbg_seed(
+      &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy,
+      reinterpret_cast<const unsigned char *>(pers), strlen(pers));
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    delete ctx;
+    return nullptr;
+  }
+
+  // Set up SSL config for server
+  ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER,
+                                    MBEDTLS_SSL_TRANSPORT_STREAM,
+                                    MBEDTLS_SSL_PRESET_DEFAULT);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    delete ctx;
+    return nullptr;
+  }
+
+  // Set random number generator
+  mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg);
+
+  // Default: don't verify client
+  mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE);
+
+  // Set minimum TLS version to 1.2
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2);
+#else
+  mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3,
+                               MBEDTLS_SSL_MINOR_VERSION_3);
+#endif
+
+  // Set SNI callback to capture client's SNI hostname
+  mbedtls_ssl_conf_sni(&ctx->conf, impl::mbedtls_sni_callback, nullptr);
+
+  return static_cast<ctx_t>(ctx);
+}
+
+inline void free_context(ctx_t ctx) {
+  if (ctx) { delete static_cast<impl::MbedTlsContext *>(ctx); }
+}
+
+inline bool set_min_version(ctx_t ctx, Version version) {
+  if (!ctx) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum
+  mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2;
+  if (version >= Version::TLS1_3) {
+#if defined(MBEDTLS_SSL_PROTO_TLS1_3)
+    min_ver = MBEDTLS_SSL_VERSION_TLS1_3;
+#endif
+  }
+  mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver);
+#else
+  // Mbed TLS 2.x uses major/minor version numbers
+  int major = MBEDTLS_SSL_MAJOR_VERSION_3;
+  int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2
+  if (version >= Version::TLS1_3) {
+#if defined(MBEDTLS_SSL_PROTO_TLS1_3)
+    minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3
+#else
+    minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2
+#endif
+  }
+  mbedtls_ssl_conf_min_version(&mctx->conf, major, minor);
+#endif
+  return true;
+}
+
+inline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) {
+  if (!ctx || !pem) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // mbedtls_x509_crt_parse expects null-terminated string for PEM
+  // Add null terminator if not present
+  std::string pem_str(pem, len);
+  int ret = mbedtls_x509_crt_parse(
+      &mctx->ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),
+      pem_str.size() + 1);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);
+  return true;
+}
+
+inline bool load_ca_file(ctx_t ctx, const char *file_path) {
+  if (!ctx || !file_path) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);
+  return true;
+}
+
+inline bool load_ca_dir(ctx_t ctx, const char *dir_path) {
+  if (!ctx || !dir_path) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path);
+  if (ret < 0) { // Returns number of certs on success, negative on error
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);
+  return true;
+}
+
+inline bool load_system_certs(ctx_t ctx) {
+  if (!ctx) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+  bool loaded = false;
+
+#ifdef _WIN32
+  loaded = impl::enumerate_windows_system_certs(
+      [&](const unsigned char *data, size_t len) {
+        return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0;
+      });
+#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
+  loaded = impl::enumerate_macos_keychain_certs(
+      [&](const unsigned char *data, size_t len) {
+        return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0;
+      });
+#else
+  for (auto path = impl::system_ca_paths(); *path; ++path) {
+    if (mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path) >= 0) {
+      loaded = true;
+      break;
+    }
+  }
+
+  if (!loaded) {
+    for (auto dir = impl::system_ca_dirs(); *dir; ++dir) {
+      if (mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir) >= 0) {
+        loaded = true;
+        break;
+      }
+    }
+  }
+#endif
+
+  if (loaded) {
+    mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr);
+  }
+  return loaded;
+}
+
+inline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                                const char *password) {
+  if (!ctx || !cert || !key) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Parse certificate
+  std::string cert_str(cert);
+  int ret = mbedtls_x509_crt_parse(
+      &mctx->own_cert,
+      reinterpret_cast<const unsigned char *>(cert_str.c_str()),
+      cert_str.size() + 1);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Parse private key
+  std::string key_str(key);
+  const unsigned char *pwd =
+      password ? reinterpret_cast<const unsigned char *>(password) : nullptr;
+  size_t pwd_len = password ? strlen(password) : 0;
+
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  ret = mbedtls_pk_parse_key(
+      &mctx->own_key, reinterpret_cast<const unsigned char *>(key_str.c_str()),
+      key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random,
+      &mctx->ctr_drbg);
+#else
+  ret = mbedtls_pk_parse_key(
+      &mctx->own_key, reinterpret_cast<const unsigned char *>(key_str.c_str()),
+      key_str.size() + 1, pwd, pwd_len);
+#endif
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Verify that the certificate and private key match
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key,
+                              mbedtls_ctr_drbg_random, &mctx->ctr_drbg);
+#else
+  ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key);
+#endif
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  return true;
+}
+
+inline bool set_client_cert_file(ctx_t ctx, const char *cert_path,
+                                 const char *key_path, const char *password) {
+  if (!ctx || !cert_path || !key_path) { return false; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Parse certificate file
+  int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Parse private key file
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password,
+                                 mbedtls_ctr_drbg_random, &mctx->ctr_drbg);
+#else
+  ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password);
+#endif
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Verify that the certificate and private key match
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key,
+                              mbedtls_ctr_drbg_random, &mctx->ctr_drbg);
+#else
+  ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key);
+#endif
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  return true;
+}
+
+inline void set_verify_client(ctx_t ctx, bool require) {
+  if (!ctx) { return; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+  mctx->verify_client = require;
+  if (require) {
+    mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
+  } else {
+    // If a verify callback is set, use OPTIONAL mode to ensure the callback
+    // is called (matching OpenSSL behavior). Otherwise use NONE.
+    mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback
+                                               ? MBEDTLS_SSL_VERIFY_OPTIONAL
+                                               : MBEDTLS_SSL_VERIFY_NONE);
+  }
+}
+
+inline session_t create_session(ctx_t ctx, socket_t sock) {
+  if (!ctx || sock == INVALID_SOCKET) { return nullptr; }
+  auto mctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  auto session = new (std::nothrow) impl::MbedTlsSession();
+  if (!session) { return nullptr; }
+
+  session->sock = sock;
+
+  int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    delete session;
+    return nullptr;
+  }
+
+  // Set BIO callbacks
+  mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb,
+                      impl::mbedtls_net_recv_cb, nullptr);
+
+  // Set per-session verify callback with session pointer if callback is
+  // registered
+  if (mctx->has_verify_callback) {
+    mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback,
+                           session);
+  }
+
+  return static_cast<session_t>(session);
+}
+
+inline void free_session(session_t session) {
+  if (session) { delete static_cast<impl::MbedTlsSession *>(session); }
+}
+
+inline bool set_sni(session_t session, const char *hostname) {
+  if (!session || !hostname) { return false; }
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+
+  int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  msession->hostname = hostname;
+  return true;
+}
+
+inline bool set_hostname(session_t session, const char *hostname) {
+  // In Mbed TLS, set_hostname also sets up hostname verification
+  return set_sni(session, hostname);
+}
+
+inline TlsError connect(session_t session) {
+  TlsError err;
+  if (!session) {
+    err.code = ErrorCode::Fatal;
+    return err;
+  }
+
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+  int ret = mbedtls_ssl_handshake(&msession->ssl);
+
+  if (ret == 0) {
+    err.code = ErrorCode::Success;
+  } else {
+    err.code = impl::map_mbedtls_error(ret, err.sys_errno);
+    err.backend_code = static_cast<uint64_t>(-ret);
+    impl::mbedtls_last_error() = ret;
+  }
+
+  return err;
+}
+
+inline TlsError accept(session_t session) {
+  // Same as connect for Mbed TLS - handshake works for both client and server
+  auto result = connect(session);
+
+  // After successful handshake, capture SNI from thread-local storage
+  if (result.code == ErrorCode::Success && session) {
+    auto msession = static_cast<impl::MbedTlsSession *>(session);
+    msession->sni_hostname = std::move(impl::mbedpending_sni());
+    impl::mbedpending_sni().clear();
+  }
+
+  return result;
+}
+
+inline bool connect_nonblocking(session_t session, socket_t sock,
+                                time_t timeout_sec, time_t timeout_usec,
+                                TlsError *err) {
+  if (!session) {
+    if (err) { err->code = ErrorCode::Fatal; }
+    return false;
+  }
+
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+
+  // Set socket to non-blocking mode
+  detail::set_nonblocking(sock, true);
+  auto cleanup =
+      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  int ret;
+  while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) {
+    if (ret == MBEDTLS_ERR_SSL_WANT_READ) {
+      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
+      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    }
+
+    // TlsError or timeout
+    if (err) {
+      err->code = impl::map_mbedtls_error(ret, err->sys_errno);
+      err->backend_code = static_cast<uint64_t>(-ret);
+    }
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  if (err) { err->code = ErrorCode::Success; }
+  return true;
+}
+
+inline bool accept_nonblocking(session_t session, socket_t sock,
+                               time_t timeout_sec, time_t timeout_usec,
+                               TlsError *err) {
+  // Same implementation as connect for Mbed TLS
+  bool result =
+      connect_nonblocking(session, sock, timeout_sec, timeout_usec, err);
+
+  // After successful handshake, capture SNI from thread-local storage
+  if (result && session) {
+    auto msession = static_cast<impl::MbedTlsSession *>(session);
+    msession->sni_hostname = std::move(impl::mbedpending_sni());
+    impl::mbedpending_sni().clear();
+  }
+
+  return result;
+}
+
+inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+  int ret =
+      mbedtls_ssl_read(&msession->ssl, static_cast<unsigned char *>(buf), len);
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return static_cast<ssize_t>(ret);
+  }
+
+  if (ret == 0) {
+    err.code = ErrorCode::PeerClosed;
+    return 0;
+  }
+
+  err.code = impl::map_mbedtls_error(ret, err.sys_errno);
+  err.backend_code = static_cast<uint64_t>(-ret);
+  impl::mbedtls_last_error() = ret;
+  return -1;
+}
+
+inline ssize_t write(session_t session, const void *buf, size_t len,
+                     TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+  int ret = mbedtls_ssl_write(&msession->ssl,
+                              static_cast<const unsigned char *>(buf), len);
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return static_cast<ssize_t>(ret);
+  }
+
+  if (ret == 0) {
+    err.code = ErrorCode::PeerClosed;
+    return 0;
+  }
+
+  err.code = impl::map_mbedtls_error(ret, err.sys_errno);
+  err.backend_code = static_cast<uint64_t>(-ret);
+  impl::mbedtls_last_error() = ret;
+  return -1;
+}
+
+inline int pending(const_session_t session) {
+  if (!session) { return 0; }
+  auto msession =
+      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+  return static_cast<int>(mbedtls_ssl_get_bytes_avail(&msession->ssl));
+}
+
+inline void shutdown(session_t session, bool graceful) {
+  if (!session) { return; }
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+
+  if (graceful) {
+    // Try to send close_notify, but don't block forever
+    int ret;
+    int attempts = 0;
+    while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 &&
+           attempts < 3) {
+      if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
+          ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
+        break;
+      }
+      attempts++;
+    }
+  }
+}
+
+inline bool is_peer_closed(session_t session, socket_t sock) {
+  if (!session || sock == INVALID_SOCKET) { return true; }
+  auto msession = static_cast<impl::MbedTlsSession *>(session);
+
+  // Check if there's already decrypted data available in the TLS buffer
+  // If so, the connection is definitely alive
+  if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; }
+
+  // Set socket to non-blocking to avoid blocking on read
+  detail::set_nonblocking(sock, true);
+  auto cleanup =
+      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  // Try a 1-byte read to check connection status
+  // Note: This will consume the byte if data is available, but for the
+  // purpose of checking if peer is closed, this should be acceptable
+  // since we're only called when we expect the connection might be closing
+  unsigned char buf;
+  int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1);
+
+  // If we got data or WANT_READ (would block), connection is alive
+  if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; }
+
+  // If we get a peer close notify or a connection reset, the peer is closed
+  return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY ||
+         ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0;
+}
+
+inline cert_t get_peer_cert(const_session_t session) {
+  if (!session) { return nullptr; }
+  auto msession =
+      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+
+  // Mbed TLS returns a pointer to the internal peer cert chain.
+  // WARNING: This pointer is only valid while the session is active.
+  // Do not use the certificate after calling free_session().
+  const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl);
+  return const_cast<mbedtls_x509_crt *>(cert);
+}
+
+inline void free_cert(cert_t cert) {
+  // Mbed TLS: peer certificate is owned by the SSL context.
+  // No-op here, but callers should still call this for cross-backend
+  // portability.
+  (void)cert;
+}
+
+inline bool verify_hostname(cert_t cert, const char *hostname) {
+  if (!cert || !hostname) { return false; }
+  auto mcert = static_cast<const mbedtls_x509_crt *>(cert);
+  std::string host_str(hostname);
+
+  // Check if hostname is an IP address
+  bool is_ip = impl::is_ipv4_address(host_str);
+  unsigned char ip_bytes[4];
+  if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
+
+  // Check Subject Alternative Names (SAN)
+  // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags
+  // - DNS names: raw string bytes
+  // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)
+  const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
+  while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
+    const unsigned char *p = san->buf.p;
+    size_t len = san->buf.len;
+
+    if (is_ip) {
+      // Check if this SAN is an IPv4 address (4 bytes)
+      if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; }
+      // Check if this SAN is an IPv6 address (16 bytes) - skip for now
+    } else {
+      // Check if this SAN is a DNS name (printable ASCII string)
+      bool is_dns = len > 0;
+      for (size_t i = 0; i < len && is_dns; i++) {
+        if (p[i] < 32 || p[i] > 126) { is_dns = false; }
+      }
+      if (is_dns) {
+        std::string san_name(reinterpret_cast<const char *>(p), len);
+        if (detail::match_hostname(san_name, host_str)) { return true; }
+      }
+    }
+    san = san->next;
+  }
+
+  // Fallback: Check Common Name (CN) in subject
+  char cn[256];
+  int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);
+  if (ret > 0) {
+    std::string cn_str(cn);
+
+    // Look for "CN=" in the DN string
+    size_t cn_pos = cn_str.find("CN=");
+    if (cn_pos != std::string::npos) {
+      size_t start = cn_pos + 3;
+      size_t end = cn_str.find(',', start);
+      std::string cn_value =
+          cn_str.substr(start, end == std::string::npos ? end : end - start);
+
+      if (detail::match_hostname(cn_value, host_str)) { return true; }
+    }
+  }
+
+  return false;
+}
+
+inline uint64_t hostname_mismatch_code() {
+  return static_cast<uint64_t>(MBEDTLS_X509_BADCERT_CN_MISMATCH);
+}
+
+inline long get_verify_result(const_session_t session) {
+  if (!session) { return -1; }
+  auto msession =
+      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+  uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl);
+  // Return 0 (X509_V_OK equivalent) if verification passed
+  return flags == 0 ? 0 : static_cast<long>(flags);
+}
+
+inline std::string get_cert_subject_cn(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<mbedtls_x509_crt *>(cert);
+
+  // Find the CN in the subject
+  const mbedtls_x509_name *name = &x509->subject;
+  while (name != nullptr) {
+    if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) {
+      return std::string(reinterpret_cast<const char *>(name->val.p),
+                         name->val.len);
+    }
+    name = name->next;
+  }
+  return "";
+}
+
+inline std::string get_cert_issuer_name(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<mbedtls_x509_crt *>(cert);
+
+  // Build a human-readable issuer name string
+  char buf[512];
+  int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer);
+  if (ret < 0) return "";
+  return std::string(buf);
+}
+
+inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
+  sans.clear();
+  if (!cert) return false;
+  auto x509 = static_cast<mbedtls_x509_crt *>(cert);
+
+  // Parse the Subject Alternative Name extension
+  const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
+  while (cur != nullptr) {
+    if (cur->buf.len > 0) {
+      // Mbed TLS stores SAN as ASN.1 sequences
+      // The tag byte indicates the type
+      const unsigned char *p = cur->buf.p;
+      size_t len = cur->buf.len;
+
+      // First byte is the tag
+      unsigned char tag = *p;
+      p++;
+      len--;
+
+      // Parse length (simple single-byte length assumed)
+      if (len > 0 && *p < 0x80) {
+        size_t value_len = *p;
+        p++;
+        len--;
+
+        if (value_len <= len) {
+          SanEntry entry;
+          // ASN.1 context tags for GeneralName
+          switch (tag & 0x1F) {
+          case 2: // dNSName
+            entry.type = SanType::DNS;
+            entry.value =
+                std::string(reinterpret_cast<const char *>(p), value_len);
+            break;
+          case 7: // iPAddress
+            entry.type = SanType::IP;
+            if (value_len == 4) {
+              // IPv4
+              char buf[16];
+              snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
+              entry.value = buf;
+            } else if (value_len == 16) {
+              // IPv6
+              char buf[64];
+              snprintf(buf, sizeof(buf),
+                       "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
+                       "%02x%02x:%02x%02x:%02x%02x:%02x%02x",
+                       p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8],
+                       p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
+              entry.value = buf;
+            }
+            break;
+          case 1: // rfc822Name (email)
+            entry.type = SanType::EMAIL;
+            entry.value =
+                std::string(reinterpret_cast<const char *>(p), value_len);
+            break;
+          case 6: // uniformResourceIdentifier
+            entry.type = SanType::URI;
+            entry.value =
+                std::string(reinterpret_cast<const char *>(p), value_len);
+            break;
+          default: entry.type = SanType::OTHER; break;
+          }
+
+          if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
+        }
+      }
+    }
+    cur = cur->next;
+  }
+  return true;
+}
+
+inline bool get_cert_validity(cert_t cert, time_t &not_before,
+                              time_t &not_after) {
+  if (!cert) return false;
+  auto x509 = static_cast<mbedtls_x509_crt *>(cert);
+
+  // Convert mbedtls_x509_time to time_t
+  auto to_time_t = [](const mbedtls_x509_time &t) -> time_t {
+    struct tm tm_time = {};
+    tm_time.tm_year = t.year - 1900;
+    tm_time.tm_mon = t.mon - 1;
+    tm_time.tm_mday = t.day;
+    tm_time.tm_hour = t.hour;
+    tm_time.tm_min = t.min;
+    tm_time.tm_sec = t.sec;
+#ifdef _WIN32
+    return _mkgmtime(&tm_time);
+#else
+    return timegm(&tm_time);
+#endif
+  };
+
+  not_before = to_time_t(x509->valid_from);
+  not_after = to_time_t(x509->valid_to);
+  return true;
+}
+
+inline std::string get_cert_serial(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<mbedtls_x509_crt *>(cert);
+
+  // Convert serial number to hex string
+  std::string result;
+  result.reserve(x509->serial.len * 2);
+  for (size_t i = 0; i < x509->serial.len; i++) {
+    char hex[3];
+    snprintf(hex, sizeof(hex), "%02X", x509->serial.p[i]);
+    result += hex;
+  }
+  return result;
+}
+
+inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+  if (!cert) return false;
+  auto crt = static_cast<mbedtls_x509_crt *>(cert);
+  if (!crt->raw.p || crt->raw.len == 0) return false;
+  der.assign(crt->raw.p, crt->raw.p + crt->raw.len);
+  return true;
+}
+
+inline const char *get_sni(const_session_t session) {
+  if (!session) return nullptr;
+  auto msession = static_cast<const impl::MbedTlsSession *>(session);
+
+  // For server: return SNI received from client during handshake
+  if (!msession->sni_hostname.empty()) {
+    return msession->sni_hostname.c_str();
+  }
+
+  // For client: return the hostname set via set_sni
+  if (!msession->hostname.empty()) { return msession->hostname.c_str(); }
+
+  return nullptr;
+}
+
+inline uint64_t peek_error() {
+  // Mbed TLS doesn't have an error queue, return the last error
+  return static_cast<uint64_t>(-impl::mbedtls_last_error());
+}
+
+inline uint64_t get_error() {
+  // Mbed TLS doesn't have an error queue, return and clear the last error
+  uint64_t err = static_cast<uint64_t>(-impl::mbedtls_last_error());
+  impl::mbedtls_last_error() = 0;
+  return err;
+}
+
+inline std::string error_string(uint64_t code) {
+  char buf[256];
+  mbedtls_strerror(-static_cast<int>(code), buf, sizeof(buf));
+  return std::string(buf);
+}
+
+inline ca_store_t create_ca_store(const char *pem, size_t len) {
+  auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
+  if (!ca_chain) { return nullptr; }
+
+  mbedtls_x509_crt_init(ca_chain);
+
+  // mbedtls_x509_crt_parse expects null-terminated PEM
+  int ret = mbedtls_x509_crt_parse(ca_chain,
+                                   reinterpret_cast<const unsigned char *>(pem),
+                                   len + 1); // +1 for null terminator
+  if (ret != 0) {
+    // Try without +1 in case PEM is already null-terminated
+    ret = mbedtls_x509_crt_parse(
+        ca_chain, reinterpret_cast<const unsigned char *>(pem), len);
+    if (ret != 0) {
+      mbedtls_x509_crt_free(ca_chain);
+      delete ca_chain;
+      return nullptr;
+    }
+  }
+
+  return static_cast<ca_store_t>(ca_chain);
+}
+
+inline void free_ca_store(ca_store_t store) {
+  if (store) {
+    auto *ca_chain = static_cast<mbedtls_x509_crt *>(store);
+    mbedtls_x509_crt_free(ca_chain);
+    delete ca_chain;
+  }
+}
+
+inline bool set_ca_store(ctx_t ctx, ca_store_t store) {
+  if (!ctx || !store) { return false; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+  auto *ca_chain = static_cast<mbedtls_x509_crt *>(store);
+
+  // Free existing CA chain
+  mbedtls_x509_crt_free(&mbed_ctx->ca_chain);
+  mbedtls_x509_crt_init(&mbed_ctx->ca_chain);
+
+  // Copy the CA chain (deep copy)
+  // Parse from the raw data of the source cert
+  mbedtls_x509_crt *src = ca_chain;
+  while (src != nullptr) {
+    int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p,
+                                         src->raw.len);
+    if (ret != 0) { return false; }
+    src = src->next;
+  }
+
+  // Update the SSL config to use the new CA chain
+  mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);
+  return true;
+}
+
+inline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {
+  certs.clear();
+  if (!ctx) { return 0; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Iterate through the CA chain
+  mbedtls_x509_crt *cert = &mbed_ctx->ca_chain;
+  while (cert != nullptr && cert->raw.len > 0) {
+    // Create a copy of the certificate for the caller
+    auto *copy = new mbedtls_x509_crt;
+    mbedtls_x509_crt_init(copy);
+    int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len);
+    if (ret == 0) {
+      certs.push_back(static_cast<cert_t>(copy));
+    } else {
+      mbedtls_x509_crt_free(copy);
+      delete copy;
+    }
+    cert = cert->next;
+  }
+  return certs.size();
+}
+
+inline std::vector<std::string> get_ca_names(ctx_t ctx) {
+  std::vector<std::string> names;
+  if (!ctx) { return names; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Iterate through the CA chain
+  mbedtls_x509_crt *cert = &mbed_ctx->ca_chain;
+  while (cert != nullptr && cert->raw.len > 0) {
+    char buf[512];
+    int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject);
+    if (ret > 0) { names.push_back(buf); }
+    cert = cert->next;
+  }
+  return names;
+}
+
+inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
+                               const char *key_pem, const char *password) {
+  if (!ctx || !cert_pem || !key_pem) { return false; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Free existing certificate and key
+  mbedtls_x509_crt_free(&mbed_ctx->own_cert);
+  mbedtls_pk_free(&mbed_ctx->own_key);
+  mbedtls_x509_crt_init(&mbed_ctx->own_cert);
+  mbedtls_pk_init(&mbed_ctx->own_key);
+
+  // Parse certificate PEM
+  int ret = mbedtls_x509_crt_parse(
+      &mbed_ctx->own_cert, reinterpret_cast<const unsigned char *>(cert_pem),
+      strlen(cert_pem) + 1);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Parse private key PEM
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+  ret = mbedtls_pk_parse_key(
+      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),
+      strlen(key_pem) + 1,
+      password ? reinterpret_cast<const unsigned char *>(password) : nullptr,
+      password ? strlen(password) : 0, mbedtls_ctr_drbg_random,
+      &mbed_ctx->ctr_drbg);
+#else
+  ret = mbedtls_pk_parse_key(
+      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),
+      strlen(key_pem) + 1,
+      password ? reinterpret_cast<const unsigned char *>(password) : nullptr,
+      password ? strlen(password) : 0);
+#endif
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Configure SSL to use the new certificate and key
+  ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert,
+                                  &mbed_ctx->own_key);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  return true;
+}
+
+inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
+  if (!ctx || !ca_pem) { return false; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  // Free existing CA chain
+  mbedtls_x509_crt_free(&mbed_ctx->ca_chain);
+  mbedtls_x509_crt_init(&mbed_ctx->ca_chain);
+
+  // Parse CA PEM
+  int ret = mbedtls_x509_crt_parse(
+      &mbed_ctx->ca_chain, reinterpret_cast<const unsigned char *>(ca_pem),
+      strlen(ca_pem) + 1);
+  if (ret != 0) {
+    impl::mbedtls_last_error() = ret;
+    return false;
+  }
+
+  // Update SSL config to use new CA chain
+  mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);
+  return true;
+}
+
+inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {
+  if (!ctx) { return false; }
+  auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+  impl::get_verify_callback() = std::move(callback);
+  mbed_ctx->has_verify_callback =
+      static_cast<bool>(impl::get_verify_callback());
+
+  if (mbed_ctx->has_verify_callback) {
+    // Set OPTIONAL mode to ensure callback is called even when verification
+    // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set)
+    mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
+    mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback,
+                            nullptr);
+  } else {
+    mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr);
+  }
+  return true;
+}
+
+inline long get_verify_error(const_session_t session) {
+  if (!session) { return -1; }
+  auto *msession =
+      static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+  return static_cast<long>(mbedtls_ssl_get_verify_result(&msession->ssl));
+}
+
+inline std::string verify_error_string(long error_code) {
+  if (error_code == 0) { return ""; }
+  char buf[256];
+  mbedtls_x509_crt_verify_info(buf, sizeof(buf), "",
+                               static_cast<uint32_t>(error_code));
+  // Remove trailing newline if present
+  std::string result(buf);
+  while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) {
+    result.pop_back();
+  }
+  return result;
+}
+
+} // namespace tls
+
+#endif // CPPHTTPLIB_MBEDTLS_SUPPORT
+
+/*
+ * Group 10: TLS abstraction layer - wolfSSL backend
+ */
+
+/*
+ * wolfSSL Backend Implementation
+ */
+
+#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT
+namespace tls {
+
+namespace impl {
+
+// wolfSSL session wrapper
+struct WolfSSLSession {
+  WOLFSSL *ssl = nullptr;
+  socket_t sock = INVALID_SOCKET;
+  std::string hostname;     // For client: set via set_sni
+  std::string sni_hostname; // For server: received from client via SNI callback
+
+  WolfSSLSession() = default;
+
+  ~WolfSSLSession() {
+    if (ssl) { wolfSSL_free(ssl); }
+  }
+
+  WolfSSLSession(const WolfSSLSession &) = delete;
+  WolfSSLSession &operator=(const WolfSSLSession &) = delete;
+};
+
+// Thread-local error code accessor for wolfSSL
+inline uint64_t &wolfssl_last_error() {
+  static thread_local uint64_t err = 0;
+  return err;
+}
+
+// Helper to map wolfSSL error to ErrorCode.
+// ssl_error is the value from wolfSSL_get_error().
+// raw_ret is the raw return value from the wolfSSL call (for low-level error).
+inline ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error,
+                                   int &out_errno) {
+  switch (ssl_error) {
+  case SSL_ERROR_NONE: return ErrorCode::Success;
+  case SSL_ERROR_WANT_READ: return ErrorCode::WantRead;
+  case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;
+  case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
+  case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
+  default:
+    if (ssl) {
+      // wolfSSL stores the low-level error code as a negative value.
+      // DOMAIN_NAME_MISMATCH (-322) indicates hostname verification failure.
+      int low_err = ssl_error; // wolfSSL_get_error returns the low-level code
+      if (low_err == DOMAIN_NAME_MISMATCH) {
+        return ErrorCode::HostnameMismatch;
+      }
+      // Check verify result to distinguish cert verification from generic SSL
+      // errors.
+      long vr = wolfSSL_get_verify_result(ssl);
+      if (vr != 0) { return ErrorCode::CertVerifyFailed; }
+    }
+    return ErrorCode::Fatal;
+  }
+}
+
+// WolfSSLContext constructor/destructor implementations
+inline WolfSSLContext::WolfSSLContext() { wolfSSL_Init(); }
+
+inline WolfSSLContext::~WolfSSLContext() {
+  if (ctx) { wolfSSL_CTX_free(ctx); }
+}
+
+// Thread-local storage for SNI captured during handshake
+inline std::string &wolfssl_pending_sni() {
+  static thread_local std::string sni;
+  return sni;
+}
+
+// SNI callback for wolfSSL server to capture client's SNI hostname
+inline int wolfssl_sni_callback(WOLFSSL *ssl, int *ret, void *exArg) {
+  (void)ret;
+  (void)exArg;
+
+  void *name_data = nullptr;
+  unsigned short name_len =
+      wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, &name_data);
+
+  if (name_data && name_len > 0) {
+    wolfssl_pending_sni().assign(static_cast<const char *>(name_data),
+                                 name_len);
+  } else {
+    wolfssl_pending_sni().clear();
+  }
+  return 0; // Continue regardless
+}
+
+// wolfSSL verify callback wrapper
+inline int wolfssl_verify_callback(int preverify_ok,
+                                   WOLFSSL_X509_STORE_CTX *x509_ctx) {
+  auto &callback = get_verify_callback();
+  if (!callback) { return preverify_ok; }
+
+  WOLFSSL_X509 *cert = wolfSSL_X509_STORE_CTX_get_current_cert(x509_ctx);
+  int depth = wolfSSL_X509_STORE_CTX_get_error_depth(x509_ctx);
+  int err = wolfSSL_X509_STORE_CTX_get_error(x509_ctx);
+
+  // Get the WOLFSSL object from the X509_STORE_CTX
+  WOLFSSL *ssl = static_cast<WOLFSSL *>(wolfSSL_X509_STORE_CTX_get_ex_data(
+      x509_ctx, wolfSSL_get_ex_data_X509_STORE_CTX_idx()));
+
+  VerifyContext verify_ctx;
+  verify_ctx.session = static_cast<session_t>(ssl);
+  verify_ctx.cert = static_cast<cert_t>(cert);
+  verify_ctx.depth = depth;
+  verify_ctx.preverify_ok = (preverify_ok != 0);
+  verify_ctx.error_code = static_cast<long>(err);
+
+  if (err != 0) {
+    verify_ctx.error_string = wolfSSL_X509_verify_cert_error_string(err);
+  } else {
+    verify_ctx.error_string = nullptr;
+  }
+
+  bool accepted = callback(verify_ctx);
+  return accepted ? 1 : 0;
+}
+
+inline void set_wolfssl_password_cb(WOLFSSL_CTX *ctx, const char *password) {
+  wolfSSL_CTX_set_default_passwd_cb_userdata(ctx, const_cast<char *>(password));
+  wolfSSL_CTX_set_default_passwd_cb(
+      ctx, [](char *buf, int size, int /*rwflag*/, void *userdata) -> int {
+        auto *pwd = static_cast<const char *>(userdata);
+        if (!pwd) return 0;
+        auto len = static_cast<int>(strlen(pwd));
+        if (len > size) len = size;
+        memcpy(buf, pwd, static_cast<size_t>(len));
+        return len;
+      });
+}
+
+} // namespace impl
+
+inline ctx_t create_client_context() {
+  auto ctx = new (std::nothrow) impl::WolfSSLContext();
+  if (!ctx) { return nullptr; }
+
+  ctx->is_server = false;
+
+  WOLFSSL_METHOD *method = wolfTLSv1_2_client_method();
+  if (!method) {
+    delete ctx;
+    return nullptr;
+  }
+
+  ctx->ctx = wolfSSL_CTX_new(method);
+  if (!ctx->ctx) {
+    delete ctx;
+    return nullptr;
+  }
+
+  // Default: verify peer certificate
+  wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_PEER, nullptr);
+
+  return static_cast<ctx_t>(ctx);
+}
+
+inline ctx_t create_server_context() {
+  auto ctx = new (std::nothrow) impl::WolfSSLContext();
+  if (!ctx) { return nullptr; }
+
+  ctx->is_server = true;
+
+  WOLFSSL_METHOD *method = wolfTLSv1_2_server_method();
+  if (!method) {
+    delete ctx;
+    return nullptr;
+  }
+
+  ctx->ctx = wolfSSL_CTX_new(method);
+  if (!ctx->ctx) {
+    delete ctx;
+    return nullptr;
+  }
+
+  // Default: don't verify client
+  wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_NONE, nullptr);
+
+  // Enable SNI on server
+  wolfSSL_CTX_SNI_SetOptions(ctx->ctx, WOLFSSL_SNI_HOST_NAME,
+                             WOLFSSL_SNI_CONTINUE_ON_MISMATCH);
+  wolfSSL_CTX_set_servername_callback(ctx->ctx, impl::wolfssl_sni_callback);
+
+  return static_cast<ctx_t>(ctx);
+}
+
+inline void free_context(ctx_t ctx) {
+  if (ctx) { delete static_cast<impl::WolfSSLContext *>(ctx); }
+}
+
+inline bool set_min_version(ctx_t ctx, Version version) {
+  if (!ctx) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  int min_ver = WOLFSSL_TLSV1_2;
+  if (version >= Version::TLS1_3) { min_ver = WOLFSSL_TLSV1_3; }
+
+  return wolfSSL_CTX_SetMinVersion(wctx->ctx, min_ver) == WOLFSSL_SUCCESS;
+}
+
+inline bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) {
+  if (!ctx || !pem) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  int ret = wolfSSL_CTX_load_verify_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(pem),
+      static_cast<long>(len), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+  wctx->ca_pem_data_.append(pem, len);
+  return true;
+}
+
+inline bool load_ca_file(ctx_t ctx, const char *file_path) {
+  if (!ctx || !file_path) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, file_path, nullptr);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+  return true;
+}
+
+inline bool load_ca_dir(ctx_t ctx, const char *dir_path) {
+  if (!ctx || !dir_path) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, dir_path);
+  // wolfSSL may fail if the directory doesn't contain properly hashed certs.
+  // Unlike OpenSSL which lazily loads certs from directories, wolfSSL scans
+  // immediately. Return true even on failure since the CA file may have
+  // already been loaded, matching OpenSSL's lenient behavior.
+  (void)ret;
+  return true;
+}
+
+inline bool load_system_certs(ctx_t ctx) {
+  if (!ctx) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+  bool loaded = false;
+
+#ifdef _WIN32
+  loaded = impl::enumerate_windows_system_certs(
+      [&](const unsigned char *data, size_t len) {
+        return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data,
+                                              static_cast<long>(len),
+                                              SSL_FILETYPE_ASN1) == SSL_SUCCESS;
+      });
+#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
+  loaded = impl::enumerate_macos_keychain_certs(
+      [&](const unsigned char *data, size_t len) {
+        return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data,
+                                              static_cast<long>(len),
+                                              SSL_FILETYPE_ASN1) == SSL_SUCCESS;
+      });
+#else
+  for (auto path = impl::system_ca_paths(); *path; ++path) {
+    if (wolfSSL_CTX_load_verify_locations(wctx->ctx, *path, nullptr) ==
+        SSL_SUCCESS) {
+      loaded = true;
+      break;
+    }
+  }
+
+  if (!loaded) {
+    for (auto dir = impl::system_ca_dirs(); *dir; ++dir) {
+      if (wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, *dir) ==
+          SSL_SUCCESS) {
+        loaded = true;
+        break;
+      }
+    }
+  }
+#endif
+
+  return loaded;
+}
+
+inline bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,
+                                const char *password) {
+  if (!ctx || !cert || !key) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  // Load certificate
+  int ret = wolfSSL_CTX_use_certificate_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(cert),
+      static_cast<long>(strlen(cert)), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Set password callback if password is provided
+  if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); }
+
+  // Load private key
+  ret = wolfSSL_CTX_use_PrivateKey_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(key),
+      static_cast<long>(strlen(key)), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Verify that the certificate and private key match
+  return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS;
+}
+
+inline bool set_client_cert_file(ctx_t ctx, const char *cert_path,
+                                 const char *key_path, const char *password) {
+  if (!ctx || !cert_path || !key_path) { return false; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  // Load certificate file
+  int ret =
+      wolfSSL_CTX_use_certificate_file(wctx->ctx, cert_path, SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Set password callback if password is provided
+  if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); }
+
+  // Load private key file
+  ret = wolfSSL_CTX_use_PrivateKey_file(wctx->ctx, key_path, SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Verify that the certificate and private key match
+  return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS;
+}
+
+inline void set_verify_client(ctx_t ctx, bool require) {
+  if (!ctx) { return; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+  wctx->verify_client = require;
+  if (require) {
+    wolfSSL_CTX_set_verify(
+        wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
+        wctx->has_verify_callback ? impl::wolfssl_verify_callback : nullptr);
+  } else {
+    if (wctx->has_verify_callback) {
+      wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER,
+                             impl::wolfssl_verify_callback);
+    } else {
+      wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_NONE, nullptr);
+    }
+  }
+}
+
+inline session_t create_session(ctx_t ctx, socket_t sock) {
+  if (!ctx || sock == INVALID_SOCKET) { return nullptr; }
+  auto wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  auto session = new (std::nothrow) impl::WolfSSLSession();
+  if (!session) { return nullptr; }
+
+  session->sock = sock;
+  session->ssl = wolfSSL_new(wctx->ctx);
+  if (!session->ssl) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    delete session;
+    return nullptr;
+  }
+
+  wolfSSL_set_fd(session->ssl, static_cast<int>(sock));
+
+  return static_cast<session_t>(session);
+}
+
+inline void free_session(session_t session) {
+  if (session) { delete static_cast<impl::WolfSSLSession *>(session); }
+}
+
+inline bool set_sni(session_t session, const char *hostname) {
+  if (!session || !hostname) { return false; }
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+
+  int ret = wolfSSL_UseSNI(wsession->ssl, WOLFSSL_SNI_HOST_NAME, hostname,
+                           static_cast<word16>(strlen(hostname)));
+  if (ret != WOLFSSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Also set hostname for verification
+  wolfSSL_check_domain_name(wsession->ssl, hostname);
+
+  wsession->hostname = hostname;
+  return true;
+}
+
+inline bool set_hostname(session_t session, const char *hostname) {
+  // In wolfSSL, set_hostname also sets up hostname verification
+  return set_sni(session, hostname);
+}
+
+inline TlsError connect(session_t session) {
+  TlsError err;
+  if (!session) {
+    err.code = ErrorCode::Fatal;
+    return err;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+  int ret = wolfSSL_connect(wsession->ssl);
+
+  if (ret == SSL_SUCCESS) {
+    err.code = ErrorCode::Success;
+  } else {
+    int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+    err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
+    err.backend_code = static_cast<uint64_t>(ssl_error);
+    impl::wolfssl_last_error() = err.backend_code;
+  }
+
+  return err;
+}
+
+inline TlsError accept(session_t session) {
+  TlsError err;
+  if (!session) {
+    err.code = ErrorCode::Fatal;
+    return err;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+  int ret = wolfSSL_accept(wsession->ssl);
+
+  if (ret == SSL_SUCCESS) {
+    err.code = ErrorCode::Success;
+    // Capture SNI from thread-local storage after successful handshake
+    wsession->sni_hostname = std::move(impl::wolfssl_pending_sni());
+    impl::wolfssl_pending_sni().clear();
+  } else {
+    int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+    err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
+    err.backend_code = static_cast<uint64_t>(ssl_error);
+    impl::wolfssl_last_error() = err.backend_code;
+  }
+
+  return err;
+}
+
+inline bool connect_nonblocking(session_t session, socket_t sock,
+                                time_t timeout_sec, time_t timeout_usec,
+                                TlsError *err) {
+  if (!session) {
+    if (err) { err->code = ErrorCode::Fatal; }
+    return false;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+
+  // Set socket to non-blocking mode
+  detail::set_nonblocking(sock, true);
+  auto cleanup =
+      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  int ret;
+  while ((ret = wolfSSL_connect(wsession->ssl)) != SSL_SUCCESS) {
+    int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+    if (ssl_error == SSL_ERROR_WANT_READ) {
+      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    } else if (ssl_error == SSL_ERROR_WANT_WRITE) {
+      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    }
+
+    // Error or timeout
+    if (err) {
+      err->code =
+          impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno);
+      err->backend_code = static_cast<uint64_t>(ssl_error);
+    }
+    impl::wolfssl_last_error() = static_cast<uint64_t>(ssl_error);
+    return false;
+  }
+
+  if (err) { err->code = ErrorCode::Success; }
+  return true;
+}
+
+inline bool accept_nonblocking(session_t session, socket_t sock,
+                               time_t timeout_sec, time_t timeout_usec,
+                               TlsError *err) {
+  if (!session) {
+    if (err) { err->code = ErrorCode::Fatal; }
+    return false;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+
+  // Set socket to non-blocking mode
+  detail::set_nonblocking(sock, true);
+  auto cleanup =
+      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  int ret;
+  while ((ret = wolfSSL_accept(wsession->ssl)) != SSL_SUCCESS) {
+    int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+    if (ssl_error == SSL_ERROR_WANT_READ) {
+      if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    } else if (ssl_error == SSL_ERROR_WANT_WRITE) {
+      if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) {
+        continue;
+      }
+    }
+
+    // Error or timeout
+    if (err) {
+      err->code =
+          impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno);
+      err->backend_code = static_cast<uint64_t>(ssl_error);
+    }
+    impl::wolfssl_last_error() = static_cast<uint64_t>(ssl_error);
+    return false;
+  }
+
+  if (err) { err->code = ErrorCode::Success; }
+
+  // Capture SNI from thread-local storage after successful handshake
+  wsession->sni_hostname = std::move(impl::wolfssl_pending_sni());
+  impl::wolfssl_pending_sni().clear();
+
+  return true;
+}
+
+inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+  int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len));
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return static_cast<ssize_t>(ret);
+  }
+
+  if (ret == 0) {
+    err.code = ErrorCode::PeerClosed;
+    return 0;
+  }
+
+  int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+  err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
+  err.backend_code = static_cast<uint64_t>(ssl_error);
+  impl::wolfssl_last_error() = err.backend_code;
+  return -1;
+}
+
+inline ssize_t write(session_t session, const void *buf, size_t len,
+                     TlsError &err) {
+  if (!session || !buf) {
+    err.code = ErrorCode::Fatal;
+    return -1;
+  }
+
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+  int ret = wolfSSL_write(wsession->ssl, buf, static_cast<int>(len));
+
+  if (ret > 0) {
+    err.code = ErrorCode::Success;
+    return static_cast<ssize_t>(ret);
+  }
+
+  // wolfSSL_write returns 0 when the peer has sent a close_notify.
+  // Treat this as an error (return -1) so callers don't spin in a
+  // write loop adding zero to the offset.
+  if (ret == 0) {
+    err.code = ErrorCode::PeerClosed;
+    return -1;
+  }
+
+  int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+  err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
+  err.backend_code = static_cast<uint64_t>(ssl_error);
+  impl::wolfssl_last_error() = err.backend_code;
+  return -1;
+}
+
+inline int pending(const_session_t session) {
+  if (!session) { return 0; }
+  auto wsession =
+      static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
+  return wolfSSL_pending(wsession->ssl);
+}
+
+inline void shutdown(session_t session, bool graceful) {
+  if (!session) { return; }
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+
+  if (graceful) {
+    int ret;
+    int attempts = 0;
+    while ((ret = wolfSSL_shutdown(wsession->ssl)) != SSL_SUCCESS &&
+           attempts < 3) {
+      int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+      if (ssl_error != SSL_ERROR_WANT_READ &&
+          ssl_error != SSL_ERROR_WANT_WRITE) {
+        break;
+      }
+      attempts++;
+    }
+  } else {
+    wolfSSL_shutdown(wsession->ssl);
+  }
+}
+
+inline bool is_peer_closed(session_t session, socket_t sock) {
+  if (!session || sock == INVALID_SOCKET) { return true; }
+  auto wsession = static_cast<impl::WolfSSLSession *>(session);
+
+  // Check if there's already decrypted data available
+  if (wolfSSL_pending(wsession->ssl) > 0) { return false; }
+
+  // Set socket to non-blocking to avoid blocking on read
+  detail::set_nonblocking(sock, true);
+  auto cleanup =
+      detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
+
+  // Peek 1 byte to check connection status without consuming data
+  unsigned char buf;
+  int ret = wolfSSL_peek(wsession->ssl, &buf, 1);
+
+  // If we got data or WANT_READ (would block), connection is alive
+  if (ret > 0) { return false; }
+
+  int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
+  if (ssl_error == SSL_ERROR_WANT_READ) { return false; }
+
+  return ssl_error == SSL_ERROR_ZERO_RETURN || ssl_error == SSL_ERROR_SYSCALL ||
+         ret == 0;
+}
+
+inline cert_t get_peer_cert(const_session_t session) {
+  if (!session) { return nullptr; }
+  auto wsession =
+      static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
+
+  WOLFSSL_X509 *cert = wolfSSL_get_peer_certificate(wsession->ssl);
+  return static_cast<cert_t>(cert);
+}
+
+inline void free_cert(cert_t cert) {
+  if (cert) { wolfSSL_X509_free(static_cast<WOLFSSL_X509 *>(cert)); }
+}
+
+inline bool verify_hostname(cert_t cert, const char *hostname) {
+  if (!cert || !hostname) { return false; }
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+  std::string host_str(hostname);
+
+  // Check if hostname is an IP address
+  bool is_ip = impl::is_ipv4_address(host_str);
+  unsigned char ip_bytes[4];
+  if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
+
+  // Check Subject Alternative Names
+  auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
+      wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
+
+  if (san_names) {
+    int san_count = wolfSSL_sk_num(san_names);
+    for (int i = 0; i < san_count; i++) {
+      auto *names =
+          static_cast<WOLFSSL_GENERAL_NAME *>(wolfSSL_sk_value(san_names, i));
+      if (!names) continue;
+
+      if (!is_ip && names->type == WOLFSSL_GEN_DNS) {
+        // DNS name
+        unsigned char *dns_name = nullptr;
+        int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName);
+        if (dns_name && dns_len > 0) {
+          std::string san_name(reinterpret_cast<char *>(dns_name),
+                               static_cast<size_t>(dns_len));
+          XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL);
+          if (detail::match_hostname(san_name, host_str)) {
+            wolfSSL_sk_free(san_names);
+            return true;
+          }
+        }
+      } else if (is_ip && names->type == WOLFSSL_GEN_IPADD) {
+        // IP address
+        unsigned char *ip_data = wolfSSL_ASN1_STRING_data(names->d.iPAddress);
+        int ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress);
+        if (ip_data && ip_len == 4 && memcmp(ip_data, ip_bytes, 4) == 0) {
+          wolfSSL_sk_free(san_names);
+          return true;
+        }
+      }
+    }
+    wolfSSL_sk_free(san_names);
+  }
+
+  // Fallback: Check Common Name (CN) in subject
+  WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509);
+  if (subject) {
+    char cn[256] = {};
+    int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn,
+                                                   sizeof(cn));
+    if (cn_len > 0) {
+      std::string cn_str(cn, static_cast<size_t>(cn_len));
+      if (detail::match_hostname(cn_str, host_str)) { return true; }
+    }
+  }
+
+  return false;
+}
+
+inline uint64_t hostname_mismatch_code() {
+  return static_cast<uint64_t>(DOMAIN_NAME_MISMATCH);
+}
+
+inline long get_verify_result(const_session_t session) {
+  if (!session) { return -1; }
+  auto wsession =
+      static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
+  long result = wolfSSL_get_verify_result(wsession->ssl);
+  return result;
+}
+
+inline std::string get_cert_subject_cn(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509);
+  if (!subject) return "";
+
+  char cn[256] = {};
+  int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn,
+                                                 sizeof(cn));
+  if (cn_len <= 0) return "";
+  return std::string(cn, static_cast<size_t>(cn_len));
+}
+
+inline std::string get_cert_issuer_name(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  WOLFSSL_X509_NAME *issuer = wolfSSL_X509_get_issuer_name(x509);
+  if (!issuer) return "";
+
+  char *name_str = wolfSSL_X509_NAME_oneline(issuer, nullptr, 0);
+  if (!name_str) return "";
+
+  std::string result(name_str);
+  XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL);
+  return result;
+}
+
+inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
+  sans.clear();
+  if (!cert) return false;
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
+      wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
+  if (!san_names) return true; // No SANs is not an error
+
+  int count = wolfSSL_sk_num(san_names);
+  for (int i = 0; i < count; i++) {
+    auto *name =
+        static_cast<WOLFSSL_GENERAL_NAME *>(wolfSSL_sk_value(san_names, i));
+    if (!name) continue;
+
+    SanEntry entry;
+    switch (name->type) {
+    case WOLFSSL_GEN_DNS: {
+      entry.type = SanType::DNS;
+      unsigned char *dns_name = nullptr;
+      int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, name->d.dNSName);
+      if (dns_name && dns_len > 0) {
+        entry.value = std::string(reinterpret_cast<char *>(dns_name),
+                                  static_cast<size_t>(dns_len));
+        XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL);
+      }
+      break;
+    }
+    case WOLFSSL_GEN_IPADD: {
+      entry.type = SanType::IP;
+      unsigned char *ip_data = wolfSSL_ASN1_STRING_data(name->d.iPAddress);
+      int ip_len = wolfSSL_ASN1_STRING_length(name->d.iPAddress);
+      if (ip_data && ip_len == 4) {
+        char buf[16];
+        snprintf(buf, sizeof(buf), "%d.%d.%d.%d", ip_data[0], ip_data[1],
+                 ip_data[2], ip_data[3]);
+        entry.value = buf;
+      } else if (ip_data && ip_len == 16) {
+        char buf[64];
+        snprintf(buf, sizeof(buf),
+                 "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
+                 "%02x%02x:%02x%02x:%02x%02x:%02x%02x",
+                 ip_data[0], ip_data[1], ip_data[2], ip_data[3], ip_data[4],
+                 ip_data[5], ip_data[6], ip_data[7], ip_data[8], ip_data[9],
+                 ip_data[10], ip_data[11], ip_data[12], ip_data[13],
+                 ip_data[14], ip_data[15]);
+        entry.value = buf;
+      }
+      break;
+    }
+    case WOLFSSL_GEN_EMAIL:
+      entry.type = SanType::EMAIL;
+      {
+        unsigned char *email = nullptr;
+        int email_len = wolfSSL_ASN1_STRING_to_UTF8(&email, name->d.rfc822Name);
+        if (email && email_len > 0) {
+          entry.value = std::string(reinterpret_cast<char *>(email),
+                                    static_cast<size_t>(email_len));
+          XFREE(email, nullptr, DYNAMIC_TYPE_OPENSSL);
+        }
+      }
+      break;
+    case WOLFSSL_GEN_URI:
+      entry.type = SanType::URI;
+      {
+        unsigned char *uri = nullptr;
+        int uri_len = wolfSSL_ASN1_STRING_to_UTF8(
+            &uri, name->d.uniformResourceIdentifier);
+        if (uri && uri_len > 0) {
+          entry.value = std::string(reinterpret_cast<char *>(uri),
+                                    static_cast<size_t>(uri_len));
+          XFREE(uri, nullptr, DYNAMIC_TYPE_OPENSSL);
+        }
+      }
+      break;
+    default: entry.type = SanType::OTHER; break;
+    }
+
+    if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
+  }
+  wolfSSL_sk_free(san_names);
+  return true;
+}
+
+inline bool get_cert_validity(cert_t cert, time_t &not_before,
+                              time_t &not_after) {
+  if (!cert) return false;
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  const WOLFSSL_ASN1_TIME *nb = wolfSSL_X509_get_notBefore(x509);
+  const WOLFSSL_ASN1_TIME *na = wolfSSL_X509_get_notAfter(x509);
+
+  if (!nb || !na) return false;
+
+  // wolfSSL_ASN1_TIME_to_tm is available
+  struct tm tm_nb = {}, tm_na = {};
+  if (wolfSSL_ASN1_TIME_to_tm(nb, &tm_nb) != WOLFSSL_SUCCESS) return false;
+  if (wolfSSL_ASN1_TIME_to_tm(na, &tm_na) != WOLFSSL_SUCCESS) return false;
+
+#ifdef _WIN32
+  not_before = _mkgmtime(&tm_nb);
+  not_after = _mkgmtime(&tm_na);
+#else
+  not_before = timegm(&tm_nb);
+  not_after = timegm(&tm_na);
+#endif
+  return true;
+}
+
+inline std::string get_cert_serial(cert_t cert) {
+  if (!cert) return "";
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  WOLFSSL_ASN1_INTEGER *serial_asn1 = wolfSSL_X509_get_serialNumber(x509);
+  if (!serial_asn1) return "";
+
+  // Get the serial number data
+  int len = serial_asn1->length;
+  unsigned char *data = serial_asn1->data;
+  if (!data || len <= 0) return "";
+
+  std::string result;
+  result.reserve(static_cast<size_t>(len) * 2);
+  for (int i = 0; i < len; i++) {
+    char hex[3];
+    snprintf(hex, sizeof(hex), "%02X", data[i]);
+    result += hex;
+  }
+  return result;
+}
+
+inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+  if (!cert) return false;
+  auto x509 = static_cast<WOLFSSL_X509 *>(cert);
+
+  int der_len = 0;
+  const unsigned char *der_data = wolfSSL_X509_get_der(x509, &der_len);
+  if (!der_data || der_len <= 0) return false;
+
+  der.assign(der_data, der_data + der_len);
+  return true;
+}
+
+inline const char *get_sni(const_session_t session) {
+  if (!session) return nullptr;
+  auto wsession = static_cast<const impl::WolfSSLSession *>(session);
+
+  // For server: return SNI received from client during handshake
+  if (!wsession->sni_hostname.empty()) {
+    return wsession->sni_hostname.c_str();
+  }
+
+  // For client: return the hostname set via set_sni
+  if (!wsession->hostname.empty()) { return wsession->hostname.c_str(); }
+
+  return nullptr;
+}
+
+inline uint64_t peek_error() {
+  return static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+}
+
+inline uint64_t get_error() {
+  uint64_t err = impl::wolfssl_last_error();
+  impl::wolfssl_last_error() = 0;
+  return err;
+}
+
+inline std::string error_string(uint64_t code) {
+  char buf[256];
+  wolfSSL_ERR_error_string(static_cast<unsigned long>(code), buf);
+  return std::string(buf);
+}
+
+inline ca_store_t create_ca_store(const char *pem, size_t len) {
+  if (!pem || len == 0) { return nullptr; }
+  // Validate by attempting to load into a temporary ctx
+  WOLFSSL_CTX *tmp_ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method());
+  if (!tmp_ctx) { return nullptr; }
+  int ret = wolfSSL_CTX_load_verify_buffer(
+      tmp_ctx, reinterpret_cast<const unsigned char *>(pem),
+      static_cast<long>(len), SSL_FILETYPE_PEM);
+  wolfSSL_CTX_free(tmp_ctx);
+  if (ret != SSL_SUCCESS) { return nullptr; }
+  return static_cast<ca_store_t>(
+      new impl::WolfSSLCAStore{std::string(pem, len)});
+}
+
+inline void free_ca_store(ca_store_t store) {
+  delete static_cast<impl::WolfSSLCAStore *>(store);
+}
+
+inline bool set_ca_store(ctx_t ctx, ca_store_t store) {
+  if (!ctx || !store) { return false; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+  auto *ca = static_cast<impl::WolfSSLCAStore *>(store);
+  int ret = wolfSSL_CTX_load_verify_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(ca->pem_data.data()),
+      static_cast<long>(ca->pem_data.size()), SSL_FILETYPE_PEM);
+  if (ret == SSL_SUCCESS) { wctx->ca_pem_data_ += ca->pem_data; }
+  return ret == SSL_SUCCESS;
+}
+
+inline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {
+  certs.clear();
+  if (!ctx) { return 0; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+  if (wctx->ca_pem_data_.empty()) { return 0; }
+
+  const std::string &pem = wctx->ca_pem_data_;
+  const std::string begin_marker = "-----BEGIN CERTIFICATE-----";
+  const std::string end_marker = "-----END CERTIFICATE-----";
+  size_t pos = 0;
+  while ((pos = pem.find(begin_marker, pos)) != std::string::npos) {
+    size_t end_pos = pem.find(end_marker, pos);
+    if (end_pos == std::string::npos) { break; }
+    end_pos += end_marker.size();
+    std::string cert_pem = pem.substr(pos, end_pos - pos);
+    WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer(
+        reinterpret_cast<const unsigned char *>(cert_pem.data()),
+        static_cast<int>(cert_pem.size()), WOLFSSL_FILETYPE_PEM);
+    if (x509) { certs.push_back(static_cast<cert_t>(x509)); }
+    pos = end_pos;
+  }
+  return certs.size();
+}
+
+inline std::vector<std::string> get_ca_names(ctx_t ctx) {
+  std::vector<std::string> names;
+  if (!ctx) { return names; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+  if (wctx->ca_pem_data_.empty()) { return names; }
+
+  const std::string &pem = wctx->ca_pem_data_;
+  const std::string begin_marker = "-----BEGIN CERTIFICATE-----";
+  const std::string end_marker = "-----END CERTIFICATE-----";
+  size_t pos = 0;
+  while ((pos = pem.find(begin_marker, pos)) != std::string::npos) {
+    size_t end_pos = pem.find(end_marker, pos);
+    if (end_pos == std::string::npos) { break; }
+    end_pos += end_marker.size();
+    std::string cert_pem = pem.substr(pos, end_pos - pos);
+    WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer(
+        reinterpret_cast<const unsigned char *>(cert_pem.data()),
+        static_cast<int>(cert_pem.size()), WOLFSSL_FILETYPE_PEM);
+    if (x509) {
+      WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509);
+      if (subject) {
+        char *name_str = wolfSSL_X509_NAME_oneline(subject, nullptr, 0);
+        if (name_str) {
+          names.push_back(name_str);
+          XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL);
+        }
+      }
+      wolfSSL_X509_free(x509);
+    }
+    pos = end_pos;
+  }
+  return names;
+}
+
+inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
+                               const char *key_pem, const char *password) {
+  if (!ctx || !cert_pem || !key_pem) { return false; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  // Load new certificate
+  int ret = wolfSSL_CTX_use_certificate_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(cert_pem),
+      static_cast<long>(strlen(cert_pem)), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  // Set password if provided
+  if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); }
+
+  // Load new private key
+  ret = wolfSSL_CTX_use_PrivateKey_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(key_pem),
+      static_cast<long>(strlen(key_pem)), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+
+  return true;
+}
+
+inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
+  if (!ctx || !ca_pem) { return false; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  int ret = wolfSSL_CTX_load_verify_buffer(
+      wctx->ctx, reinterpret_cast<const unsigned char *>(ca_pem),
+      static_cast<long>(strlen(ca_pem)), SSL_FILETYPE_PEM);
+  if (ret != SSL_SUCCESS) {
+    impl::wolfssl_last_error() =
+        static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+    return false;
+  }
+  return true;
+}
+
+inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {
+  if (!ctx) { return false; }
+  auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
+
+  impl::get_verify_callback() = std::move(callback);
+  wctx->has_verify_callback = static_cast<bool>(impl::get_verify_callback());
+
+  if (wctx->has_verify_callback) {
+    wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER,
+                           impl::wolfssl_verify_callback);
+  } else {
+    wolfSSL_CTX_set_verify(
+        wctx->ctx,
+        wctx->verify_client
+            ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
+            : SSL_VERIFY_NONE,
+        nullptr);
+  }
+  return true;
+}
+
+inline long get_verify_error(const_session_t session) {
+  if (!session) { return -1; }
+  auto *wsession =
+      static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
+  return wolfSSL_get_verify_result(wsession->ssl);
+}
+
+inline std::string verify_error_string(long error_code) {
+  if (error_code == 0) { return ""; }
+  const char *str =
+      wolfSSL_X509_verify_cert_error_string(static_cast<int>(error_code));
+  return str ? std::string(str) : std::string();
+}
+
+} // namespace tls
+
+#endif // CPPHTTPLIB_WOLFSSL_SUPPORT
+
+// WebSocket implementation
+namespace ws {
+
+inline bool WebSocket::send_frame(Opcode op, const char *data, size_t len,
+                                  bool fin) {
+  std::lock_guard<std::mutex> lock(write_mutex_);
+  if (closed_) { return false; }
+  return detail::write_websocket_frame(strm_, op, data, len, fin, !is_server_);
+}
+
+inline ReadResult WebSocket::read(std::string &msg) {
+  while (!closed_) {
+    Opcode opcode;
+    std::string payload;
+    bool fin;
+
+    if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
+                                    CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
+      closed_ = true;
+      return Fail;
+    }
+
+    switch (opcode) {
+    case Opcode::Ping: {
+      std::lock_guard<std::mutex> lock(write_mutex_);
+      detail::write_websocket_frame(strm_, Opcode::Pong, payload.data(),
+                                    payload.size(), true, !is_server_);
+      continue;
+    }
+    case Opcode::Pong: {
+      std::lock_guard<std::mutex> lock(ping_mutex_);
+      unacked_pings_ = 0;
+      continue;
+    }
+    case Opcode::Close: {
+      if (!closed_.exchange(true)) {
+        // Echo close frame back
+        std::lock_guard<std::mutex> lock(write_mutex_);
+        detail::write_websocket_frame(strm_, Opcode::Close, payload.data(),
+                                      payload.size(), true, !is_server_);
+      }
+      return Fail;
+    }
+    case Opcode::Text:
+    case Opcode::Binary: {
+      auto result = opcode == Opcode::Text ? Text : Binary;
+      msg = std::move(payload);
+
+      // Handle fragmentation
+      if (!fin) {
+        while (true) {
+          Opcode cont_opcode;
+          std::string cont_payload;
+          bool cont_fin;
+          if (!impl::read_websocket_frame(
+                  strm_, cont_opcode, cont_payload, cont_fin, is_server_,
+                  CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
+            closed_ = true;
+            return Fail;
+          }
+          if (cont_opcode == Opcode::Ping) {
+            std::lock_guard<std::mutex> lock(write_mutex_);
+            detail::write_websocket_frame(
+                strm_, Opcode::Pong, cont_payload.data(), cont_payload.size(),
+                true, !is_server_);
+            continue;
+          }
+          if (cont_opcode == Opcode::Pong) {
+            std::lock_guard<std::mutex> lock(ping_mutex_);
+            unacked_pings_ = 0;
+            continue;
+          }
+          if (cont_opcode == Opcode::Close) {
+            if (!closed_.exchange(true)) {
+              std::lock_guard<std::mutex> lock(write_mutex_);
+              detail::write_websocket_frame(
+                  strm_, Opcode::Close, cont_payload.data(),
+                  cont_payload.size(), true, !is_server_);
+            }
+            return Fail;
+          }
+          // RFC 6455: continuation frames must use opcode 0x0
+          if (cont_opcode != Opcode::Continuation) {
+            closed_ = true;
+            return Fail;
+          }
+          msg += cont_payload;
+          if (msg.size() > CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) {
+            closed_ = true;
+            return Fail;
+          }
+          if (cont_fin) { break; }
+        }
+      }
+      // RFC 6455 Section 5.6: text frames must contain valid UTF-8
+      if (result == Text && !impl::is_valid_utf8(msg)) {
+        close(CloseStatus::InvalidPayload, "invalid UTF-8");
+        return Fail;
+      }
+      return result;
+    }
+    default: closed_ = true; return Fail;
+    }
+  }
+  return Fail;
+}
+
+inline bool WebSocket::send(const std::string &data) {
+  return send_frame(Opcode::Text, data.data(), data.size());
+}
+
+inline bool WebSocket::send(const char *data, size_t len) {
+  return send_frame(Opcode::Binary, data, len);
+}
+
+inline void WebSocket::close(CloseStatus status, const std::string &reason) {
+  if (closed_.exchange(true)) { return; }
+  ping_cv_.notify_all();
+  std::string payload;
+  auto code = static_cast<uint16_t>(status);
+  payload.push_back(static_cast<char>((code >> 8) & 0xFF));
+  payload.push_back(static_cast<char>(code & 0xFF));
+  // RFC 6455 Section 5.5: control frame payload must not exceed 125 bytes
+  // Close frame has 2-byte status code, so reason is limited to 123 bytes
+  payload += reason.substr(0, 123);
+  {
+    std::lock_guard<std::mutex> lock(write_mutex_);
+    detail::write_websocket_frame(strm_, Opcode::Close, payload.data(),
+                                  payload.size(), true, !is_server_);
+  }
+
+  // RFC 6455 Section 7.1.1: after sending a Close frame, wait for the peer's
+  // Close response before closing the TCP connection. Use a short timeout to
+  // avoid hanging if the peer doesn't respond.
+  strm_.set_read_timeout(CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND, 0);
+  Opcode op;
+  std::string resp;
+  bool fin;
+  while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) {
+    if (op == Opcode::Close) { break; }
+  }
+}
+
+inline WebSocket::~WebSocket() {
+  {
+    std::lock_guard<std::mutex> lock(ping_mutex_);
+    closed_ = true;
+  }
+  ping_cv_.notify_all();
+  if (ping_thread_.joinable()) { ping_thread_.join(); }
+}
+
+inline void WebSocket::start_heartbeat() {
+  if (ping_interval_sec_ == 0) { return; }
+  ping_thread_ = std::thread([this]() {
+    std::unique_lock<std::mutex> lock(ping_mutex_);
+    while (!closed_) {
+      ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
+      if (closed_) { break; }
+      // If the peer has failed to respond to the previous pings, give up.
+      // RFC 6455 does not define a pong-timeout mechanism; this is an
+      // opt-in liveness check controlled by max_missed_pongs_.
+      if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) {
+        lock.unlock();
+        close(CloseStatus::GoingAway, "pong timeout");
+        return;
+      }
+      lock.unlock();
+      if (!send_frame(Opcode::Ping, nullptr, 0)) {
+        lock.lock();
+        closed_ = true;
+        break;
+      }
+      lock.lock();
+      unacked_pings_++;
+    }
+  });
+}
+
+inline const Request &WebSocket::request() const { return req_; }
+
+inline bool WebSocket::is_open() const { return !closed_; }
+
+// WebSocketClient implementation
+inline WebSocketClient::WebSocketClient(
+    const std::string &scheme_host_port_path, const Headers &headers)
+    : headers_(headers) {
+  detail::UrlComponents uc;
+  if (detail::parse_url(scheme_host_port_path, uc) && !uc.scheme.empty() &&
+      !uc.host.empty() && !uc.path.empty()) {
+    auto &scheme = uc.scheme;
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    if (scheme != "ws" && scheme != "wss") {
+#else
+    if (scheme != "ws") {
+#endif
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+      std::string msg = "'" + scheme + "' scheme is not supported.";
+      throw std::invalid_argument(msg);
+#endif
+      return;
+    }
+
+    auto is_ssl = scheme == "wss";
+
+    host_ = std::move(uc.host);
+
+    port_ = is_ssl ? 443 : 80;
+    if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; }
+
+    path_ = std::move(uc.path);
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+    is_ssl_ = is_ssl;
+#else
+    if (is_ssl) { return; }
+#endif
+
+    is_valid_ = true;
+  }
+}
+
+inline WebSocketClient::~WebSocketClient() { shutdown_and_close(); }
+
+inline bool WebSocketClient::is_valid() const { return is_valid_; }
+
+inline void WebSocketClient::shutdown_and_close() {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if (is_ssl_) {
+    if (tls_session_) {
+      tls::shutdown(tls_session_, true);
+      tls::free_session(tls_session_);
+      tls_session_ = nullptr;
+    }
+    if (tls_ctx_) {
+      tls::free_context(tls_ctx_);
+      tls_ctx_ = nullptr;
+    }
+  }
+#endif
+  if (ws_ && ws_->is_open()) { ws_->close(); }
+  ws_.reset();
+  if (sock_ != INVALID_SOCKET) {
+    detail::shutdown_socket(sock_);
+    detail::close_socket(sock_);
+    sock_ = INVALID_SOCKET;
+  }
+}
+
+inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm) {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+  if (is_ssl_) {
+    if (!detail::setup_client_tls_session(
+            host_, tls_ctx_, tls_session_, sock_,
+            server_certificate_verification_, ca_cert_file_path_,
+            ca_cert_store_, read_timeout_sec_, read_timeout_usec_)) {
+      return false;
+    }
+
+    strm = std::unique_ptr<Stream>(new detail::SSLSocketStream(
+        sock_, tls_session_, read_timeout_sec_, read_timeout_usec_,
+        write_timeout_sec_, write_timeout_usec_));
+    return true;
+  }
+#endif
+  strm = std::unique_ptr<Stream>(
+      new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_,
+                               write_timeout_sec_, write_timeout_usec_));
+  return true;
+}
+
+inline bool WebSocketClient::connect() {
+  if (!is_valid_) { return false; }
+  shutdown_and_close();
+
+  Error error;
+  sock_ = detail::create_client_socket(
+      host_, std::string(), port_, address_family_, tcp_nodelay_, ipv6_v6only_,
+      socket_options_, connection_timeout_sec_, connection_timeout_usec_,
+      read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
+      write_timeout_usec_, interface_, error);
+
+  if (sock_ == INVALID_SOCKET) { return false; }
+
+  std::unique_ptr<Stream> strm;
+  if (!create_stream(strm)) {
+    shutdown_and_close();
+    return false;
+  }
+
+  std::string selected_subprotocol;
+  if (!detail::perform_websocket_handshake(*strm, host_, port_, path_, headers_,
+                                           selected_subprotocol)) {
+    shutdown_and_close();
+    return false;
+  }
+  subprotocol_ = std::move(selected_subprotocol);
+
+  Request req;
+  req.method = "GET";
+  req.path = path_;
+  ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
+                                                 websocket_ping_interval_sec_,
+                                                 websocket_max_missed_pongs_));
+  return true;
+}
+
+inline ReadResult WebSocketClient::read(std::string &msg) {
+  if (!ws_) { return Fail; }
+  return ws_->read(msg);
+}
+
+inline bool WebSocketClient::send(const std::string &data) {
+  if (!ws_) { return false; }
+  return ws_->send(data);
+}
+
+inline bool WebSocketClient::send(const char *data, size_t len) {
+  if (!ws_) { return false; }
+  return ws_->send(data, len);
+}
+
+inline void WebSocketClient::close(CloseStatus status,
+                                   const std::string &reason) {
+  if (ws_) { ws_->close(status, reason); }
+}
+
+inline bool WebSocketClient::is_open() const { return ws_ && ws_->is_open(); }
+
+inline const std::string &WebSocketClient::subprotocol() const {
+  return subprotocol_;
+}
+
+inline void WebSocketClient::set_read_timeout(time_t sec, time_t usec) {
+  read_timeout_sec_ = sec;
+  read_timeout_usec_ = usec;
+}
+
+inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
+  write_timeout_sec_ = sec;
+  write_timeout_usec_ = usec;
+}
+
+inline void WebSocketClient::set_websocket_ping_interval(time_t sec) {
+  websocket_ping_interval_sec_ = sec;
+}
+
+inline void WebSocketClient::set_websocket_max_missed_pongs(int count) {
+  websocket_max_missed_pongs_ = count;
+}
+
+inline void WebSocketClient::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
+
+inline void WebSocketClient::set_address_family(int family) {
+  address_family_ = family;
+}
+
+inline void WebSocketClient::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; }
+
+inline void WebSocketClient::set_socket_options(SocketOptions socket_options) {
+  socket_options_ = std::move(socket_options);
+}
+
+inline void WebSocketClient::set_connection_timeout(time_t sec, time_t usec) {
+  connection_timeout_sec_ = sec;
+  connection_timeout_usec_ = usec;
+}
+
+inline void WebSocketClient::set_interface(const std::string &intf) {
+  interface_ = intf;
+}
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+
+inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
+  ca_cert_file_path_ = path;
+}
+
+inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
+  ca_cert_store_ = store;
+}
+
+inline void
+WebSocketClient::enable_server_certificate_verification(bool enabled) {
+  server_certificate_verification_ = enabled;
+}
+
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+} // namespace ws
+
+// ----------------------------------------------------------------------------
+
+} // namespace httplib
+
+#endif // CPPHTTPLIB_HTTPLIB_H

+ 255 - 0
third_party/hyper_lpr_sdk.h

@@ -0,0 +1,255 @@
+//
+// Created by tunm on 2023/1/25.
+//
+
+#ifndef ZEPHYRLPR_HYPER_LPR_SDK_H
+#define ZEPHYRLPR_HYPER_LPR_SDK_H
+
+#include <stdint.h>
+
+#if defined(_WIN32)
+#ifdef HYPER_BUILD_SHARED_LIB
+#define HYPER_CAPI_EXPORT __declspec(dllexport)
+#else
+#define HYPER_CAPI_EXPORT
+#endif
+#else
+#define HYPER_CAPI_EXPORT __attribute__((visibility("default")))
+#endif // _WIN32
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * API Result - API调用结果
+ * */
+typedef int HREESULT;
+
+typedef enum HResultCode{
+    Ok = 0,
+    Err = -1,
+} HResultCode;
+
+/**
+ * camera stream format - 支持的相机流格式
+ * Contains several common camera stream formats on the market -
+ * 包含了几款市面上常见的相机流格式
+ */
+typedef enum HLPR_ImageFormat {
+    STREAM_RGB = 0,             ///< Image in RGB format - RGB排列格式的图像
+    STREAM_BGR = 1,             ///< Image in BGR format (Opencv Mat default) - BGR排列格式的图像(OpenCV的Mat默认)
+    STREAM_RGBA = 2,            ///< Image in RGB with alpha channel format - 带alpha通道的RGB排列格式的图像
+    STREAM_BGRA = 3,            ///< Image in BGR with alpha channel format - 带alpha通道的BGR排列格式的图像
+    STREAM_YUV_NV12 = 4,        ///< Image in YUV NV12 format - YUV NV12排列的图像格式
+    STREAM_YUV_NV21 = 5,        ///< Image in YUV NV21 format - YUV NV21排列的图像格式
+} HLPR_ImageFormat;
+
+
+/**
+ * Camera picture corner mode - 相机画面转角模式
+ * To cope with the rotation of some devices, four image rotation modes are provided here -
+ * 为应对某些设备的画面自带旋转,这里提供四种图像旋转模式
+ */
+typedef enum HLPR_Rotation {
+    CAMERA_ROTATION_0 = 0,      ///< 0 degree - 0
+    CAMERA_ROTATION_90 = 1,     ///< 90 degree - 90
+    CAMERA_ROTATION_180 = 2,    ///< 180 degree - 180
+    CAMERA_ROTATION_270 = 3,    ///< 270 degree - 270
+} HLPR_Rotation;
+
+/**
+ * Image Buffer Data struct - 图像数据流结构
+ * */
+typedef struct HLPR_ImageData {
+    uint8_t *data;                      ///< Image data stream - 图像数据流
+    int width;                          ///< Width of the image - 宽
+    int height;                         ///< Height of the image - 高
+    HLPR_ImageFormat format;            ///< Format of the image - 传入需要解析数据流格式
+    HLPR_Rotation rotation;             ///< The rotation Angle of the image - 图像的画面旋转角角度
+} HLPR_ImageData, *P_HLPR_ImageData;
+
+
+/**
+ * Plate layers - 车牌层数
+ * */
+typedef enum HLPR_PlateLayers {
+    PLATE_LAYERS_MONO = 0,       ///< 单层车牌
+    PLATE_LAYERS_DOUBLE,     ///< 双层车牌
+} HLPR_Layers;
+
+
+/**
+ * Detector Level - 检测器等级
+ * */
+typedef enum HLPR_DetectLevel {
+    DETECT_LEVEL_LOW = 0,       ///< 高开销检测模式 (推荐)
+    DETECT_LEVEL_HIGH,          ///< 低开销检测模式
+} HLPR_DetectLevel;
+
+/**
+ * PlateType Type - 车牌类型(中国)
+ * */
+typedef enum HLPR_PlateType {
+    PLATE_TYPE_UNKNOWN = -1,                         ///< 未知车牌
+    PLATE_TYPE_BLUE = 0,                             ///< 蓝牌
+    PLATE_TYPE_YELLOW_SINGLE = 1,                    ///< 黄牌单层
+    PLATE_TYPE_WHILE_SINGLE = 2,                     ///< 白牌单层
+    PLATE_TYPE_GREEN = 3,                            ///< 绿牌新能源
+    PLATE_TYPE_BLACK_HK_MACAO = 4,                   ///< 黑牌港澳
+    PLATE_TYPE_HK_SINGLE = 5,                        ///< 香港单层
+    PLATE_TYPE_HK_DOUBLE = 6,                        ///< 香港双层
+    PLATE_TYPE_MACAO_SINGLE = 7,                     ///< 澳门单层
+    PLATE_TYPE_MACAO_DOUBLE = 8,                     ///< 澳门双层
+    PLATE_TYPE_YELLOW_DOUBLE = 9,                    ///< 黄牌双层
+} HLPR_PlateType;
+
+/**
+ * Plate Result - 车牌检测结果
+ * */
+typedef struct HLPR_PlateResult {
+    float x1;                                       ///< 左上角点x坐标
+    float y1;                                       ///< 左上角点y坐标
+    float x2;                                       ///< 右下角点x坐标
+    float y2;                                       ///< 右下角点y坐标
+    HLPR_PlateType type;                           ///< 车牌类型
+    float text_confidence;                           ///< 置信度
+    char code[128];                                ///< 车牌号码字符串
+} HLPR_PlateResult, *P_HLPR_PlateResult;
+
+/**
+ * Plate Result List - 车牌检测结果列表
+ * */
+typedef struct HLPR_PlateResultList {
+    unsigned long plate_size;                       ///< 车牌数量
+    P_HLPR_PlateResult plates;                      ///< 检测车牌结果列表
+} HLPR_PlateResultList, *P_HLPR_PlateResultList;
+
+/**
+ * HyperLPR Context Instantiating parameters - Context的实例化参数对象
+ * */
+typedef struct HLPR_ContextConfiguration {
+    char *models_path;                              ///< 模型文件地址
+    int max_num;                                    ///< 识别最大数量
+    int threads;                                    ///< 线程数 (推荐1)
+    bool use_half;                                  ///< 是否使用半精度推理模式
+    float box_conf_threshold;                        ///< 检测框阈值
+    float nms_threshold;                             ///< 非极大值抑制阈值
+    float rec_confidence_threshold;                   ///< 识别置信度阈值
+    HLPR_DetectLevel det_level;                     ///< 检测器等级(推荐low)
+} HLPR_ContextConfiguration, *P_HLPR_ContextConfiguration;
+
+/**
+ * Data Buffer - 数据缓冲流
+ * */
+typedef struct HLPR_DataBuffer HLPR_DataBuffer, *P_HLPR_DataBuffer;
+
+/**
+ * The runtime object after HyperLPR is instantiated - 实例化运行时的Context对象
+ * */
+typedef struct HLPR_Context HLPR_Context, *P_HLPR_Context;
+
+/************************************************************************
+* Carry parameters to create a data buffer stream instantiation object.
+* 携带创建数据缓冲流实例化对象.
+* [out] return: Model instant handle - 返回实例化后的指针句柄
+************************************************************************/
+HYPER_CAPI_EXPORT extern P_HLPR_DataBuffer HLPR_CreateDataBuffer(
+        P_HLPR_ImageData data       // [in] Image Buffer Data struct - 图像数据流结构
+);
+
+/************************************************************************
+* Create a data buffer stream instantiation object.
+* 创建数据缓冲流实例化对象.
+* [out] return: Model instant handle - 返回实例化后的指针句柄
+************************************************************************/
+HYPER_CAPI_EXPORT extern P_HLPR_DataBuffer HLPR_CreateDataBufferEmpty();
+
+/************************************************************************
+* Set the DataBuffer rotation mode.
+* 设置DataBuffer旋转模式.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_DataBufferSetData(
+        P_HLPR_DataBuffer buffer,               // [in] DataBuffer handle - 相机流组件的句柄指针
+        const uint8_t *data,                    // [in] Raw data stream - 原始的数据流
+        int width,                              // [in] Image width - 图像宽度
+        int height                              // [in] Image height - 图像高度
+);
+
+/************************************************************************
+* Set the DataBuffer data format.
+* 设置DataBuffer数据格式.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_DataBufferSetRotationMode(
+        P_HLPR_DataBuffer buffer,              // [in] DataBuffer handle - 数据流组件的句柄指针
+        HLPR_Rotation mode                     // [in] DataBuffer mode - 数据流组件旋转模式
+);
+
+/************************************************************************
+* Set the DataBuffer rotation mode.
+* 设置DataBuffer旋转模式.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_DataBufferSetStreamFormat(
+        P_HLPR_DataBuffer buffer,            // [in] DataBuffer handle - 数据流组件的句柄指针
+        HLPR_ImageFormat mode                // [in] DataBuffer data format - 数据流组件数据格式
+);
+
+/************************************************************************
+* Releases the DataBuffer object that has been instantiated.
+* 释放已经被实例化后的模型对象.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_ReleaseDataBuffer(
+        P_HLPR_DataBuffer buffer             // [in] DataBuffer handle - 相机流组件的句柄指针
+);
+
+/************************************************************************
+* Create a data Context instantiation object.
+* 创建Context实例化对象.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern P_HLPR_Context HLPR_CreateContext(
+        P_HLPR_ContextConfiguration configuration       // [in] Context configuration - 配置表
+);
+
+/************************************************************************
+* Query the Context instantiation state.
+* 查询实例化后的状态.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_ContextQueryStatus(
+        P_HLPR_Context ctx                      // [in] Context handle - Context的指针句柄
+);
+
+/************************************************************************
+* Update Data Buffer Stream.
+* 喂入数据流并更新进行车牌识别.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_ContextUpdateStream(
+        P_HLPR_Context ctx,                     // [in] Context handle - Context的指针句柄
+        P_HLPR_DataBuffer buffer,               // [in] DataBuffer handle - 数据流组件的句柄指针
+        P_HLPR_PlateResultList results          // [out] Results List - 返回结果的列表
+);
+
+/************************************************************************
+* Release Context.
+* 释放Context的实例化对象.
+* [out] Result Code - 返回结果码
+************************************************************************/
+HYPER_CAPI_EXPORT extern HREESULT HLPR_ReleaseContext(
+        P_HLPR_Context ctx                     // [in] Context handle - Context的指针句柄
+);
+
+//HYPER_CAPI_EXPORT extern HREESULT HLPR_DataBufferTest(P_HLPR_DataBuffer buffer, const char *save_path);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif //ZEPHYRLPR_HYPER_LPR_SDK_H

+ 333 - 0
third_party/ini.c

@@ -0,0 +1,333 @@
+/* inih -- simple .INI file parser
+
+SPDX-License-Identifier: BSD-3-Clause
+
+Copyright (C) 2009-2025, Ben Hoyt
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <assert.h>
+
+#include "ini.h"
+
+#if !INI_USE_STACK
+#if INI_CUSTOM_ALLOCATOR
+#include <stddef.h>
+void* ini_malloc(size_t size);
+void ini_free(void* ptr);
+void* ini_realloc(void* ptr, size_t size);
+#else
+#include <stdlib.h>
+#define ini_malloc malloc
+#define ini_free free
+#define ini_realloc realloc
+#endif
+#endif
+
+#define MAX_SECTION 50
+#define MAX_NAME 50
+
+/* Used by ini_parse_string() to keep track of string parsing state. */
+typedef struct {
+    const char* ptr;
+    size_t num_left;
+} ini_parse_string_ctx;
+
+/* Strip whitespace chars off end of given string, in place. end must be a
+   pointer to the NUL terminator at the end of the string. Return s. */
+static char* ini_rstrip(char* s, char* end)
+{
+    while (end > s && isspace((unsigned char)(*--end)))
+        *end = '\0';
+    return s;
+}
+
+/* Return pointer to first non-whitespace char in given string. */
+static char* ini_lskip(const char* s)
+{
+    while (*s && isspace((unsigned char)(*s)))
+        s++;
+    return (char*)s;
+}
+
+/* Return pointer to first char (of chars) or inline comment in given string,
+   or pointer to NUL at end of string if neither found. Inline comment must
+   be prefixed by a whitespace character to register as a comment. */
+static char* ini_find_chars_or_comment(const char* s, const char* chars)
+{
+#if INI_ALLOW_INLINE_COMMENTS
+    int was_space = 0;
+    while (*s && (!chars || !strchr(chars, *s)) &&
+           !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
+        was_space = isspace((unsigned char)(*s));
+        s++;
+    }
+#else
+    while (*s && (!chars || !strchr(chars, *s))) {
+        s++;
+    }
+#endif
+    return (char*)s;
+}
+
+/* Similar to strncpy, but ensures dest (size bytes) is
+   NUL-terminated, and doesn't pad with NULs. */
+static char* ini_strncpy0(char* dest, const char* src, size_t size)
+{
+    /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
+    size_t i;
+    for (i = 0; i < size - 1 && src[i]; i++)
+        dest[i] = src[i];
+    dest[i] = '\0';
+    return dest;
+}
+
+/* See documentation in header file. */
+int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user)
+{
+    /* Uses a fair bit of stack (use heap instead if you need to) */
+#if INI_USE_STACK
+    char line[INI_MAX_LINE];
+    size_t max_line = INI_MAX_LINE;
+#else
+    char* line;
+    size_t max_line = INI_INITIAL_ALLOC;
+#endif
+#if INI_ALLOW_REALLOC && !INI_USE_STACK
+    char* new_line;
+#endif
+    char section[MAX_SECTION] = "";
+#if INI_ALLOW_MULTILINE
+    char prev_name[MAX_NAME] = "";
+#endif
+
+    size_t offset;
+    char* start;
+    char* end;
+    char* name;
+    char* value;
+    int lineno = 0;
+    int error = 0;
+    char abyss[16];  /* Used to consume input when a line is too long. */
+    size_t abyss_len;
+
+    assert(reader != NULL);
+    assert(stream != NULL);
+    assert(handler != NULL);
+
+#if !INI_USE_STACK
+    line = (char*)ini_malloc(INI_INITIAL_ALLOC);
+    if (!line) {
+        return -2;
+    }
+#endif
+
+#if INI_HANDLER_LINENO
+#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
+#else
+#define HANDLER(u, s, n, v) handler(u, s, n, v)
+#endif
+
+    /* Scan through stream line by line */
+    while (reader(line, (int)max_line, stream) != NULL) {
+        offset = strlen(line);
+
+#if INI_ALLOW_REALLOC && !INI_USE_STACK
+        while (max_line < INI_MAX_LINE &&
+               offset == max_line - 1 && line[offset - 1] != '\n') {
+            max_line *= 2;
+            if (max_line > INI_MAX_LINE)
+                max_line = INI_MAX_LINE;
+            new_line = ini_realloc(line, max_line);
+            if (!new_line) {
+                ini_free(line);
+                return -2;
+            }
+            line = new_line;
+            if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
+                break;
+            offset += strlen(line + offset);
+        }
+#endif
+
+        lineno++;
+
+        /* If line exceeded INI_MAX_LINE bytes, discard till end of line. */
+        if (offset == max_line - 1 && line[offset - 1] != '\n') {
+            while (reader(abyss, sizeof(abyss), stream) != NULL) {
+                if (!error)
+                    error = lineno;
+                abyss_len = strlen(abyss);
+                if (abyss_len > 0 && abyss[abyss_len - 1] == '\n')
+                    break;
+            }
+        }
+
+        start = line;
+#if INI_ALLOW_BOM
+        if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
+                           (unsigned char)start[1] == 0xBB &&
+                           (unsigned char)start[2] == 0xBF) {
+            start += 3;
+        }
+#endif
+        start = ini_rstrip(ini_lskip(start), line + offset);
+
+        if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
+            /* Start-of-line comment */
+        }
+#if INI_ALLOW_MULTILINE
+        else if (*prev_name && *start && start > line) {
+#if INI_ALLOW_INLINE_COMMENTS
+            end = ini_find_chars_or_comment(start, NULL);
+            *end = '\0';
+            ini_rstrip(start, end);
+#endif
+            /* Non-blank line with leading whitespace, treat as continuation
+               of previous name's value (as per Python configparser). */
+            if (!HANDLER(user, section, prev_name, start) && !error)
+                error = lineno;
+        }
+#endif
+        else if (*start == '[') {
+            /* A "[section]" line */
+            end = ini_find_chars_or_comment(start + 1, "]");
+            if (*end == ']') {
+                *end = '\0';
+                ini_strncpy0(section, start + 1, sizeof(section));
+#if INI_ALLOW_MULTILINE
+                *prev_name = '\0';
+#endif
+#if INI_CALL_HANDLER_ON_NEW_SECTION
+                if (!HANDLER(user, section, NULL, NULL) && !error)
+                    error = lineno;
+#endif
+            }
+            else if (!error) {
+                /* No ']' found on section line */
+                error = lineno;
+            }
+        }
+        else if (*start) {
+            /* Not a comment, must be a name[=:]value pair */
+            end = ini_find_chars_or_comment(start, "=:");
+            if (*end == '=' || *end == ':') {
+                *end = '\0';
+                name = ini_rstrip(start, end);
+                value = end + 1;
+#if INI_ALLOW_INLINE_COMMENTS
+                end = ini_find_chars_or_comment(value, NULL);
+                *end = '\0';
+#endif
+                value = ini_lskip(value);
+                ini_rstrip(value, end);
+
+#if INI_ALLOW_MULTILINE
+                ini_strncpy0(prev_name, name, sizeof(prev_name));
+#endif
+                /* Valid name[=:]value pair found, call handler */
+                if (!HANDLER(user, section, name, value) && !error)
+                    error = lineno;
+            }
+            else {
+                /* No '=' or ':' found on name[=:]value line */
+#if INI_ALLOW_NO_VALUE
+                *end = '\0';
+                name = ini_rstrip(start, end);
+                if (!HANDLER(user, section, name, NULL) && !error)
+                    error = lineno;
+#else
+                if (!error)
+                    error = lineno;
+#endif
+            }
+        }
+
+#if INI_STOP_ON_FIRST_ERROR
+        if (error)
+            break;
+#endif
+    }
+
+#if !INI_USE_STACK
+    ini_free(line);
+#endif
+
+    return error;
+}
+
+/* See documentation in header file. */
+int ini_parse_file(FILE* file, ini_handler handler, void* user)
+{
+    return ini_parse_stream((ini_reader)fgets, file, handler, user);
+}
+
+/* See documentation in header file. */
+int ini_parse(const char* filename, ini_handler handler, void* user)
+{
+    FILE* file;
+    int error;
+
+    file = fopen(filename, "r");
+    if (!file)
+        return -1;
+    error = ini_parse_file(file, handler, user);
+    fclose(file);
+    return error;
+}
+
+/* An ini_reader function to read the next line from a string buffer. This
+   is the fgets() equivalent used by ini_parse_string(). */
+static char* ini_reader_string(char* str, int num, void* stream) {
+    ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
+    const char* ctx_ptr = ctx->ptr;
+    size_t ctx_num_left = ctx->num_left;
+    char* strp = str;
+    char c;
+
+    if (ctx_num_left == 0 || num < 2)
+        return NULL;
+
+    while (num > 1 && ctx_num_left != 0) {
+        c = *ctx_ptr++;
+        ctx_num_left--;
+        *strp++ = c;
+        if (c == '\n')
+            break;
+        num--;
+    }
+
+    *strp = '\0';
+    ctx->ptr = ctx_ptr;
+    ctx->num_left = ctx_num_left;
+    return str;
+}
+
+/* See documentation in header file. */
+int ini_parse_string(const char* string, ini_handler handler, void* user) {
+    return ini_parse_string_length(string, strlen(string), handler, user);
+}
+
+/* See documentation in header file. */
+int ini_parse_string_length(const char* string, size_t length,
+                            ini_handler handler, void* user) {
+    ini_parse_string_ctx ctx;
+
+    ctx.ptr = string;
+    ctx.num_left = length;
+    return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
+                            user);
+}

+ 189 - 0
third_party/ini.h

@@ -0,0 +1,189 @@
+/* inih -- simple .INI file parser
+
+SPDX-License-Identifier: BSD-3-Clause
+
+Copyright (C) 2009-2025, Ben Hoyt
+
+inih is released under the New BSD license (see LICENSE.txt). Go to the project
+home page for more info:
+
+https://github.com/benhoyt/inih
+
+*/
+
+#ifndef INI_H
+#define INI_H
+
+/* Make this header file easier to include in C++ code */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/* Nonzero if ini_handler callback should accept lineno parameter. */
+#ifndef INI_HANDLER_LINENO
+#define INI_HANDLER_LINENO 0
+#endif
+
+/* Visibility symbols, required for Windows DLLs */
+#ifndef INI_API
+#if defined _WIN32 || defined __CYGWIN__
+#	ifdef INI_SHARED_LIB
+#		ifdef INI_SHARED_LIB_BUILDING
+#			define INI_API __declspec(dllexport)
+#		else
+#			define INI_API __declspec(dllimport)
+#		endif
+#	else
+#		define INI_API
+#	endif
+#else
+#	if defined(__GNUC__) && __GNUC__ >= 4
+#		define INI_API __attribute__ ((visibility ("default")))
+#	else
+#		define INI_API
+#	endif
+#endif
+#endif
+
+/* Typedef for prototype of handler function.
+
+   Note that even though the value parameter has type "const char*", the user
+   may cast to "char*" and modify its content, as the value is not used again
+   after the call to ini_handler. This is not true of section and name --
+   those must not be modified.
+*/
+#if INI_HANDLER_LINENO
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value,
+                           int lineno);
+#else
+typedef int (*ini_handler)(void* user, const char* section,
+                           const char* name, const char* value);
+#endif
+
+/* Typedef for prototype of fgets-style reader function. */
+typedef char* (*ini_reader)(char* str, int num, void* stream);
+
+/* Parse given INI-style file. May have [section]s, name=value pairs
+   (whitespace stripped), and comments starting with ';' (semicolon). Section
+   is "" if name=value pair parsed before any section heading. name:value
+   pairs are also supported as a concession to Python's configparser.
+
+   For each name=value pair parsed, call handler function with given user
+   pointer as well as section, name, and value (data only valid for duration
+   of handler call). Handler should return nonzero on success, zero on error.
+
+   Returns 0 on success, line number of first error on parse error (doesn't
+   stop on first error), -1 on file open error, or -2 on memory allocation
+   error (only when INI_USE_STACK is zero).
+*/
+INI_API int ini_parse(const char* filename, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
+   close the file when it's finished -- the caller must do that. */
+INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user);
+
+/* Same as ini_parse(), but takes an ini_reader function pointer instead of
+   filename. Used for implementing custom or string-based I/O (see also
+   ini_parse_string). */
+INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
+                     void* user);
+
+/* Same as ini_parse(), but takes a zero-terminated string with the INI data
+   instead of a file. Useful for parsing INI data from a network socket or
+   which is already in memory. */
+INI_API int ini_parse_string(const char* string, ini_handler handler, void* user);
+
+/* Same as ini_parse_string(), but takes a string and its length, avoiding
+   strlen(). Useful for parsing INI data from a network socket or which is
+   already in memory, or interfacing with C++ std::string_view. */
+INI_API int ini_parse_string_length(const char* string, size_t length, ini_handler handler, void* user);
+
+/* Nonzero to allow multi-line value parsing, in the style of Python's
+   configparser. If allowed, ini_parse() will call the handler with the same
+   name for each subsequent line parsed. */
+#ifndef INI_ALLOW_MULTILINE
+#define INI_ALLOW_MULTILINE 1
+#endif
+
+/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
+   the file. See https://github.com/benhoyt/inih/issues/21 */
+#ifndef INI_ALLOW_BOM
+#define INI_ALLOW_BOM 1
+#endif
+
+/* Chars that begin a start-of-line comment. Per Python configparser, allow
+   both ; and # comments at the start of a line by default. */
+#ifndef INI_START_COMMENT_PREFIXES
+#define INI_START_COMMENT_PREFIXES ";#"
+#endif
+
+/* Nonzero to allow inline comments (with valid inline comment characters
+   specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
+   Python 3.2+ configparser behaviour. */
+#ifndef INI_ALLOW_INLINE_COMMENTS
+#define INI_ALLOW_INLINE_COMMENTS 1
+#endif
+#ifndef INI_INLINE_COMMENT_PREFIXES
+#define INI_INLINE_COMMENT_PREFIXES ";"
+#endif
+
+/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
+#ifndef INI_USE_STACK
+#define INI_USE_STACK 1
+#endif
+
+/* Maximum line length for any line in INI file (stack or heap). Note that
+   this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
+#ifndef INI_MAX_LINE
+#define INI_MAX_LINE 200
+#endif
+
+/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
+   fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
+   zero. */
+#ifndef INI_ALLOW_REALLOC
+#define INI_ALLOW_REALLOC 0
+#endif
+
+/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
+   is zero. */
+#ifndef INI_INITIAL_ALLOC
+#define INI_INITIAL_ALLOC 200
+#endif
+
+/* Stop parsing on first error (default is to keep parsing). */
+#ifndef INI_STOP_ON_FIRST_ERROR
+#define INI_STOP_ON_FIRST_ERROR 0
+#endif
+
+/* Nonzero to call the handler at the start of each new section (with
+   name and value NULL). Default is to only call the handler on
+   each name=value pair. */
+#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
+#define INI_CALL_HANDLER_ON_NEW_SECTION 0
+#endif
+
+/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
+   call the handler with value NULL in this case. Default is to treat
+   no-value lines as an error. */
+#ifndef INI_ALLOW_NO_VALUE
+#define INI_ALLOW_NO_VALUE 0
+#endif
+
+/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory
+   allocation functions (INI_USE_STACK must also be 0). These functions must
+   have the same signatures as malloc/free/realloc and behave in a similar
+   way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */
+#ifndef INI_CUSTOM_ALLOCATOR
+#define INI_CUSTOM_ALLOCATOR 0
+#endif
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* INI_H */

+ 193 - 0
third_party/md5ex1.c

@@ -0,0 +1,193 @@
+#include "md5ex1.h"
+#include <string.h>
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
+#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
+#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | (~z)))
+#define FF(a, b, c, d, x, s, ac) \
+    { (a) += F((b), (c), (d)) + (x) + (uint32_t)(ac); \
+      (a) = ROTATE_LEFT((a), (s)); \
+      (a) += (b); }
+#define GG(a, b, c, d, x, s, ac) \
+    { (a) += G((b), (c), (d)) + (x) + (uint32_t)(ac); \
+      (a) = ROTATE_LEFT((a), (s)); \
+      (a) += (b); }
+#define HH(a, b, c, d, x, s, ac) \
+    { (a) += H((b), (c), (d)) + (x) + (uint32_t)(ac); \
+      (a) = ROTATE_LEFT((a), (s)); \
+      (a) += (b); }
+#define II(a, b, c, d, x, s, ac) \
+    { (a) += I((b), (c), (d)) + (x) + (uint32_t)(ac); \
+      (a) = ROTATE_LEFT((a), (s)); \
+      (a) += (b); }
+
+// v41.2 修复警告:将block参数改为指针,修复-Wstringop-overread警告
+static void MD5_TransformEx1(uint32_t state[4], const uint8_t* block) {
+	uint32_t a = state[0], b = state[1], c = state[2], d = state[3], x[16];
+	// v41.2 修复:从指针正确读取64字节
+	const uint8_t* p = (const uint8_t*)block;
+	for (int i = 0; i < 16; i++) {
+		x[i] = (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
+		       ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+		p += 4;
+	}
+	FF(a, b, c, d, x[0], 7, 0xD76AA478);
+	FF(d, a, b, c, x[1], 12, 0xE8C7B756);
+	FF(c, d, a, b, x[2], 17, 0x242070DB);
+	FF(b, c, d, a, x[3], 22, 0xC1BDCEEE);
+	FF(a, b, c, d, x[4], 7, 0xF57C0FAF);
+	FF(d, a, b, c, x[5], 12, 0x4787C62A);
+	FF(c, d, a, b, x[6], 17, 0xA8304613);
+	FF(b, c, d, a, x[7], 22, 0xFD469501);
+	FF(a, b, c, d, x[8], 7, 0x698098D8);
+	FF(d, a, b, c, x[9], 12, 0x8B44F7AF);
+	FF(c, d, a, b, x[10], 17, 0xFFFF5BB1);
+	FF(b, c, d, a, x[11], 22, 0x895CD7BE);
+	FF(a, b, c, d, x[12], 7, 0x6B901122);
+	FF(d, a, b, c, x[13], 12, 0xFD987193);
+	FF(c, d, a, b, x[14], 17, 0xA679438E);
+	FF(b, c, d, a, x[15], 22, 0x49B40821);
+	GG(a, b, c, d, x[1], 5, 0xF61E2562);
+	GG(d, a, b, c, x[6], 9, 0xC040B340);
+	GG(c, d, a, b, x[11], 14, 0x265E5A51);
+	GG(b, c, d, a, x[0], 20, 0xE9B6C7AA);
+	GG(a, b, c, d, x[5], 5, 0xD62F105D);
+	GG(d, a, b, c, x[10], 9, 0x02441453);
+	GG(c, d, a, b, x[15], 14, 0xD8A1E681);
+	GG(b, c, d, a, x[4], 20, 0xE7D3FBC8);
+	GG(a, b, c, d, x[9], 5, 0x21E1CDE6);
+	GG(d, a, b, c, x[14], 9, 0xC33707D6);
+	GG(c, d, a, b, x[3], 14, 0xF4D50D87);
+	GG(b, c, d, a, x[8], 20, 0x455A14ED);
+	GG(a, b, c, d, x[13], 5, 0xA9E3E905);
+	GG(d, a, b, c, x[2], 9, 0xFCEFA3F8);
+	GG(c, d, a, b, x[7], 14, 0x676F02D9);
+	GG(b, c, d, a, x[12], 20, 0x8D2A4C8A);
+	HH(a, b, c, d, x[5], 4, 0xFFFA3942);
+	HH(d, a, b, c, x[8], 11, 0x8771F681);
+	HH(c, d, a, b, x[11], 16, 0x6D9D6122);
+	HH(b, c, d, a, x[14], 23, 0xFDE5380C);
+	HH(a, b, c, d, x[1], 4, 0xA4BEEA44);
+	HH(d, a, b, c, x[4], 11, 0x4BDECFA9);
+	HH(c, d, a, b, x[7], 16, 0xF6BB4B60);
+	HH(b, c, d, a, x[10], 23, 0xBEBFBC70);
+	HH(a, b, c, d, x[13], 4, 0x289B7EC6);
+	HH(d, a, b, c, x[0], 11, 0xEAA127FA);
+	HH(c, d, a, b, x[3], 16, 0xD4EF3085);
+	HH(b, c, d, a, x[6], 23, 0x04881D05);
+	HH(a, b, c, d, x[9], 4, 0xD9D4D039);
+	HH(d, a, b, c, x[12], 11, 0xE6DB99E5);
+	HH(c, d, a, b, x[15], 16, 0x1FA27CF8);
+	HH(b, c, d, a, x[2], 23, 0xC4AC5665);
+	II(a, b, c, d, x[0], 6, 0xF4292244);
+	II(d, a, b, c, x[7], 10, 0x432AFF97);
+	II(c, d, a, b, x[14], 15, 0xAB9423A7);
+	II(b, c, d, a, x[5], 21, 0xFC93A039);
+	II(a, b, c, d, x[12], 6, 0x655B59C3);
+	II(d, a, b, c, x[3], 10, 0x8F0CCC92);
+	II(c, d, a, b, x[10], 15, 0xFFEFF47D);
+	II(b, c, d, a, x[1], 21, 0x85845DD1);
+	II(a, b, c, d, x[8], 6, 0x6FA87E4F);
+	II(d, a, b, c, x[15], 10, 0xFE2CE6E0);
+	II(c, d, a, b, x[6], 15, 0xA3014314);
+	II(b, c, d, a, x[13], 21, 0x4E0811A1);
+	II(a, b, c, d, x[4], 6, 0xF7537E82);
+	II(d, a, b, c, x[11], 10, 0xBD3AF235);
+	II(c, d, a, b, x[2], 15, 0x2AD7D2BB);
+	II(b, c, d, a, x[9], 21, 0xEB86D391);
+	state[0] += a;
+	state[1] += b;
+	state[2] += c;
+	state[3] += d;
+}
+
+void MD5_InitEx1(MD5_CTXEx1* ctx) {
+	ctx->count[0] = ctx->count[1] = 0;
+	ctx->state[0] = 0x67452301;
+	ctx->state[1] = 0xEFCDAB89;
+	ctx->state[2] = 0x98BADCFE;
+	ctx->state[3] = 0x10325476;
+}
+
+void MD5_UpdateEx1(MD5_CTXEx1* ctx, const uint8_t* input, size_t len) {
+	size_t i, index, partLen;
+	index = (ctx->count[0] >> 3) & 0x3F;
+	ctx->count[0] += (uint32_t)(len << 3);
+	if (ctx->count[0] < (len << 3)) ctx->count[1]++;
+	ctx->count[1] += (uint32_t)(len >> 29);
+	partLen = MD5_BLOCK_SIZE - index;
+	
+	if (len >= partLen) {
+		memcpy(&ctx->buffer[index], input, partLen);
+		MD5_TransformEx1(ctx->state, ctx->buffer);
+		
+		// v41.2 修复警告:使用临时缓冲区确保64字节对齐
+		uint8_t block[MD5_BLOCK_SIZE];
+		for (i = partLen; i + MD5_BLOCK_SIZE <= len; i += MD5_BLOCK_SIZE) {
+			memcpy(block, &input[i], MD5_BLOCK_SIZE);
+			MD5_TransformEx1(ctx->state, block);
+		}
+		index = 0;
+	}
+	else {
+		i = 0;
+	}
+	memcpy(&ctx->buffer[index], &input[i], len - i);
+}
+
+void MD5_FinalEx1(uint8_t digest[MD5_DIGEST_SIZE], MD5_CTXEx1* ctx) {
+	uint8_t bits[8];
+	size_t index, padLen;
+	static const uint8_t PADDING[MD5_BLOCK_SIZE] = { 0x80 };
+	
+	for (int i = 0; i < 8; i++)
+		bits[i] = (ctx->count[i >> 2] >> ((i & 3) << 3)) & 0xFF;
+	
+	index = (ctx->count[0] >> 3) & 0x3F;
+	padLen = (index < 56) ? (56 - index) : (120 - index);
+	MD5_UpdateEx1(ctx, PADDING, padLen);
+	MD5_UpdateEx1(ctx, bits, 8);
+	
+	for (int i = 0; i < MD5_DIGEST_SIZE; i++)
+		digest[i] = (ctx->state[i >> 2] >> ((i & 3) << 3)) & 0xFF;
+}
+
+// 字节数组转16进制字符串
+void bytes_to_hexEx1(const uint8_t* bytes, size_t len, char* hex_str) {
+	const char hex_chars[] = "0123456789abcdef";
+	for (size_t i = 0; i < len; i++) {
+		hex_str[i * 2] = hex_chars[(bytes[i] >> 4) & 0xF];
+		hex_str[i * 2 + 1] = hex_chars[bytes[i] & 0xF];
+	}
+	hex_str[len * 2] = '\0';
+}
+
+// MD5 哈希字符串,输出16进制
+void md5_hexEx1(const char* input, char* output) {
+	MD5_CTXEx1 ctx;
+	uint8_t digest[MD5_DIGEST_SIZE];
+	MD5_InitEx1(&ctx);
+	MD5_UpdateEx1(&ctx, (const uint8_t*)input, strlen(input));
+	MD5_FinalEx1(digest, &ctx);
+	bytes_to_hexEx1(digest, MD5_DIGEST_SIZE, output);
+}
+
+// MD5 哈希字节数组,输出16进制
+void md5_bytesEx1(const uint8_t* input, size_t len, char* output) {
+	MD5_CTXEx1 ctx;
+	uint8_t digest[MD5_DIGEST_SIZE];
+	MD5_InitEx1(&ctx);
+	MD5_UpdateEx1(&ctx, input, len);
+	MD5_FinalEx1(digest, &ctx);
+	bytes_to_hexEx1(digest, MD5_DIGEST_SIZE, output);
+}
+
+// 三次 MD5 加密(只返回最终结果)
+void md5_tripleEx1(const char* input, char* output) {
+	char temp[33];
+	md5_hexEx1(input, temp);           // 第一次
+	md5_hexEx1(temp, output);          // 第二次
+	strcpy(temp, output);
+	md5_hexEx1(temp, output);          // 第三次
+}

+ 28 - 0
third_party/md5ex1.h

@@ -0,0 +1,28 @@
+#ifndef MD5_H_EX1
+#define MD5_H_EX1
+#include <stdint.h>
+#include <stddef.h>
+#define MD5_BLOCK_SIZE 64
+#define MD5_DIGEST_SIZE 16
+#ifdef __cplusplus
+extern "C" {
+#endif
+typedef struct {
+    uint32_t state[4];
+    uint32_t count[2];
+    uint8_t buffer[MD5_BLOCK_SIZE];
+} MD5_CTXEx1;
+// 基础MD5函数
+void MD5_InitEx1(MD5_CTXEx1 *ctx);
+void MD5_UpdateEx1(MD5_CTXEx1 *ctx, const uint8_t *input, size_t len);
+void MD5_FinalEx1(uint8_t digest[MD5_DIGEST_SIZE], MD5_CTXEx1 *ctx);
+// 便捷函数
+void md5_hexEx1(const char *input, char *output);
+void md5_bytesEx1(const uint8_t *input, size_t len, char *output);
+void md5_tripleEx1(const char *input, char *output);
+// 工具函数
+void bytes_to_hexEx1(const uint8_t *bytes, size_t len, char *hex_str);
+#ifdef __cplusplus
+}
+#endif
+#endif